Accept several email addresses in a form with the multiple attribute

A popular tweet of mine that’s been doing rounds again (thanks to an RT by Stefan, whom you should definitely follow) is this little tip:

By setting the multiple attribute on an input[type="email"] you can have it accept multiple entries. Each entry is separated by a comma and is validated individually.

Here’s a little demo video of how that works:

💁‍♂️ For a slight moment there you can see that [email protected] is considered valid. As per RFC 822 the [email protected] format — used mainly in local networks — indeed is allowed.

~

However, it was pointed out to me that on iOS this isn’t usable by default:

On iOS, the “email keyboard” looks like this, with no comma to be found (not even when switching to numbers/symbols):

To work around this limitation you can manually override the input to use the regular keyboard by setting the inputmode attribute to text.

That way we still have the built-in browser validation rules (triggered by [type="email"]) and a means to type in a comma (triggered by [inputmode="text"]). Double win!

💁‍♂️ Sidenote: With this inputmode attribute you can create better number inputs.

~

Combining what we know, here’s a full demo for you to play with:

See the Pen
Accepting multiple e-mail addresses in one input
by Bramus (@bramus)
on CodePen.

~

Did this help you out? Like what you see?
Thank me with a coffee.

I don't do this for profit but a small one-time donation would surely put a smile on my face. Thanks!

☕️ Buy me a Coffee (€3)

To stay in the loop you can follow @bramus or follow @bramusblog on Twitter.

Nested Media Queries

I can’t seem to find any mention of this in the Media Queries Module specification, but apparently it’s allowed to nest media queries, as shared by Šime Vidas:

That’s … awesome! 🤯

Fiddling with it a bit more, turns out this snippet also works as expected:

@media not print {
  @media (min-width: 0) {
    p {
      font-weight: bold;
    }
    @media (max-width: 750px) {
      p {
        background: yellow;
      }
    }
  }
}

You can play with this CodePen demo to try it yourself.

💁‍♂️ Don’t confuse Nested Media Queries with CSS Nesting, an upcoming feature of CSS, which allows you to nest selectors.

UPDATE: Thanks to reader Vadim Makeev for pointing out that support for nested @media blocks was added to Opera 12.50 back in 2012! Its syntax is defined in the CSS Conditional Rules Module specification.

🔥 Like what you see? Want to stay in the loop? Here's how:

The future of CSS: Higher Level Custom Properties to control multiple declarations

When using CSS Custom Properties we mainly use them directly as variables in calculations for other properties. Having one CSS Custom Property control a varying set of other properties — such as both colors and numbers — is not exactly possible. There are some hacky workarounds we can use, but these don’t cover all scenarios. Thankfully there’s a new idea popping up: Higher Level Custom Properties. Although still premature, these Higher Level Custom Properties would allow us to drop the hacks.

Let’s take a look at our current options, and how this (possible) future addition to the CSS spec — along with the @if at-rule it introduces — might look …

~

~

# CSS Custom Properties as Variables

When working with CSS Custom Properties today, they are mainly used as CSS Variables. If you’ve used them, you’re quite familiar with code like this:

:root {
    --square-size: 2vw;
    --square-padding: 0.25vw;
}

.square {
    width: var(--square-size);
    padding: var(--square-padding);
    aspect-ratio: 1/1;
}

.square--big {
    --square-size: 16vw;
    --square-padding: 1vw;
}

Using the var() function we create a CSS Variable which gets substituted for the value of the Custom Property it refers to.

E.g. The variable var(--square-size) will hold the value of the --square-size Custom Property — namely 2vw — which is then set as the value for the width CSS property.

🤔 CSS Custom Properties vs. CSS Variables — Is there a difference?

Yes there's a difference:

  • A CSS Custom Property is any property whose name starts with two dashes (U+002D HYPHEN-MINUS), like --foo. Just like with a normal property you can assign a value to it, e.g. --foo: 200;.
  • A CSS Variable is created when the var() function is used. When creating the CSS Variable var(--my-prop), it will be replaced with the value of the --my-prop Custom Property it refers to, namely 200.

~

# Using CSS Custom Properties to affect multiple CSS declarations

In the example above we have two types of squares: regular sized ones and big ones. To differentiate between them we need to toggle the .square--big class. Toggling that class affects two CSS Custom Properties: both --square-size and --square-padding are altered.

But what if we wanted not to toggle a HTML class but a CSS Custom Property to do so? E.g. we want to toggle one CSS Custom Property, and have that automatically affect both --square-size and --square-padding.

As it stands today it’s not very straightforward to let one single CSS Custom Property affect multiple other CSS Properties, unless you resort to some hacky workarounds. Let’s take a look at the options we have today.

~

# Binary Custom Properties

If all you’re setting is numeric values, you can use Binary CSS Custom Properties within calculations. You give these Binary Custom Properties the value of 0 or 1 and use them within your calculations. Think of these Binary Custom Properties like light switches: they can either be OFF/false (0) or ON/true (1).

:root {
    --is-big: 0;
}

.square--big {
    --is-big: 1;
}

.square {
    width: calc(
        2vw * (1 - var(--is-big)) /* Value to apply when --is-big is 0 (~false) */
        +
        16vw * var(--is-big) /* Value to apply when --is-big is 1 (~true): */
    );
    padding: calc(
        0.25vw * (1 - var(--is-big)) /* Value to apply when --is-big is 0 (~false) */
        +
        1vw * var(--is-big) /* Value to apply when --is-big is 1 (~true): */
    );
    aspect-ratio: 1/1;
}

In the example above the --is-big Custom Property acts as a binary toggle that controls the results of the calc() functions. In the case of --is-big having a value of 0 those functions will yield one specific value, while when --is-big is set to 1 it will yield another value.

☝️ With some extra effort you can even perform Logical Operations (AND, NAND, OR, NOR, XOR, …) using CSS Custom Properties!?

Ana Tudor worked out the math for us in Logical Operations with CSS Custom Properties:

:root {
    --j: 1;
    --k: 0;
}

element {
    --notj: calc(1 - var(--j));
    --and: calc(var(--k)*var(--i));
    --nand: calc(1 - var(--k)*var(--i));
    --or: calc(1 - (1 - var(--k))*(1 - var(--i)));
    --nor: calc((1 - var(--k))*(1 - var(--i)));
    --xor: calc((var(--k) - var(--i))*(var(--k) - var(--i)));
}

🤯

~

# The Guaranteed-Invalid Value Hack

When you need to set things other than numeric values — such as colors — you can’t rely on a toggle that is either 0 or 1, as performing calculations with colors is invalid.

.square {
    /* ❌ This won't work! ❌ */
    color: calc(
        hotpink * (1 - var(--is-big))
        +
        lime * var(--is-big)
    );
}

The spec detailing calc() is clear on this:

It can be used wherever <length>, <frequency>, <angle>, <time>, <percentage>, <number>, or <integer> values are allowed.

CSS Values and Units Level 3: 8.1 Mathematical Expressions: `calc()`

What you can do however is use The CSS Custom Property Toggle Trick by James0x57 — which I like to call “The Guaranteed-Invalid Value Hack” — where you set a Custom Property to the “guaranteed-invalid value” of initial to force the var() function to use its fallback value:

If, for whatever reason, one wants to manually reset a variable to the guaranteed-invalid value, using the keyword initial will do this.

CSS Custom Properties for Cascading Variables Module Level 1: 2.2. Guaranteed-Invalid Values

In code it boils down to this:

--my-var: initial; /* initial => var() will use the fallback value */
color: var(--my-var, green); /* ~> green */
--my-var: hotpink; /* Any value other than `initial` (even simply one space!) => var() will not use the fallback value */
color: var(--my-var, green); /* ~> hotpink */

That means that you can flip the switch ON by setting a Custom Property to the value of initial. Here’s an example where the text will turn green and italic once --is-checked is flipped on:

input[type="checkbox"] + label {
    --is-checked: ; /* OFF */
    color: var(--is-checked, green);
    border: var(--is-checked, none);
    font-style: var(--is-checked, italic);
}

input[type="checkbox"]:checked + label {
    --is-checked: initial; /* ON */
}

A limitation of this approach however is that you can’t define several values to use in case --is-checked is in the OFF state. Say I want the text in the example above to be both red by default and with a border. Setting --is-checked to red will only get me halfway, as that value is only valid for the color property here.

input[type="checkbox"] + label {
    --is-checked: red; /* Default value to use */
    color: var(--is-checked, green); /* ✅ Will be red by default */
    border: var(--is-checked, none); /* ❌ What about a default value for border? */
    font-style: var(--is-checked, italic); /* ❌ What about a default value for font-style? */
}

~

# Update 2020.01.22: The Space Toggle Trick

UPDATE: As James0x57 himself pointed out in the comments below, the “CSS Custom Property Toggle Trick” can be used for this, but it takes some adjustments when compared to the implementation above. Here’s what James0x57 calls the Space Toggle Trick:

  • Consider the value   (space) to be the ON position, and the value of initial to be the OFF position.
  • Assign property values to new custom properties using the syntax --value-to-use-if-custom-toggle-is-on: var(--my-custom-toggle) value;, where you put the value to be used after the CSS Variable.

    --toggler: initial;
    --red-if-toggler: var(--toggler) red;
  • To use the value, use the var() syntax as before (e.g. adding a fallback value):

    background: var(--red-if-toggler, green); /* will be green! */
  • If you have more than one property than can affect a toggle, you can chain them up:

    • AND Logic:

      --red-if-togglersalltrue: var(--tog1) var(--tog2) var(--tog3) red;
    • OR Logic:

      -red-if-anytogglertrue: var(--tog1, var(--tog2, var(--tog3))) red;

Here’s a pen that applies his technique, with some cleaned up property names:

See the Pen
3. Binary Custom Properties + “The CSS Custom Property Toggle Trick” (Renamed)
by Bramus (@bramus)
on CodePen.

Thanks for clarifying James0x57, as I only understood half of your hack before 😅

~

# Future Solution: Higher Level Custom Properties

So the problem is that, as it stands today, we can’t have one single CSS Custom Property affect a varying set of other CSS Properties, or at least not in an easy way. At the CSS WG Telecon from early December 2020 Lea Verou proposed something called “Higher Level Custom Properties”, which would allow exactly that!

🚨 Do note that this proposal is still in it’s very very early stages and part of an ongoing discussion. The CSS WG has merely expressed interest in this proposal, suggesting that it should be explored further. If if tends to be helpful and possible, only then work on a Working Draft will start. Right now it still is a concept.

~

# Definition and Example

“Higher Level Custom Properties” are Custom Properties that control a number of other CSS Properties. As the proposal stands right now you use them in combination with a newly proposed @if at-rule, like so:

.square {
    width: 2vw;
    padding: 0.25vw;
    aspect-ratio: 1/1;

    @if (var(--size) = big) {
        width: 16vw;
        padding: 1vw;
    }
}

Unlike the Custom Properties we know today, a Higher Level Custom Property controls multiple declarations, way beyond simple variable substitution. In the example above we set our HLCP --size to have a value of big. This value isn’t used directly, but affects the other properties width and padding.

Using this HLCP also improves the meaning of our code. Setting width: 16vw; does not clearly express our intent, whereas setting --size: big; does.

💁‍♂️ If you don’t like @if then please don’t discard the whole idea immediately, but focus on the problem it’s trying to fix here. Lea’s proposal is a possible solution, not the solution. Could be that — in the end — we end up with a totally different syntax.

~

# Issues that still need to be tackled

Before you get too excited, there are still some cases that need to be taken care of. In a follow-up comment on the proposal, Lea documented some already identified issues.

🚨 Note that these issues are blocking issues. As long as these aren’t resolved, HLCPs won’t happen.

# Partial Application

A first issue is a problem with the desugaring of @if and partial application. Behind the scenes a @if at-rule desugars to the still discussed if() function call. The example above eventually becomes this:

.square {
    width: if(var(--size) = big, 16vw, 2vw);
    padding: if(var(--size) = big, 1vw, 0.25vw);
    aspect-ratio: 1/1;
}

This leads to no issue here, but it becomes quirky when comparing against percentages for example.

E.g. consider this:

.foo {
	@if (1em > 5%) {
		width: 400px;
		height: 300px;
	}
}

which desugars to:

.foo {
	width: if(1em > 5%, 400px);
	height: if(1em > 5%, 300px);
}

Now consider that an element that matches .foo is inside a 600px by 400px container and has a computed font-size of 25px; This makes 1em > 5% evaluate to false on the width property and true on the height property, which would make the @if partially applied. We most definitely don’t want that.

There are some ideas floating around to fix this — such as forcing percentages/lengths to always be compared against the width — but that’s still a bit vague right now.

# Cascading

Another issue that was pointed out is one on Cascading. I especially like this one, as it gives us a good insight in how CSS behaves and works:

Inline conditionals will have the IACVT (Invalid At Computed Value Time) behavior that we have come to know and love (?) from Custom Properties. Since @if will desugar to inline conditionals, it will also fall back to that, which may sometimes be surprising. This means that these two snippets are not equivalent:

.notice {
	background: palegoldenrod;
}

.notice {
	/* Desugars to background: if(var(--warning) = on, orange, unset); */
	@if (var(--warning) = on) {
		background: orange;
	}
}
.notice {
	/* Desugars to background: if(var(--warning) = on, orange, palegoldenrod); */
	background: palegoldenrod;

	@if (var(--warning) = on) {
		background: orange;
	}
}

You can file IACVT (Invalid At Computed Value Time) in the #TIL section there.

A declaration can be invalid at computed-value […] if it uses a valid custom property, but the property value, after substituting its var() functions, is invalid. When this happens, the computed value of the property is either the property’s inherited value or its initial value […].

This explains why in the example below the background won’t be red but (the default) transparent.

:root { --not-a-color: 20px; }
p { background-color: red; }
p { background-color: var(--not-a-color); }

👉 As 20px is no valid <color> value, the last declaration will become background-color: initial;.

💡 If we would have written background-color: 20px directly (e.g. without the use of Custom Properties), then that declaration would have simply been discarded due to being invalid, and we would have ended up with a red background.

~

# In Closing

The “Higher Level Custom Properties” idea by Lea Verou is one that quite excites me, as it solves an actual issue one can have in their code and would avoid having to use one of the nasty hacks.

There’s still a long way to go before we might actually see this land, yet as the CSS WG has expressed interest I’m hopeful that the already identified issues will be wrinkled out, and that work on an official spec can start.

If you have your own input on this subject, then I suggest to participate in the Higher Level Custom Properties discussion on GitHub.

~

🔥 Like what you see? Want to stay in the loop? Here's how:

Injecting a JavaScript Attack Vector using CSS Custom Properties

Earlier this week I saw this tweet by Sansec float by:

This one’s pretty nice I must say: as the syntax for CSS Custom Properties is overly permissive (see here) you can use Custom Properties to store your JavaScript attack vector in. If you then use window.getComputedStyle to extract the contents of the Custom Property (see here) and combine it with a function constructor and an IIFE, it’s possible to execute it.

Here’s a pen that loads a remote confetti script using the method described:

Let this underline the importance of a Content Security Policy to prevent remote script loading script evaluation.

Update: Blocking this “hack” with a proper CSP

It took me some time to figure out — as I’m no CSP expert — but turns out the unsafe-inline keyword in the CSP’s source list is enough to block the execution of the JS-IN-CSS.

As a reminder, here are the four allowed keywords:

  • 'none', as you might expect, matches nothing.
  • 'self' matches the current origin, but not its subdomains.
  • 'unsafe-inline' allows inline JavaScript and CSS.
  • 'unsafe-eval' allows text-to-JavaScript mechanisms like eval.

I first thought unsafe-inline would be insufficient here as the code does not call eval, but apparently a function constructor is (correctly!) considered equally harmful, and therefore also blocked.

Here’s an updated demo that blocks the script evaluation:

See the Pen
Injecting a JavaScript attack vector using CSS Custom Properties (with CSP)
by Bramus (@bramus)
on CodePen.

The CSP used is this one:

<meta
    http-equiv="Content-Security-Policy"
    content="script-src https://cpwebassets.codepen.io https://cdpn.io https://cdn.jsdelivr.net 'unsafe-inline';"
>

It works as follows:

  • https://cpwebassets.codepen.io and https://cdpn.io are there for the CodePen demo to work
  • https://cdn.jsdelivr.net is there to allow legitimate loading of scripts — such as a jQuery you might need — from that CDN.
  • unsafe-inline is the one that prevents the execution of the JS-IN-CSS defined script by blocking the call to the function constructor

That calls for confetti! 🤪

Did this help you out? Like what you see?
Thank me with a coffee.

I don't do this for profit but a small one-time donation would surely put a smile on my face. Thanks!

☕️ Buy me a Coffee (€3)

To stay in the loop you can follow @bramus or follow @bramusblog on Twitter.

Using the Numpad in vi over SSH

One of the things that kept on bothering me when SSH’ing is that the keys of the numpad would not work in vi: when pressing any key on the numpad, another key would be sent somehow.

Should’ve Google’d this a lot earlier because the fix is very simple: in the preferences of Terminal, go to Profiles → Your Profile → Advanced and uncheck the “Allow VT100 application keypad mode” option.

You can also tweak this setting from the CLI using PlistBuddy:

/usr/libexec/PlistBuddy -c "Delete 'Window Settings':Pro:StrictVTKeypad" ~/Library/Preferences/com.apple.Terminal.plist > /dev/null 2>&1
/usr/libexec/PlistBuddy -c "Set 'Window Settings':Pro:StrictVTKeypad bool true" ~/Library/Preferences/com.apple.Terminal.plist

There, much better 🙂

☝ I’ve also added this tweak to ./freshinstall, a tool which I built to automatically configure my MacBook (Preferences, Dotfiles, Installed Software, etc)

Did this help you out? Like what you see?
Thank me with a coffee.

I don't do this for profit but a small one-time donation would surely put a smile on my face. Thanks!

☕️ Buy me a Coffee (€3)

To stay in the loop you can follow @bramus or follow @bramusblog on Twitter.

css-houdini-circles — A Houdini Paint Worklet that draws Colorful Background Circles

Last night — inspired by the Paint Worklet demos on Houdini.how — I decided to give Houdini a spin myself and created my own Paint Worklet. The result is css-houdini-circles which draws a bunch of random circles on the background.

🎩 Houdini, ain't that a magician?

Houdini is a set of low-level APIs that exposes parts of the CSS engine, giving developers the power to extend CSS by hooking into the styling and layout process of a browser’s rendering engine. Houdini is a group of APIs that give developers direct access to the CSS Object Model (CSSOM), enabling developers to write code the browser can parse as CSS, thereby creating new CSS features without waiting for them to be implemented natively in browsers.

It really is magic, hence it's name Houdini. I'd recommend this slidedeck and this video to get you started

As a user you can configure the number of circles, the size range, the opacity range, and the colors to use. In true Houdini style these are all configured using CSS Custom Properties.

.element {
    --colors: #f94144, #f3722c, #f8961e, #f9844a, #f9c74f, #90be6d, #43aa8b, #4d908e;
    --min-radius: 20;
    --max-radius: 100;
    --num-circles: 30;
    --min-opacity: 10;
    --max-opacity: 50;

    background-image: paint(circles);
}

~

Here’s a CodePen demo for you to play with:

(Hit “Edit on CodePen” and change the CSS Custom Properties to your liking)

~

What first started out as a demo on CodePen, eventually led to me creating and publishing it as a package on NPM.

npm install css-houdini-circles

Furthermore I also created a pull request to add the site to the Houdini.now website. I was quite surprised to learn it already got merged and is already published on the website 🙂

~

Feel free to fork the repo to have a starting point to creating your own Paint Worklet. If you know how to work with HTML canvas, I’m confident you’ll find it very easy to do so.

css-houdini-circles source (GitHub) →
css-houdini-circles on npm →

Send mail with Exim from the CLI

I needed to test whether mail relaying worked with Exim or not. To do so I used this command:

exim -v [email protected]

After this type in your mail contents as shown below and hit CTRL+D afterwards to send it:

From:[email protected]
Subject: Hello From the CLI
This is a test from the CLI

It’s also possible to use a one-liner:

echo -e "From:[email protected]\nSubject: Hello From the CLI\nThis is a test from the CLI" | exim -v [email protected]

You’ll see the raw output appear on screen, and should see a 250 status code near the end if all went well.

Did this help you out? Like what you see?
Thank me with a coffee.

I don't do this for profit but a small one-time donation would surely put a smile on my face. Thanks!

☕️ Buy me a Coffee (€3)

To stay in the loop you can follow @bramus or follow @bramusblog on Twitter.

Customise the caret color with the CSS caret-color property

Thanks to this tweet by Álvaro Trigo I found out that you can change the color of the caret — that little blinking | in text inputs — using the caret-color CSS property:

Example:

input {
  caret-color: red;
}

💁‍♂️ Do note that the caret-color defaults to currentColor, so you don’t need to manually match the color set on an element.

~

Browser support is really great:

Data on support for the css-caret-color feature across the major browsers from caniuse.com

💡 Shown above is a live CanIUse.com table, showing an always up-to-date support table. By the time you are reading this browser support might have become better.

~

Wondering if there were more caret-* properties this turned out to be not the case. A pity though, as I can think of cases where I would like to set the caret-height and caret-thickness, such as emulating the cursor of a terminal:

~

As a fun little experiment Tim Carambat created a Pen that changes the caret color from green to yellow to red as you near the maxlength of the <textarea> you’re typing in.

See the Pen
Color Changing Cursor in TextArea
by Timothy Carambat (@rambat1010)
on CodePen.

Usability-wise this isn’t that great, but it’s fun nonetheless 🙂

Native Aspect Ratio Boxes in CSS thanks to aspect-ratio


Old vs. New. Image by @una.

Back in May 2020 I was very delighted to read that the first Working Draft of the CSS Box Sizing Module Level 4 got published, as it featured an addition to CSS that I’ve been wanting for a long time now: native support for aspect ratio boxes through the new aspect-ratio CSS property.

With Chromium 89 (current Canary) and Firefox 85 (current Nightly) already supporting aspect-ratio unflagged, it’s time to start playing with this new addition and start thinking about dropping all those nasty hacks to mimic aspect ratios in CSS. Let’s take a look …

🤔 Working Draft (WD)?

The Working Draft (WD) phase is the first phase of the W3C Recommendation Track, and is considered the exploring phase of a W3C spec.

From thereon a spec can become a Candidate Recommendation (CR) to finally land on being a Recommendation (REC). In between those three stages there are two transition stages: Last Call Working Draft (LCWD) and Proposed Recommendation (PR)

In visual form, the Recommendation Track looks something like this:

See An Inside View of the CSS Working Group at W3C for more details on all phases.

⚠️ Note that the aspect-ratio has not shipped with all browsers yet. Here’s an overview of browser support that I’ll keep up-to-date:

~

~

# Welcome aspect-ratio

In short, the aspect-ratio property allows you to define a preferred aspect ratio on elements:

.box {
  width: 20vw;
  aspect-ratio: 16 / 9;
}

[CodePen Demo]

In the example above the .box will have a preferred aspect ratio of 16:9. Since its width is set to 20vw, the resulting height will be 20vw / 16 * 9 = 11.25vw. Easy, right?

~

# Allowed values for aspect-ratio

The value as set in the example above for aspect-ratio is of the <ratio> type:

  • It typically consists of two numbers separated by a /. The first parameter targets the width and the second one the height.
  • It’s also allowed to pass in just a single number. In that case the second number will be we considered to be 1. E.g. a <ratio> of 2 will translate to 2 / 1.
  • Passing in a 0 for either of the numbers is not allowed.
  • The spaces around the / are not required, so 2/1 is also a valid <ratio> value.

Another allowed value for the aspect-ratio property — which also is the default — is auto. This indicates that the box has no preferred aspect ratio and should size itself as it normally would.

🙋‍♂️ Hold up! How come images already behave correctly, without needing to define an aspect-ratio?

Images may be commonly used, but they are a quite uncommon type of HTML element:

  1. Images are replaced elements:

    A replaced element is an element whose content is outside the scope of the CSS formatting model, such as an image or embedded document. For example, the content of the HTML <img> element is often replaced by the image that its src attribute designates.

    Just check your DevTools: the browser will make an extra HTTP request for any image and fetch its contents separately. Once loaded, the browser will replace the original img tag with the actual image contents.

  2. Images have an intrinsic aspect ratio:

    The intrinsic dimensions represent a preferred or natural size of the object itself; that is, they are not a function of the context in which the object is used.

    Each photo that you take with your phone results in an image that has a certain width and height, which is referred to as the intrinsic or natural width/height. The intrinsic aspect ratio is the ratio between the intrinsic width and intrinsic height.

    When the browser has fetched the image and needs to draw it on screen it will take its intrinsic aspect ratio into account to know how big it should be drawn.

  3. When you define width and height attributes on an img, the browser will take those into account when drawing the image on screen. Nowadays browsers even internally map those properties to CSS sizing properties.

☝️ Do note that you can still set an aspect-ratio on an element that has an intrinsic aspect ratio. In that case your defined aspect-ratio will override the intrinsic aspect ratio.

~

# The fine print

# Aspect of what?

Depending upon which of width or height you set, the box dimensions will be calculated against that.

.box {
  width: 20vw;
  aspect-ratio: 16 / 9; /* Dimensions will be calculated against the width,
                           yielding a height of 11.25vw (20vw / 16 * 9) */
}
.box {
  height: 20vw;
  aspect-ratio: 16 / 9; /* Dimensions will be calculated against the height,
                           yielding a width of 35.55vw (20vw / 9 * 16) */
}

# aspect-ratio+width+height = 🚫

Setting an aspect-ratio won’t have effect on elements that have both a CSS width and CSS height set to a value other than auto. Only one of width or height can be explicitly set, and the other should remain set to auto.

.box {
  width: 20vw;
  height: 20vw;
  aspect-ratio: 16 / 9; /* won't have any effect! */
}

# aspect-ratio + percentage based width/height

In case one of the width and height should be set to a percentage based value such as 100%, the targeted box will take a look at the direct parent element’s dimensions to define its value upon that.

.parent {
  height: 100px;
}
.parent .box {
  height: 100%;
  aspect-ratio: 1 / 1; /* .box will be 100px by 100px */
}

There’s some more edge cases here too, but let’s not get too deep into the spec 😉

# aspect-ratio sets a preferred aspect ratio

Setting an aspect-ratio will tell the browser that this is a preferred aspect ratio. Should the content of the box be larger, then the box will simply grow.

div {
  aspect-ratio: 1/1;
  /* 'width' and 'height' both default to 'auto' */
}
+----------+  +----------+  +----------+
| ~~~~~~~~ |  | ~~~~~~~~ |  | ~~~~~~~~ |
| ~~~~~~~~ |  | ~~~~~~~~ |  | ~~~~~~~~ |
| ~~~~~~~  |  | ~~~~~~~~ |  | ~~~~~~~~ |
|          |  | ~~~      |  | ~~~~~~~~ |
+----------+  +----------+  | ~~~~~~~~ |
                            | ~~~~~~   |
                            +----------+

To maintain the aspect-ratio, you can set overflow to auto so that a scrollbar will be shown should the contents be larger:

div {
  overflow: auto;
  aspect-ratio: 1/1;
}
+----------+  +----------+  +----------+
| ~~~~~~~~ |  | ~~~~~~~~ |  | ~~~~~~~~^|
| ~~~~~~~~ |  | ~~~~~~~~ |  | ~~~~~~~~ |
| ~~~~~~~  |  | ~~~~~~~~ |  | ~~~~~~~~ |
|          |  | ~~~      |  | ~~~~~~~~v|
+----------+  +----------+  +----------+

What also works, is setting min-height

Overriding the min-height property also maintains the 1:1 aspect ratio, but will result in content overflowing the box if it is not otherwise handled.

div {
  aspect-ratio: 1/1;
  min-height: 0;
}
+----------+  +----------+  +----------+
| ~~~~~~~~ |  | ~~~~~~~~ |  | ~~~~~~~~ |
| ~~~~~~~~ |  | ~~~~~~~~ |  | ~~~~~~~~ |
| ~~~~~~~  |  | ~~~~~~~~ |  | ~~~~~~~~ |
|          |  | ~~~      |  | ~~~~~~~~ |
+----------+  +----------+  +-~~~~~~~~-+
                              ~~~~~~    

~

# Demos

# Using aspect-ratio with a fallback for older browsers

Thanks to the powerful @supports it’s possible to add a fallback for browsers that don’t support aspect-ratio. In the demo below (based upon this demo by Una) a fallback using the padding-top hack is applied:

[CodePen Demo]

# Using aspect-ratio with CSS Custom Properties

By introducing a CSS Custom Property it’s possible to make your code more generic and extract away a .aspect-ratio class.

[CodePen Demo]

To use it, add apply the .aspect-ratio on the element you want, and pass in a --aspect-ratio CSS Custom Property:

<div
  class="aspect-ratio"
  style="--aspect-ratio: 16/9;"
>I am an aspect ratio box</div>

The code is written so that it will use the value for --aspect-ratio in both the fallback and the modern version.

# Automatically setting aspect-ratio on iframes and the like

When you embed an iframe you most likely set its width and height HTML attribute.

<iframe
  src="https://www.youtube.com/embed/e7BkmF8CJpQ"
  width="560"
  height="315"></iframe>

It’s possible to use the values of these attributes to automatically set the aspect-ratio.

iframe[width][height] {
  aspect-ratio: attr(width) / attr(height);
}

Heck, you could even target [width][height] if you’d want!

💁‍♂️ FYI: This is also what browsers nowadays do for images: they map the values from the width and height HTML attributes from images to a aspect-ratio in order to prevent Cumulative Layout Shift.

Firefox’s internal stylesheet for example looks like this:

img, input[type="image"], video, embed, iframe, marquee, object, table {
  aspect-ratio: attr(width) / attr(height);
}

marquee, lol 😆

[CodePen Demo]

🐛 I’ve noticed that reading the width/height attribute values using attr() to pass them into aspect-ratio doesn’t seem to work in current Chromium. To cater for that I’m also passing their values by means of a CSS Custom Property …

<iframe
  src="https://www.youtube.com/embed/e7BkmF8CJpQ"
  width="560"
  height="315"
  style="--aspect-ratio: 560 / 315"
></iframe>
🙋‍♂️ Why doesn’t this iframe demo have a padding-top fallback injected using :after?

Just like images, iframes also are replaced elements. It’s not possible to inject contents using :before/:after on replaced elements.

If you really need to have a fallback, you need to wrap the iframe in a container and apply the aspect-ratio on the container. See Embed Responsively and adjust were needed.

~

# In Closing

After 8 years of wanting this feature to land in CSS (ref) I’m very happy to see this addition make it into the spec. It’s still a Working Draft right now, but that doesn’t stop me from being excited about it already. I hope you are too 🙂

🔥 Like what you see? Want to stay in the loop? Here's how:

Speed up build times with this little Git trick

When building applications on build pipelines like GitHub Actions, Google Cloud Build, CircleCI, etc. every second counts. Here’s this small trick I use to speed up build times: when cloning the repo from its Git source, I instruct Git to do a shallow clone of the single branch it is building.

💡 If you’re running a prebuilt “git clone” step on your Build Pipeline it most likely already uses this trick. Be sure to double check though.

~

Shallow Cloning

When doing a git clone you, by default, get the entire history of the project along with that. Take this clone of Laravel for example:

$ git clone [email protected]:laravel/laravel.git
Cloning into 'laravel'...
remote: Enumerating objects: 19, done.
remote: Counting objects: 100% (19/19), done.
remote: Compressing objects: 100% (15/15), done.
remote: Total 32004 (delta 5), reused 11 (delta 3), pack-reused 31985
Receiving objects: 100% (32004/32004), 9.94 MiB | 6.98 MiB/s, done.
Resolving deltas: 100% (18934/18934), done.

That’s a whopping 32004 objects totalling ±10MiB that have been downloaded, even though the default Laravel branch only counts 66 files spread across 36 directories.

The objects contained in this ±10MiB make up the entire history of every file and folder the project. To build the project we don’t really need all that, as we’re only interested in the latest version of each file and folder. By leveraging the --depth argument of our git clone command, we can enforce just that. This is what we call Shallow Cloning.

$ git clone --depth 1 [email protected]:laravel/laravel.git
Cloning into 'laravel'...
remote: Enumerating objects: 108, done.
remote: Counting objects: 100% (108/108), done.
remote: Compressing objects: 100% (88/88), done.
remote: Total 108 (delta 6), reused 49 (delta 1), pack-reused 0
Receiving objects: 100% (108/108), 41.80 KiB | 535.00 KiB/s, done.
Resolving deltas: 100% (6/6), done.

That’s a much speedier clone with only 108 objects, totalling a mere ±40KiB!

🤔 You could argue that 10MiB worth of objects is still OK to clone, but think of scenarios where you have a big “monorepo” with plenty of branches … then you’ll be talking about hundreds of wasted MiBs, if not GiBs.

~

Single Branch Cloning

When building a project you’re building only one certain branch. Information about the other branches is irrelevant at that time. To directly clone one specific branch we can use the --branch option to target said branch. With that alone we won’t get there though, we we still need to discard information about other branches. This is where the --single-branch option comes into play:

$ git clone --branch 3.0 --single-branch [email protected]:laravel/laravel.git
Cloning into 'laravel'...
remote: Enumerating objects: 20392, done.
remote: Total 20392 (delta 0), reused 0 (delta 0), pack-reused 20392
Receiving objects: 100% (20392/20392), 5.79 MiB | 853.00 KiB/s, done.
Resolving deltas: 100% (12731/12731), done.

Here we’ve cloned only the 3.0 branch of Laravel, resulting in roughly 10000 fewer objects to be downloaded.

By checking the contents of git branch -a we can also verify that other branch info has not been fetched:

$ cd laravel
$ git branch -a
* 3.0
  remotes/origin/3.0

~

Shallow Cloning + Single Branch Cloning

By combining both we can download only the latest files of a specific branch. Since the use of --single-branch is implied when using --depth, we can drop the former and our command will look like this:

$ git clone --depth 1 --branch <branchname> <repo>

Here’s an example downloading the Laravel 3.0 branch:

$ git clone --depth 1 --branch 3.0 [email protected]:laravel/laravel.git
Cloning into 'laravel'...
remote: Enumerating objects: 545, done.
remote: Counting objects: 100% (545/545), done.
remote: Compressing objects: 100% (465/465), done.
remote: Total 545 (delta 78), reused 293 (delta 45), pack-reused 0
Receiving objects: 100% (545/545), 1.34 MiB | 832.00 KiB/s, done.
Resolving deltas: 100% (78/78), done.

~

With this in place you’ll see your build times drop by minutes, especially when working on a monorepo with many branches.

Did this help you out? Like what you see?
Thank me with a coffee.

I don't do this for profit but a small one-time donation would surely put a smile on my face. Thanks!

☕️ Buy me a Coffee (€3)

To stay in the loop you can follow @bramus or follow @bramusblog on Twitter.

🐳 Building Docker Images on Google Cloud Build? Check out this trick to enable caching.