CSS – WebKit https://webkit.org Open Source Web Browser Engine Wed, 04 Mar 2026 16:46:44 +0000 en-US hourly 1 https://wordpress.org/?v=6.9.1 When will CSS Grid Lanes arrive? How long until we can use it? https://webkit.org/blog/17758/when-will-css-grid-lanes-arrive-how-long-until-we-can-use-it/ Thu, 22 Jan 2026 10:00:30 +0000 https://webkit.org/?p=17758 Anytime an exciting new web technology starts to land in browsers, developers want to know “when in the world am I going to be able to use this?”

Currently, the finalized syntax for Grid Lanes is available in Safari Technology Preview. Edge, Chrome and Firefox have all made significant progress on their implementations, so it’s going to arrive sooner than you think.

Plus, you can start using it as soon as you want with progressive enhancement. This article will show you how.

Web page of content items — each item is a group with an image, headline and text inside a card with rounded corners. The page is in a browser window on the left, with a layout created with Grid Lanes. The items have different aspect ratios, and nestle together into columns, where all of the content for each item can be seen. On the right, is another browser window with the same content — this time laid out in columns again, but also rows. In order for the items to fit into the rows, the photos have all been cropped into squares, the headlines are truncated to fit onto one line, and the teaser descriptions are all chopped to only be three paragraphs long. All the rest of the content is simply missing, not shown in order to force each item to be the same shape and size as the others. And fit on the grid.
Deliver the layout on the left using Grid Lanes to browsers with support, while providing a fallback for other browsers.

(If you haven’t heard of Grid Lanes yet, it’s a new tool for layout that makes it easy to create masonry-style layouts in CSS alone. Read Introducing CSS Grid Lanes to learn all about it. And read New Safari developer tools provide insight into CSS Grid Lanes to learn about our new developer tooling that makes using Grid Lanes it even easier.)

Current status of implementations

Where are browsers in the process of getting ready to ship support for Grid Lanes? Let’s look at the progress that’s been made over the last seven years.

Firefox was first

It’s the team that was at Mozilla in 2019-2020 who wrote the original CSS Working Group Editor’s Draft for Grid level 3, proposing concrete ideas for how masonry-style layouts would work in CSS. The feature shipped in Firefox Nightly in very early 2020. Some of the syntax has since changed, but under the hood, the way this new layout feature relies on and expands CSS Grid is basically the same, which means much of the heavy lifting for implementing it in the Gecko layout engine is underway.

Firefox does need to update their implementation (including updating to the new syntax and adding the new flow-tolerance property, among other things) but if you want to try it out in Firefox today, you can enter about:config in the URL bar, search for “masonry” and set the flag to true — or use Firefox Nightly where it’s already on by default. (At the moment, remember to use the original grid-template-*: masonry syntax to trigger this layout, instead of display: grid-lanes.)

Safari picked up the pace

In 2022, Safari’s WebKit team picked up where Mozilla left off in 2020, and started implementing the same original proposal for CSS Grid Layout Level 3. We also restarted the discussion inside the CSS Working Group, hoping to advance the original Editor’s Draft to a point where it was mature enough that browsers could feel confident shipping.

The WebKit implementation was enabled on-by-default in Safari Technology Preview 163 in February 2023. It’s been updated continuously as the CSS specification has changed.

You can use Safari Technology Preview today to try out the official web standard, make demos using display: grid-lanes, and learn how it works. Keep an eye on the Safari Release notes to see when it ships in Safari beta.

Screenshot of webpage for CSS Grid Layout Module level 3 specification.
Grid Lanes is defined in CSS Grid Layout Module Level 3.

Chrome & Edge are on board, too

A variation for how masonry layouts could work in CSS landed in Chrome and Edge 140 behind a flag in July 2025. Rather than implementing the same syntax as Safari and Firefox, Chromium experimented with an alternative proposal. This drove debates in the CSSWG about how exactly this feature should work and what its syntax should be. With key syntax decisions now finalized, Chromium engineers at Edge are updating their implementation. Keep an eye on the Chrome Status issue for the latest news.

Bottom line — all the major browser engines are making progress. Now is a great time to learn how Grid Lanes works. And consider if, when and how you could start using it.

Yes, you can start using it kinda soon-ish

Great developers are always mindful of users whose browsers don’t have support. Not only does it take time for all browsers to ship new features, it takes time for all users to update. But this does not mean you have to wait multiple years before using new technologies. It just means you just have to be savvy.

You can progressively enhance your code to create masonry-style layouts, and support older browsers, both at the same time. How? As always, there are multiple options. Which choice is best for you depends on your use case, your team, and your code base. Let’s go through three different approaches you could use:

Option 1: Polyfill — use a JavaScript library as a backup for Grid Lanes

One common trick for using a new CSS feature when it’s still not available in all browsers is to use a polyfill — i.e.: use JavaScript to fill in the missing functionality.

Lucky for you, there are already tried and true JS libraries out in the world for creating masonry layouts. Masonry.js is a popular one. Perhaps you are using it now. You can keep using it by itself, and ignore Grid Lanes. Or you can switch to using the JS library as a polyfill.

The approach here is to go ahead and use CSS Grid Lanes to handle the layout in CSS alone — in browsers with support for Grid Lanes, even if that’s still only preview browsers. At the same time, architect your code to also work with a JavaScript library. Test in a browser without support for Grid Lanes to make sure the JS layout works.

The key is to structure your code with conditionals, so browsers with Grid Lanes support use CSS, while those without use JS. In your CSS, use Feature Queries to ensure the right CSS is used under the right conditions. In JavaScript, use if statements.

For example, you can structure your CSS like this:

/* Native Grid Lanes for supporting browsers */
@supports (display: grid-lanes) {
  .grid {
    display: grid-lanes;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: 1lh;
  }
}

/* Additional CSS needed only for browsers without Grid Lanes */
@supports not (display: grid-lanes) {
  .grid-item {
    margin: 1lh;
  }
  /* Perhaps also include a fallback layout in case JS doesn't run */
}

Then in JavaScript, you can check to see whether or not Grid Lanes is supported. If not, load the file. And then start using Masonry JS (or another library), according to its documentation.

// Check if CSS Grid Lanes is NOT supported
if (!CSS.supports('display', 'grid-lanes')) {

    // Dynamically load masonry.js
    const script = document.createElement('script');
    script.src = 'https://unpkg.com/masonry-layout@4/dist/masonry.pkgd.min.js';
    script.onload = function() {

    // Use Masonry.js after the script loads
    new Masonry('.grid', {
        itemSelector: '.grid-item',
        columnWidth: 200,
    });
    };
    document.head.appendChild(script);
}

It’s important to conditionally load the JS library only if the browser doesn’t support Grid Lanes. There’s no reason to have all users download and run the JS file when some percent don’t need it. That percentage might be small today (even zero), but over time it will grow to 100%.

Save future you the task of having to change your code later. Structure it today so in a future when all users have Grid Lanes, no one has to do anything. Users get the best experience, even if no one on your team ever cleans out the old code.

With this technique, browsers with Grid Lanes support use pure CSS, while older browsers load and use JavaScript. By switching to using the JavaScript library a polyfill, not as the primary layout mechanism, increasing numbers of users will get the benefit of a faster and more robust layout sooner.

Of course, maybe this won’t work for your project. Maybe it’s too complicated to architect your HTML and surrounding layout to work for both Grid Lanes and a masonry library at the same time. So what are the other options?

Option 2: Don’t use Grid Lanes — use another layout in CSS instead

Of course, you might be screaming “it’s too early to use Grid Lanes!” There is always the option of simply waiting to use a new technology. Perhaps another layout mode in CSS like Grid level 1, Flexbox or Multicolumn are good enough for your needs. And you can hold off using any tool for accomplishing a masonry-style layout until you feel more confident about Grid Lanes.

CSS Multicolumn is an interesting option that you might not be familiar with. It shipped in browsers decades ago (before Can I Use kept track). With origins that date back to the 1990s, Multicolumn suffered from the fate of most early CSS — the specification was not detailed enough, and that resulted in a lot of differences between browser implementations. This frustrated developers, resulting in Multicolumn falling out of favor.

In more recent years, Multicolumn level 1 has gotten a lot of love, and the specification now contains far more detail. This has helped browsers squash interop bugs. There’s even a Multicolumn level 2 specification bringing new features in the future. There’s still more work to do to create true interoperability, but it’s worth reconsidering Multicolumn to see if can solve your use case today.

Multicolumn and Grid Lanes can result in very similar-looking layouts. They are fundamentally different, however, in the way content flows. These differences impact the order of what comes into focus when tabbing through content, readability / scanability, and user experience. So consider carefully.

graphic comparing Grid Lanes layout vs Multicolumn. Two browser windows side by side. The layouts are basically the same visually, but Grid Lanes flows the content across the page, keeping 20 items above the bottom edge of the viewport. Multicolumn pours the items 1 to 5 down the first column, then starts the next column with the 16th item, showing 16-19... etc. Where is item 6-15? Not on screen.

Try out the demos we created to compare how Multicolumn and Grid Lanes work. Select different layouts from the dropdown menus, and turn on item numbers to emphasize the difference.

Option 3: Use Grid Lanes — along with a fallback layout in CSS

While “don’t use Grid Lanes” is always an option, perhaps the best approach is to write your code so that Grid Lanes is used when supported, and another layout mode in CSS is used as the fallback. This avoids using JavaScript for layout, while still delivering the newer layout to the users who do have support.

For example, let’s imagine we want to use Grid Lanes, and leverage CSS Grid (level 1) when Grid Lanes isn’t supported. To make the original Grid layout work, can use CSS to force all the items be the same aspect ratio by cropping images and truncating text.

To do this, we can apply layout to the container using the display property — twice. First we’ll declare display: grid, then we’ll immediately declare display: grid-lanes.

.grid-container {
  display: grid;
  display: grid-lanes; /* will override grid in browsers that support */
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 1lh;
}

In browsers that support Grid Lanes, the second declaration will override the first. The display: grid rule will be ignored. And the layout will use Grid Lanes, resulting in a layout that packs content of different aspect ratios into a set of columns.

In browsers that do not support Grid Lanes, the browser will ignore the second declaration. It sees display: grid-lanes and goes “what? That’s not a thing. You must have misspelled something. Ignore!” This leaves grid as the layout that’s applied. The content will be laid out into clear rows as well as columns.

This is a tried and true technique that’s been used by developers for over two decades — relying on the fact that CSS just ignores anything it doesn’t understand. It does not throw an error message. It does not stop parsing. It just ignores that line of code and moves along.

We can also use a Feature Query to write code that only gets applied in browsers without support for Grid Lanes. Let’s use the aspect-ratio property to force all images into the same aspect ratio. And use object-fit: cover to crop those images to fit in the box, instead of letting them be squished.

/* Additional CSS for browsers without Grid Lanes support */
@supports not (display: grid-lanes) {
  .grid-item {
    img {
      aspect-ratio: 1; /* resize every image into a square */
      object-fit: cover; /* crop, don't squish the image */
    }
    h3 {
      white-space: nowrap; /* don't wrap the headline */
      overflow: hidden; /* crop the extra */
      text-overflow: ellipsis; /* add an ellipsis if it overflows */
    }
    p {
      display: -webkit-box; /* current practice in all browsers */
      -webkit-box-orient: vertical;
      -webkit-line-clamp: 3; /* clamps to this many lines of text */
      overflow: hidden;
    }
  }
}

We can force our headline to not wrap with white-space: nowrap. Once the headline is on one line, we can hide whatever doesn’t fit with overflow: hidden. Then, text-overflow: ellipsis adds “…” to the end of what’s visible.

When we want to truncate multi-line text to a specific number of lines, we can use the -webkit-line-clamp technique. While originally invented by the WebKit team long ago when prefixes were the best practice for browsers rolling out new ideas, today -webkit-box, -webkit-box-orient and -webkit-line-clamp are supported by all browsers. (No browser has shipped a replacement yet because a complete web standard for defining such a tool is still under debate.)

This is the exact same image from the opening of the article. Web page of content items — each item is a group with an image, headline and text inside a card with rounded corners. The page is in a browser window on the left, with a layout created with Grid Lanes. The items have different aspect ratios, and nestle together into columns, where all of the content for each item can be seen. On the right, is another browser window with the same content — this time laid out in columns again, but also rows. In order for the items to fit into the rows, the photos have all been cropped into squares, the headlines are truncated to fit onto one line, and the teaser descriptions are all chopped to only be three paragraphs long. All the rest of the content is simply missing, not shown in order to force each item to be the same shape and size as the others. And fit on the grid.

This approach results in a masonry-style waterfall layout being delivered to the browsers that support Grid Lanes, while a more traditional layout of equal-sized boxes are delivered using CSS Grid level 1 to browsers that don’t yet support Grid Lanes.

It’s up to you

It’s totally up to you how you want to handle the fallback for a lack of support for Grid Lanes, but you definitely have options. This is one of the benefits of writing CSS, and not just using a 3rd-party utility framework that abstracts all the flexibility away. Progressive enhancement techniques bring the future into the present, and let you start using Grid Lanes far sooner!

Learn more about Grid Lanes

This is our third article in a series about Grid Lanes. The first introduces CSS Grid Lanes, explaining what it is and how to use it (and yes, it can be used “in the other direction”). The second article shows off our new developer tools and explains why they are particularly helpful for setting flow tolerance. Also, check out our demos in Safari Technology Preview. And be sure to come back to webkit.org soon for more articles about Grid Lanes.

  1. Introducing CSS Grid Lanes
  2. New Safari developer tools provide insight into CSS Grid Lanes
  3. Demos of Grid Lanes
]]>
New Safari developer tools provide insight into CSS Grid Lanes https://webkit.org/blog/17746/new-safari-developer-tools-provide-insight-into-css-grid-lanes/ Wed, 14 Jan 2026 22:45:00 +0000 https://webkit.org/?p=17746 You might have heard recently that Safari Technology Preview 234 landed the final plan for supporting masonry-style layouts in CSS. It’s called Grid Lanes.

web browser showing a 6-column layout of photos of various aspect ratios, packed vertically
Try out all our demos of CSS Grid Lanes today in Safari Technology Preview.

CSS Grid Lanes adds a whole new capability to CSS Grid. It lets you line up content in either columns or rows — and not both.

This layout pattern allows content of various aspect ratios to pack together. No longer do you need to truncate content artificially to make it fit. Plus, the content that’s earlier in the HTML gets grouped together towards the start of the container. If new items get lazy loaded, they appear at the end without reshuffling what’s already on screen.

It can be tricky to understand the content flow pattern as you are learning Grid Lanes. The content is not flowing down the first column to the very bottom of the container, and then back up to the top of the second column. (If you want that pattern, use CSS Multicolumn or Flexbox.)

With Grid Lanes, the content flows perpendicular to the layout shape you created. When you define columns, the content flows back and forth across those columns, just like to how it would if rows existed. If you define rows, the content will flow up and down through the rows — in the column direction, as if columns were there.

diagram showing how for waterfall layout there are columns, while content flows side to side. And for brick, the content is laid out in rows, while it the order flows up and down.

Having a way to see the order of items can make it easier to understand this content flow. Introducing the CSS Grid Lanes Inspector in Safari. It’s just the regular Grid Inspector, now with more features.

Grid Lanes photo demo in Safari, with Web Inspector open to the Layout panel, and all the tools for the Grid Inspector turned on. Grid lines are marked with dotted lines. Columns are labeled with numbers and sizes. And each photo is marked with a label like Item 1 — which makes it clear the order of content in the layout.

Safari’s Grid Inspector already reveals the grid lines for Grid Lanes, and labels track sizes, line numbers, line names, and area names. Now it has a new feature — “Order Numbers”.

By turning on the order numbers in the example above, we can clearly see how Item 1, 2, 3, and 4 flow across the columns, as if there were a row. Then Item 5 is in the middle right, followed by Item 6 on the far right, and so on.

You might be tempted to believe the content order doesn’t matter. With pages like this photo gallery — most users will have no idea how the photos are ordered in the HTML. But for many users, the content order has a big impact on their experience. You should always consider what it’s like to tab through content — watching one item after another sequentially come into focus. Consider what it’s like to listen to the site through a screenreader while navigating by touch or keyboard. With Grid Lanes, you can adjust flow-tolerance to reduce the jumping around and put items where people expect.

To know which value for flow tolerance to choose, it really helps to quickly see the order of items. That makes it immediately clear how your CSS impacts the result.

Order Numbers in the Grid Inspector is an extension of a feature Safari’s Flexbox Inspector has had since Safari 16.0 — marking the order of Flex items. Seeing content order is also helpful when using the order property in Flexbox.

Web browser showing photo layout — this time a Flexbox layout. The Web Inspector is open to the Layout tab, and the Flexbox Inspector is enabled. The lines of the layout are marked with dotted lines... and each item is labeled with its order.

Order Numbers in Safari’s Grid Inspector works for CSS Grid and Subgrid, as well as Grid Lanes.

Try out Safari’s layout tooling

The Grid and Flexbox layout inspectors might seem similar across browsers, but the team behind Safari’s Web Inspector has taken the time to finely polish the details. In both the Grid and Flexbox Inspectors, you can simultaneously activate as many overlays as you want. No limits. And no janky scrolling due to performance struggles.

Safari’s Flexbox Inspector visually distinguishes between excess free space and Flex gaps, since knowing which is which can solve confusion. It shows the boundaries of items, revealing how they are distributed both on the main axis and the cross axis of Flexbox containers. And it lists all the Flexbox containers, making it easier to understand what’s happening overall.

Our Grid Inspector has a simple and clear interface, making it easy to understand the options. It lists all of the Grid containers — and you can show the overlay for every single one at the same time. The overlays don’t disappear when you scroll. And of course, you can change the default colors of the overlays, to best contrast with your site content.

And Safari’s Grid and Flexbox Inspectors are the only browser devtools that label content order. We hope seeing the order of content in Grid Lanes helps you understand it more thoroughly and enjoy using this powerful new layout mechanism.

Try out Order Numbers

Order Numbers in Safari’s Grid Inspector shipped today in Safari Technology Preview 235. Let us know what you think. There’s still time to polish the details to make the most helpful tool possible. You can ping Jen Simmons on Bluesky or Mastodon with links, comments and ideas.

For more

Note: Learn more about Web Inspector from the Web Inspector Reference documentation.
]]>
Introducing CSS Grid Lanes https://webkit.org/blog/17660/introducing-css-grid-lanes/ Fri, 19 Dec 2025 09:00:13 +0000 https://webkit.org/?p=17660 It’s here, the future of masonry layouts on the web! After the groundwork laid by Mozilla, years of effort by Apple’s WebKit team, and many rounds debate at the CSS Working Group with all the browsers, it’s now clear how it works.

Introducing CSS Grid Lanes.

.container {
  display: grid-lanes;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 16px;
}

Try it today in Safari Technology Preview 234.

How Grid Lanes work

Let’s break down exactly how to create this classic layout.

Classic masonry-style layout of photos of various aspect ratios, all the same width, aligned in six columns
You can try out this demo of photo gallery layouts today in Safari Technology Preview.

First, the HTML.

<main class="container">
  <figure><img src="photo-1.jpg"></figure>
  <figure><img src="photo-2.jpg"></figure>
  <figure><img src="photo-3.jpg"></figure>
  <!-- etc -->
</main>

Let’s start by applying display: grid-lanes to the main element to create a Grid container ready to make this kind of layout. Then we use grid-template-columns to create the “lanes” with the full power of CSS Grid.

In this case, we’ll use repeat(auto-fill, minmax(250px, 1fr)) to create flexible columns at least 250 pixels wide. The browser will decide how many columns to make, filling all available space.

And then, gap: 16px gives us 16 pixel gaps between the lanes, and 16 pixel gaps between items within the lanes.

.container {
  display: grid-lanes;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 16px;
}

That’s it! In three lines of CSS, with zero media queries or container queries, we created a flexible layout that works on all screen sizes.

Think of it like a highway of cars in bumper-to-bumper traffic.

Cartoon drawing of a highway from above. Nine cars fill four lanes of traffic, bumper to bumper. Each car has a number labeling it, showing the order these would be in HTML.

Just like the classic Masonry library, as the browser decides where to put each item, the next one is placed in whichever column gets it closest to the top of the window. Like traffic, each car “changes lanes” to end up in the lane that gets them “the furthest ahead”.

This layout makes it possible for users to tab across the lanes to all currently-visible content, (not down the first column below the fold to the very bottom, and then back to the top of the second column). It also makes it possible for you to build a site that keeps loading more content as the user scrolls, infinitely, without needing JavaScript to handle the layout.

The power of Grid

Varying lane sizes

Because Grid Lanes uses the full power of CSS Grid to define lanes using grid-template-*, it’s easy to create creative design variations.

For example, we can create a flexible layout with alternating narrow and wide columns — where both the first and last columns are always narrow, even as the number of columns changes with the viewport size. This is accomplished with grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr) minmax(16rem, 2fr)) minmax(8rem, 1fr).

Demo layout of photos, where the 1st, 3rd, 5th, and 7th column are narrow, while the 2nd, 4th and 6th columns are twice as wide.
Try out the demo of photo gallery layouts today in Safari Technology Preview.

There’s a whole world of possibilities using grid-template-* syntax.

Spanning items

Since we have the full power of Grid layout, we can also span lanes, of course.

A complex layout of titles with teaser text for over two dozen articles — telling people what they'll experience if they open the article. The first teaser has a very large headline with text, and spans four columns. Five more teasers are medium-sized, bowl and next to the hero. The rest of the space available is filled in with small teasers. None of the teasers have the same amount of content as the rest. The heights of each box are random, and the layout tucks each box up against the one above it.
Try out the demo of newspaper article layout today in Safari Technology Preview.
main {
  display: grid-lanes;
  grid-template-columns: repeat(auto-fill, minmax(20ch, 1fr));
  gap: 2lh;
}
article { 
  grid-column: span 1; 
}
@media (1250px < width) {
  article:nth-child(1) { 
    grid-column: span 4;             
  }
  article:nth-child(2), article:nth-child(3), article:nth-child(4), article:nth-child(5), article:nth-child(6), article:nth-child(7), article:nth-child(8) { 
    grid-column: span 2; 
  }
}

All the article teasers are first set to span 1 column. Then the 1st item is specifically told to span 4 columns, while the 2nd – 8th to span 2 columns. This creates a far more dynamic graphic design than the typical symmetrical, everything the same-width, everything the same-height layout that’s dominated over the last decade.

Placing items

We can also explicitly place items while using Grid Lanes. Here, the header is always placed in the last column, no matter how many columns exist.

A layout of paintings — each has a bit of text below the painting: title, etc. The paintings are laid out in 8 columns. Over on the right, spanning across two columns is the header of the website.
Try out the demo of a museum website layout today in Safari Technology Preview.
main {
  display: grid-lanes;
  grid-template-columns: repeat(auto-fill, minmax(24ch, 1fr));
}
header {
  grid-column: -3 / -1;
}

Changing directions

Yes, lanes can go either direction! All of the examples above happen to create a “waterfall” shape, where the content is laid out in columns. But Grid Lanes can be used to create a layout in the other direction, in a “brick” layout shape.

Contrasting cartoon drawings: on the left, waterfall layout with boxes lined up in columns, falling down the page. And "brick" layout, with boxes flowing left to right, stacked like bricks in rows.

The browser automatically creates a waterfall layout when you define columns with grid-template-columns, like this:

.container {
  display: grid-lanes;
  grid-template-columns: 1fr 1fr 1fr 1fr;
}

If you want a brick layout in the other direction, instead define the rows with grid-template-rows:

.container {
  display: grid-lanes;
  grid-template-rows: 1fr 1fr 1fr;
}

This works automatically thanks to a new default forgrid-auto-flow, the normal value. It figures out whether to create columns or rows based on whether you defined the lanes using grid-template-columns or grid-template-rows.

The CSS Working Group is still discussing which property will explicitly control the flow orientation, and what its syntax will be. The debate is over whether to reuse grid-auto-flow or create new properties like grid-lanes-direction. If you’re interested in reading about the options being considered or chime in with your thoughts, see this discussion.

However, since normal will be the initial value either way, you don’t have to wait for this decision to learn Grid Lanes. When you define only one direction — grid-template-rows or grid-template-columns — it will Just Work™. (If it doesn’t, check if grid-auto-flow is set to a conflicting value. You canunset it if needed.)

Flow Tolerance

“Tolerance” is a new concept created for Grid Lanes. It lets you adjust just how picky the layout algorithm is when deciding where to place items.

Look at the next drawing. Notice that Car 4 is a tiny bit shorter than Car 1. When the “tolerance” is zero, Car 6 ends up in the right-most lane, while Car 7 is on the left. Car 6 ends up behind Car 4 on the right because that gets it a tiny bit closer “down the road” (closer to the top of the Grid container). Car 7 then takes the next-closest-to-the-top slot, and ends up behind Car 1 on the left. The end result? The first horizontal grouping of content is ordered 1, 2, 3, 4, and the next is 7, 5, 6.

Same cartoon drawing of the highway of bumper to bumper traffic from above.

But the difference in length between Car 1 and Car 4 is tiny. Car 6 isn’t meaningfully closer to the top of the page. And having item 6 on the right, with item 7 on the left is likely an unexpected experience — especially for users who are tabbing through content, or when the content order is somehow labeled.

These tiny differences in size don’t matter in any practical sense. Instead, the browser should consider item sizes like Car 1 and Car 4 to be a tie. That’s why the default for flow-tolerance is 1em — which means only differences in content length greater than 1 em will matter when figuring out where the next item goes.

If you’d like the layout of items to shuffle around less, you can set a higher value for flow-tolerance. In the next digram, the tolerance is set to half-a-car, causing the cars to lay out basically from left to right and only moving to another lane to avoid the extra-long limo. Now, the horizontal groupings of content are 1, 2, 3, 4, and 5, 6, 7.

Now the highway has the cars ordered in a fashion that's less chaotic.

Think of tolerance as how chill you want the car drivers to be. Will they change lanes to get just a few inches ahead? Or will they only move if there’s a lot of space in the other lane? The amount of space you want them to care about is the amount you set in flow-tolerance.

Remember that people tabbing through the page will see each item highlighted as it comes into focus, and may be experiencing the page through a screenreader. A tolerance that’s set too high can create an awkward experience jumping up and down the layout. A tolerance that’s too low can result in jumping back and forth across the layout more than necessary. Adjust flow-tolerance to something appropriate for the sizes and size variations of your content.

[Note, when this article was first published, this property was named item-tolerance in the specification and in Safari Technology Preview 234. On January 7, 2026 the CSS Working Group resolved to change the name to flow-tolerance. We updated the name in Safari Technology Preview 236.]

Try it out

Try out Grid Lanes in Safari Technology Preview today! All of the demos at webkit.org/demos/grid3 have been updated with the new syntax, including other use cases for Grid Lanes. It’s not just for images! For example, a mega menu footer full of links suddenly becomes easy to layout.

A layout of 15 groups of links. Each has between two and nine links in the group — so they are all very different heights from each other. The layout has five columns of these groups, where each group just comes right after the group above it. Without any regard for rows.
Try out the mega menu demo today in Safari Technology Preview.
.container {
  display: grid-lanes;
  grid-template-columns: repeat(auto-fill, minmax(max-content, 24ch));
  column-gap: 4lh;
}

What’s next?

There are a few last decisions for the CSS Working Group to make. But overall, the feature as described in this article is ready to go. It’s time to try it out. And it’s finally safe to commit the basic syntax to memory!

We’d love for you to make some demos! Demonstrate what new use cases you can imagine. And let us know about any bugs or possible improvements you discover. Ping Jen Simmons on Bluesky or Mastodon with links, comments and ideas.

Our team has been working on this since mid-2022, implementing in WebKit and writing the web standard. We can’t wait to see what you will do with it.

]]>
Rolling the Dice with CSS random() https://webkit.org/blog/17285/rolling-the-dice-with-css-random/ Thu, 21 Aug 2025 19:02:33 +0000 https://webkit.org/?p=17285 Random functions in programming languages are amazing. You can use them to generate variations, to make things feel spontaneous and fresh. Until now there was no way to create a random number in CSS. Now, the random() function is on its way. You’ll be able to create a random animation delay, layout content at a random place on the screen, create a random color, or anything you want — all without any JavaScript.

The Basics

This new function has three arguments in this pattern: random(min, max, step). You state a minimum and maximum value to define the range from which the random number will be chosen. You can use any type of number (integer, percentage, length, angle, etc.) as long as all three arguments match. The step argument is optional, but is particularly useful for when you want to ensure round numbers.

For example, random(0, 100, 2) will choose an even number between 0 and 100, while random(0turn, 1turn) will be a fraction of a turn — basically, any decimal between 0 and 360 degrees.

Let’s take a look at how to use random() by going through several demos. We’ll start by creating a field of stars out of HTML and CSS.

A star field with CSS and HTML

A pure CSS star field using randomly placed circles, glow effects and four-pointed stars
<html>
<body>
    <div class="star"></div>
    <div class="star"></div><div class="star"></div>
  </body>
</html>

We’ll start by creating an HTML element for each star. Then paint the sky and style the stars with white circles of any size for now to act as stars. Setting the star to be fixed positioning will make it easy to place them randomly.

body {
  background-color: black;
}

.star {
  background-color: white;
  border-radius: 50%;
  aspect-ratio: 1/1;
  width: 3px;
  position: fixed;
}    

Now we’ll distribute the stars by setting the top and left properties to a random value, in this case 0-100% for top (Y-axis) and a separate random value for left (X-axis). By default, each random() function will generate different random values using a different random base value generated from a uniform distribution.

.star {
  background-color: white;
  border-radius: 50%;
  aspect-ratio: 1/1; 
  width: 3px;
  position: fixed;
  top: random(0%, 100%);
  left: random(0%, 100%);
}
Randomly placed dots

For a more dynamic look, make the size of the “stars” random as well.

.star {
   background-color: white;
   border-radius: 50%;
   aspect-ratio: 1/1; 
   width: random(2px, 10px, 1px);
   position: fixed;
   top: random(0%, 100%);
   left: random(0%, 100%);
 }

Notice that the top and left random values used percentages. But for width you can select a random size based on pixels instead. Just remember that whatever unit you use needs to be the same for each parameter. The third argument used in the example above is used to ensure star sizes are in 1px increments for a well-rounded number to give a better spread of sizes for more variety.

Randomly sized circles for stars

The idea can be taken further by adding some special effects, like adding a subtle glow to the stars with layered drop shadows and blending effects.

.star {
    --star-size: random(--random-star-size, 1px, 7px, 1px);
    background-color: white;
    border-radius: 50%;
    aspect-ratio: 1/1;
    width: var(--star-size);
    position: fixed;
    top: random(0%, 100%);
    left: random(0%, 100%);
    filter: drop-shadow(0px 0px calc(var(--star-size) * 0.5) oklch(0.7 0.2 random(0, 100))) 
            drop-shadow(0px 0px calc(var(--star-size) * 2) white);
    mix-blend-mode: hard-light;
}

Shared randomness and custom properties

The --star-size custom property allows re-using the randomly generated pixel size, but there are some important details to using custom properties with random(). First, you’ll see what looks like another custom property as the first parameter of the random() function. The --random-star-size name used here is known as an ident. It’s used to ensure the random value generated is used in the other random() function calls where the same ident is provided.

You might be asking, why is that necessary here? It’s important to realize that setting a custom property to a CSS function isn’t like other programming languages where a variable stores a result. Custom properties are more like a simple text replacement mechanism. That means wherever you are calling that custom property with var() what’s actually happening is something more like text substitution, replacing the var() with a duplicate of the property you declared. In the case of this example, that means another random() call, not the resulting value of the function.

Back to the example, an ideal blur effect for the glow will need to be based on whatever the random size of the star ends up being. That’s doable using a calc() to use a factor of the star size for the blur size of the layered drop shadows. Doing that means depends on getting the same value of randomness so using the named ident is the right approach.

The first drop shadow also uses random() to select a hue with oklch() to add vibrant colors that are composited over a soft white glow with a hard-light blend mode. The combination adds a subtle, but dynamic colorization to the stars.

Random glowing circles for stars

Using a named ident shares the randomness across the properties of a single element. It’s one of the many ways to share randomness. The random() function has variety of approaches depending on your needs. You can also use the element-shared value to share a random value for a given property across all matched elements, or mix the two to share the value everywhere. Unlike a named ident, which will share the value whenever the ident is used, element-shared will share the random value for a given property across all the elements it applies to.

One way to see this work in the star field example is to add some four-pointed stars with a simple glyph.

.star.fourpointed {
    clip-path: shape(from 50% 0%,line to 50.27% 3.25%, …);
    --star-size: random(--random-four-point-size, 20px, 60px, 1px);
    rotate: random(element-shared, -45deg, 45deg);
}
Randomly placed and rotated four-pointed stars

A CSS shape() is used to define the four-pointed glyph that will clip the circular white background. The star size can still be random but will need boosted to an order of magnitude larger to feel right. A more realistic styling for these stars comes from finding a way to mimic the diffraction rays that are seen in images of stars. They all angle the same way for may important physics reasons. So while a static degree of rotation could be assigned, it’s a tad more fun and dynamic to assign one randomly. But doing that effectively means finding a way to use the same random angle for all of the four-pointed stars.

That’s where element-shared comes into play. The element-shared value generates a random value shared by all elements for that property. That makes all of the four-pointed stars rotate with the same random angle.

Here’s the complete version of the star field example that you can experiment with: https://codepen.io/jdatapple/pen/YPyELeV

Randomly placed rectangles

There are lots of other ways to make use of random(). Building on some of the concepts of the star field example, you can explore using random() with layout tools like grid.

Randomly placed, randomly colored rectangles

In this variant example, the webpage area is equally divided into 100 rows and 100 columns. Then randomly colored rectangles are positioned at random in the grid:

.grid {
  display: grid;
  --rows: 100;
  --columns: 100;
  grid-template-rows: repeat(var(--rows), 1fr);
  grid-template-columns: repeat(var(--columns), 1fr);
  width: 100vw;
  height: 100vh;
}

.rectangle {
  background-color: lch(100% 90% random(0deg, 360deg));
  grid-area: random(1, var(--rows), 1) / random(1, var(--columns), 1);
}

You can view the final result on any browser that supports random(): https://codepen.io/ntim/pen/dPYGJxj

Stacks of Photos

Another example of making use of random() is creating stacks of photos that are randomly placed and oriented to look like they’ve been tossed on top of each other. You can spend a lot of time setting all of the photos just so, or let the computer procedurally generate it each time the page is loaded.

Stacks of desert landscape photos with images randomly rotated with randomly offset placement
.stack img {
    width: 100%;
    grid-column: 1;
    grid-row: 1;
    border: 10px solid hsl(0, 100%, 100%);
    box-shadow: 10px 10px 40px hsl(0, 0%, 0%, 20%);

    --random-rotate: rotate(random(-1 * var(--rotate-offset), var(--rotate-offset)));

    transition: .3s ease-out;
    transform: var(--random-rotate);
    transform-origin: random(0%, 100%) random(0%, 100%);
}

More than that, it’s also straightforward to include randomness in interactivity. Adding random translation to the hover state of the images adds to the whimsy:

.stack:hover img {
    transform: var(--random-rotate) translateX(random(-1 * var(--translate-offset), var(--translate-offset))) translateY(random(-1 * var(--translate-offset), var(--translate-offset)));
}

Wheel of Fortune

Random rotation example of a wheel sliced into 20 pieces with emoji placed in every other slice and a green spin button

The random() function can even be used to create interactive elements that need unpredictable outcomes. The wheel of fortune demo showcases this perfectly.

@keyframes spin {
    from {
        rotate: 0deg;
    }
    to {
        rotate: 10turn; /* Fallback for browsers that don't support `random()` */
        rotate: random(2turn, 10turn, 20deg);
    }
}

When the “SPIN” button is clicked, the @keyframe animation uses random() to generate a rotation value that sets when and where the wheel lands. This example highlights the growing capabilities available in modern CSS defining all of the interactivity, randomness, and animation in the stylesheet.

You can see the demo in action here: https://codepen.io/ntim/pen/WbQrMow

A handy randomness reference

There are lots of different ways to use random() depending on what you need and different ways to share the randomness between elements.

Maximum randomness

Both properties get a different value, and it’s different per element too, so you get lots of random rectangles.

.random-rect {
  width: random(100px, 200px);
  height: random(100px, 200px);
}

Shared by name within an element

Using an ident both properties get the same value, but it’s still different per element, so you get lots of random squares.

.random-square {
    width: random(--foo, 100px, 200px);
    height: random(--foo, 100px, 200px);
}

Shared between elements within a property

With element-shared both properties get different values, but they’re shared by every element, so you get lots of identical rectangles of a single random size.

.shared-random-rect {
    width: random(element-shared, 100px, 200px);
    height: random(element-shared, 100px, 200px);
}

Shared by name globally

Using both a named ident and element-shared means both properties get the same value, and every element shares the random value, so you get lots of identical squares of a single random size.

.shared-random-squares {
    width: random(--foo element-shared, 100px, 200px);
    height: random(--foo element-shared, 100px, 200px);
}

Try it out and tell us how it goes

You can try out the random() function today in Safari Technology Preview! However, it’s important to note that there are ongoing discussions in the CSS Working Group about the specification, and several open issues remain about whether this approach best serves developers’ needs.

While the examples above demonstrate the exciting possibilities, we’re actively asking for feedback from the web development community to help shape the final direction, and you can help. If you experiment with random() in Safari Technology Preview, we’d love to hear about your experience. What works well? What feels awkward? Does the way shared values are expressed make sense to you? Do you have use cases that are missing? Or better suggestions for the name of element-shared? Your feedback will directly influence how this feature moves forward from its current form. This is a chance for you to help shape CSS—try it out and let us know what you think.

You can share your feedback with me, Jon Davis, on Bluesky / Mastodon, or our other evangelists — Jen Simmons, on Bluesky / Mastodon, or Saron Yitbarek, on BlueSky. You can also follow WebKit on LinkedIn. If you find a bug or problem, please file a WebKit bug report.

]]>
A gentle introduction to anchor positioning https://webkit.org/blog/17240/a-gentle-introduction-to-anchor-positioning/ Tue, 12 Aug 2025 22:09:02 +0000 https://webkit.org/?p=17240 Anchor positioning allows you to place an element on the page based on where another element is. It makes it easier to create responsive menus and tooltips with less code using only CSS. Here’s how it works.

Let’s say you have an avatar in your nav, like this:

Nav bar with two text options and one avatar

When you click the avatar, you want a menu to appear right below it. The clicking interaction can be handled with just CSS using the Popover API. But once you click, where does your menu show up?

Figuring this out typically requires some JavaScript. But now, with anchor positioning, you can accomplish this with just a few lines of CSS. Anchor positioning will use where the avatar is to determine where the menu will go.

For example, you might want to place it just below the avatar, nice and left-aligned, like this:

Nav bar with avatar's menu expanded and left aligned.

Or you can have it hang out on the side of the avatar, having a party off to the right, like this:

Nav bar with avatar's menu expanded to the right of the photo.

You can position it in a number of places, but I think that first example looks good. It’s something you’d frequently see on the web on sites and web apps. Let’s walk through the code of how to make it happen.

The first step in placing your menu is letting it know about your avatar.

A great way to think about the relationship between your avatar and your menu is to think of your menu as if it’s anchored to your avatar. With that in mind, we’ll refer to your avatar as your anchor and your menu as your target.

You’ll name your anchor by declaring an anchor-name on the avatar element. Since your avatar represents your profile and is behaving like button, let’s give it a class of profile-button.

Here’s what it looks like:

.profile-button {
  anchor-name: --profile-button;
}

Then you’ll go to your menu (your target) and declare a position-anchor where the value is the anchor-name of the anchor that you previously declared. This is what establishes the connection and tells the target about the anchor.

Let’s go to your target and declare it there:

.profile-menu {
  position-anchor: --profile-button;
}

And the final step is giving your menu an absolute or fixed position, like this:

.profile-menu {
  position-anchor: --profile-button;
  position: absolute;
}

Great, you’ve established your connection! Now you need to decide where to put your menu.

Position-area

One way to do this is to use the position-area property.

The position-area allows you to place your element on a nine-square grid where the anchor takes the center spot. The space that contains this grid is the containing block of the anchor.

Black grid with nine squares and avatar in the center labeled left, center, right, top, bottom.

You can use this grid to determine where you want to position your menu. Do you want the menu to show up on the top right? Great! You can write top right.

.profile-menu {
  position-anchor: --profile-button;
  position: absolute;
  position-area: top right;
}
Black grid with blue box on the top right and avatar in the center.

But here’s the thing about top right — it might feel like an intuitive way to describe where you want your target to be placed, but it’s actually not the best way to go about it.

Whenever possible, you want to use logical properties, not physical ones, in your CSS. That’s a more inclusive way to describe what you’re trying to do that doesn’t make assumptions about writing mode or language.

Here’s the same nine-square grid using logical properties:

Black grid with avatar in the center with labels start/block-start, center, end/block-end, end/inline-end, start/inline-start

If you rewrite your above physical property as a logical one, you’d go from top right to block-start inline-end, like this:

.profile-menu {
  position-anchor: --profile-button;
  position: absolute;
  position-area: block-start inline-end;
}
Black grid with avatar in the center and the top right block in blue with block-start inline-end in it

Do you want it to be on the bottom center of your grid? No problem, we’d write that logically as block-end center.

.profile-menu {
  position-anchor: --profile-button;
  position: absolute;
  position-area: block-end center;
}
Black grid with avatar in the center and the bottom center block being blue with block-end center written on it

In your example, you want to put the menu under the avatar, left-aligned, to create that layout you’re going for. But, in this case, there’s a problem: the menu is wider than the avatar. So when you use block-end center as your position-area value, it crosses the grid lines and pokes out like this:

Table showing values, flexbox and grid columns for Item Flow properties.

Let’s take the grid lines away and see what that would look like.

Nav bar with two text items and one avatar with expanded menu left aligned under avatar.

That’s not the look you’re going for. How do you get that clean, left-alignment instead?

You can set position-area to block-end span-inline-end. Instead of centering the element, that’ll start the position directly below the avatar and let it spill over to end of the inline direction, like this:

Table showing values, flexbox and grid columns for Item Flow properties.

And here’s the code:

.profile-menu {
  position-anchor: --profile-button;
  position: absolute;
  position-area: block-end span-inline-end;
}

Great, exactly what you want!

The main logical values you can use are start/block-start, end/block-end, start/inline-start and end/inline-end. And, if you must, you can use the physical values: left, center, right, top, bottom.

For a full list of values you can use, check out MDN’s resource here. Let’s get back to the positioning.

The positioning you set for your menu works on desktop, where you’re got plenty of space on the right for the element to spill over, but what about on mobile where your viewport is more narrow?

In mobile view, it’s probably better to do a right-align and allow the menu to spill over to the left instead. To do that, what we want is a way to tell the menu to switch to a different position when it’s run out of room.

And anchor positioning can do that! The benefit of anchor positioning is you get this responsive flexibility. It knows when there’s no room for your defined position and it’ll happily try something else. So let’s give it a different position to try when it’s out of space. To do that, you’ll use position-try.

Here’s what that looks like:

.profile-menu {
  position-anchor: --profile-button;
  position: absolute;
  position-area: block-end span-inline-end;
  position-try: block-end span-inline-start;
}

position-try allows you to give your element a new position to try, hence the name. Here’s what the results look like in action.

Anchor()

The anchor() function uses a different framework than position-area for placing your target. Whereas position-area uses the concept of a grid to place the target, anchor() is all about placing your target based on the edges of your anchor.

It can only be used within the inset properties. Inset properties allow you to control where the element is located by declaring the offsets from its default positions. They can be the physical insets, which are top , right, bottom, and left, the logical inset properties, which are inset-block-start , inset-block-end, inset-inline-start and inset-inline-end, and the short-hand inset properties, inset-block and inset-inline .

So to recreate the above effect where the left side of the avatar is aligned to the left side of the menu, you would use the left inset property and assign it to anchor(left). Now the two sides are lined up to each other. And then to line up the top of the menu with the bottom of the avatar, you would set the top inset property of the menu to anchor(bottom).

Here’s the code put together:

.profile-menu {
  position-anchor: --profile-button;
  position: absolute;
  left: anchor(left);
  top: anchor(bottom);
}

This uses physical properties, but once again, we really should be using logical properties, so let’s rewrite this.

.profile-menu {
  position-anchor: --profile-button;
  position: absolute;
  inset-inline-start: anchor(start);
  inset-block-start: anchor(end);
}

Here, left and right values are replaced with inset-inline-start and inset-block-start. For the anchor() function, there are a few logical values to pick from. start and end allow you to set the start and end of the anchor’s containing block based on whatever axis the inset property you’re using is on. You also have self-start and self-end which are based on the anchor element’s content instead of its containing block.

anchor() also allows you to be specific about what anchor you’re working with. You can pass in an optional anchor-name and be explicit about your anchor. If you did that here, here’s what it would look like:

.profile-menu {
  position-anchor: --profile-button;
  position: absolute;
  left: anchor(--profile-button left);
  top: anchor(--profile-button bottom);
}

But in this situation, you only have the one anchor, so if you don’t explicitly state it, it’ll use whatever you set as your position-anchor.

The anchor() function can also be used in the calc() function. So far, you’ve been aligning the menu with the avatar’s container which includes some padding. Let’s say you wanted to align it with just the photo minus the padding, what would you do?

One way is to use the calc() function and add the padding (which is 1.25em) to the anchor(start) value, like this:

.profile-menu {
  position-anchor: --profile-button;
  position: absolute;
  inset-inline-start: calc(anchor(start) + 1.25em);
  inset-block-start: anchor(bottom);
}

That’ll get you this result (the pink line is to illustrate alignment):

Table showing values, flexbox and grid columns for Item Flow properties.

position-area and anchor() both help you position your targets based on your anchor. Which one you use depends on your preferred mental model. Thinking of anchor positioning as a grid that you place things on can be a helpful and intuitive way of writing your code. But if you prefer to think of positioning as assigning values to the edges of your anchor, that’s fine too. They can both help you accomplish your goals.

To play around with the different ways to position your target, check out this CodePen I set up for you to experiment. Replace the position-area with your own values and see how the target element moves around. Try to put it in different places and give anchor() a spin too.

There are more things you can do with anchor positioning but the information in this blog post should get you pretty far. For more details on additional properties, checkout MDN’s resource here. You can also check out this fun game, Anchoreum, to teach you anchor positioning.

And let me know what you think of this post. Send me, Saron Yitbarek, a message on BlueSky, or reach out to our other evangelists — Jen Simmons, on Bluesky / Mastodon, and Jon Davis, on Bluesky / Mastodon. You can also follow WebKit on LinkedIn. If you find a bug or problem, please file a WebKit bug report.

]]>
Item Flow – Part 2: next steps for Masonry https://webkit.org/blog/17219/item-flow-part-2-next-steps-for-masonry/ Fri, 08 Aug 2025 01:57:46 +0000 https://webkit.org/?p=17219 UPDATE December 2025: When this article was published, the CSS Working Group was considering creating a new mechanism called Item Flow, described at length below. Since then, the CSSWG has gone in another direction, choosing instead to keep things simple. Grid Lanes has emerged as the winner of the path to take for creating masonry-style layouts in CSS. You can read all about it in Introducing CSS Grid Lanes. Perhaps Item Flow might be revived someday, but for now, it’s official dead.

Back in March, we published Item Flow, Part 1: a new unified concept for layout, an article about a new idea for unifying flex-flow and grid-auto-flow into a single set of properties under a new item-flow shorthand. We shared some new ideas being developed in the CSS Working Group, discussed how they might fit into Flexbox and Grid, and hinted at the implications for future layouts.

Here’s a refresher of what Item Flow will entail:

Table showing values, flexbox and grid columns for Item Flow properties.

The Item Flow proposal is still being refined and has a few outstanding issues. Once adopted, it will let you replace some of the Flexbox- and Grid-specific properties with universal Item Flow properties in the future. These properties offer a new, unified syntax for existing features (though the existing syntax will work for a long time) as well as new capabilities in both Flexbox and Grid. For more info, check out Part 1 of this blog post.

In this post, we’ll unpack the big question — what does this mean for Masonry?

You might remember some of the open debate questions we blogged about earlier. Most have been resolved. At its January F2F, the CSS Working Group adopted the idea of re-using grid templating and placement properties (grid-*) for masonry-style layouts, and it adopted (in principle) the idea of using the new Item Flow properties to control how items are placed. You can see this in the current official Working Draft of the masonry layout spec.

But there are still two key open questions for Masonry and how it should integrate with Item Flow: how do you switch into a masonry-style layout, and what do row and column mean?

Let’s dig into these open questions, the debates and proposals, and your role in influencing the future of this much anticipated layout.

Integrating item-flow and masonry-style layout

Introducing Item Flow to the world of masonry-style layouts has opened up a number of questions on how it will work. In particular, there are two issues currently being debated. Let’s walk through both.

Issue #1: How do we switch into a masonry-style layout?

The first question is, how do we trigger a masonry-style layout? On the surface, this question is about syntax, and indeed there are two to pick from. But if we take a step back, it’s much more than that.

Determining how we trigger masonry-style layouts requires us to first consider a bigger question about where masonry-style layouts fit into CSS displays. Do you view this layout as its own, unique display, separate and distinct from Grid and Flexbox? Or is it similar enough to Grid to be considered a member of that ecosystem? These two mental models, two very different opinions on what a masonry-style layout even is, are reflected in the two ways to trigger this layout.

If your mental model of the masonry-style layout puts it in a world all on its own, then using a new, unique display value makes more sense. If we went with that, the code would look like this:

.container {
  display: masonry;
  grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
  gap: 1rem;
}

But if you see masonry-style layouts as simply a variation of Grid, then using grid as the display value and the new item-flow keyword to collapse the rows would make more sense. That would look like this:

.container {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
  item-flow: collapse;
  gap: 1rem;
}

We’ll discuss the pros and cons of display: masonry vs display: grid option later in this article. There’s currently an open issue about which we should use.

Issue #2: What’s a row, anyway?

When it comes to describing a layout, there are two approaches. We can think of it in terms of “flow-of-items” or by the “shape-of-layout.” While these aren’t technical terms and aren’t officially part of the CSS syntax, they’re helpful terms to use for this conversation. Both “flow-of-items” and “shape-of-layout” are valid in Flexbox and Grid, but in a masonry-style layout these two interpretations diverge.

Flexbox

layout of rounded rectangles in rows

Here, we’re using Flexbox to lay out nine items across several rows and columns. But when we describe this layout, do we describe it as items organized by rows or columns?

Describing it by “flow-of-items” would have us look at how items are placed, or how they flow across the page. In this example, items are placed left to right, filling up the first row then moving down to the next one.

same layout of rounded rectangles in rows but grayed out with purple arrows going across it

So we would describe the “flow-of-items” here in terms of rows.

But there’s a second way to describe it, called “shape-of-layout,” which focuses on the visual shape that this layout makes. The shape that stands out here is rows.

same layout of rounded rectangles but rows are outlined in purple borders

The columns are basically nonexistent, but we can see that our items are organized into three distinct tracks, so our “shape-of-layout” value would also be rows.

You’ll notice in our Flexbox example that the “shape-of-layout” and the “flow-of-items” are the same. Whichever way you think about it, row works to describe it. While technically, the widths of the elements could also be the same and result in columns, that’s not the typical use case. In most Flexbox examples, row would be true for both “shape-of-layout” and “flow-of-items.”

Now, what about Grid?

Grid

If we look at a simple Grid example and try to determine its “flow-of-items”, you’ll see something similar.

grid of green tiles numbered one through nine

The items flow from left to right, filing up the first row then moving downward. Just like Flexbox, we can describe this layout’s “flow-of-items” in terms of rows.

grid of green rectangles with purple arrow going through them

But when we consider our “shape-of-layout” perspective, we have a problem — there isn’t a clear shape that stands out. We have three distinct rows and three distinct columns.

grid of green rectangles with rows outlined in blue and rows outlined in pink

So for Grid, we can easily pick row for our “flow-of-items” value, but the resulting “shape-of-layout” depends on the width and spanning of the elements. Sometimes there’s a clear shape. Other times, there isn’t.

What about for a masonry-style layout?

Masonry-style layout

In a masonry-style layout, these two concepts diverge. To demonstrate, let’s start by looking at a masonry-style layout example in terms of “shape-of-layout.”

blue rounded rectangles numbered one through eight and laid out in a masonry style.

Masonry-style layouts have a very particular, recognizable shape made of strict columns and messy rows. In this example, we see how, while the rows are inconsistent and broken up, we have four distinct columns.

Blue rounded rectangles numbered one through eight and laid out in masonry style with columns outlined in orange.

So for “shape-of-layout,” we would describe this example in terms of columns.

For “flow-of-items,” we see that we start placing our items from left to right, and only when our first row fills up do we go down to the next, top, most available row.

Blue rounded rectangles numbered one through eight and laid out in masonry style with purple arrow going across.

In contrast to our first Flexbox example, the values for “shape-of-layout” and “flow-of-items” are not the same. When it comes to the masonry-style layout, there is a clear distinction between the two.

In fact, the relationship between “shape-of-layout” and “flow-of-items” is actually core to how masonry-style layouts work and what makes them so special. This layout collapses its rows in the direction that’s perpendicular to the flow of content — that’s what makes it so unique. While it’s the “shape of layout” that catches the eye, it’s actually the perpendicular relationship between layout and content flow that creates the “waterfall effect” that defines it. The elements flow in one direction and the shape they make as a result is in the other direction. If, instead, you want this layout shape to be in the same direction as flow, you’d use Multicolumn instead.

So which should we use? Which is the better description to tell the browser what to do? And what do we name the item-flow sub-property that accepts this keyword?

In Flexbox, we use flex-direction to choose between row and column layouts—so would we use item-direction here? The name item-direction sounds like it’s describing the “flow-of-items” since it’s the direction of item placement. In this case, item-direction: row would indicate a waterfall-style masonry layout, like the one above. If we want a “shape-of-layout” interpretation, then maybe a different name like item-track: column would be better, since it indicates the orientation of the tracks.

But what are your thoughts?

Putting it all together: Masonry-style layouts with Item Flow

We at WebKit see masonry-style layout as a type of Grid layout, so we favor re-using display: grid and using item-direction: row | column with the “flow-of-items” interpretation. To give you a better idea of how it would work, let’s walk through an example using Item Flow to implement the masonry-style layout.

Let’s say you want to create this classic masonry / waterfall layout:

Selection of photos in a browser laid out in masonry style.

Using Item Flow, here’s the code you would write to implement it:

.container {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
  item-flow: row collapse;  
  gap: 1rem;
}

With this proposal, you use display: grid, and you define the size and number of columns using any of the many techniques for track definition allowed by CSS Grid. The only difference is the addition of item-flow to collapse the row axis.

By default in CSS Grid, content flows across the page in the inline direction, in this case, rows — the same direction as grid-auto-flow: row. That’s the whole point of using the masonry-style layout instead of Multicolumn, to get content to flow across the page, instead of down. Let’s number the items to make the masonry-layout pattern more obvious:

Selection of numbered photos in a browser laid out in masonry style.

Most of the code is familiar if you know CSS Grid, and that’s intentional. We want you to have to learn as little as possible to implement this layout. All it takes to trigger a masonry-style layout is item-pack: collapse. And the direction in which the content flows is set with item-direction: row or column , just like in Grid. You could write the syntax using longhands:

item-direction: row;  /* default, so unneccesary to state */
item-pack: collapse;  

Or you can combine the two together using the shorthand to say:

item-flow: row collapse;  

Or even:

item-flow: collapse;  

You don’t need the word “row”, but it can help to make it obvious what’s happening. You are collapsing the rows, as you pack the items into the masonry-style layout. And if you wanted to create a masonry-style layout going the other direction, you can collapse the columns instead, like this:

.container {
  display: grid;
  grid-template-rows: repeat(auto-fill, minmax(8lh, 1fr));
  item-flow: column collapse;  
  gap: 1rem;
}

This solution fits what we believe to be the correct mental model. The masonry-style layout isn’t a distinct, unique layout that necessitates its own display value. It’s a variant of Grid and they have at lot in common. It makes sense to us to treat it as such. It also better fits with our existing vocabulary, like flex-direction, creating a more unified system that works consistently for all three display types, which would also make it easier to learn.

We believe this solution is not only simpler for developers to implement, but is also the most intuitive. If we look at the end user’s point of view, describing the masonry-style layout in terms of columns might feel the most obvious. Visually, it’s the columns that stand out. But they only stand out after we’ve finished laying out all the items. When the developer is creating the layout and turning a list of items into the masonry-style shape, they have to decide where to put each element and how the elements flow. “Where should each element go?” is a question all developers have to answer, and we believe that answering that question with a matching property will feel more intuitive from the developer’s point of view.

That’s our take and we believe it would be the simplest to use, teach, and understand. But it’s all up for debate, which makes it all the more important to hear from web developers and gather more input. So if you have an opinion on how this should work, particularly on the different options for the masonry-style layout switch and what you prefer in terms of row/column control syntax, we’d love to hear from you. Find Jen Simmons on Bluesky or Mastodon or Saron Yitbarek on Bluesky to share your feedback.

]]>
So many ranges, so little time: A cheatsheet of animation-ranges for your next scroll-driven animation https://webkit.org/blog/17184/so-many-ranges-so-little-time-a-cheatsheet-of-animation-ranges-for-your-next-scroll-driven-animation/ Wed, 09 Jul 2025 23:25:38 +0000 https://webkit.org/?p=17184 Background

If you’re new to scroll-driven animations, welcome! To start, it might be a good idea to read this beginner-friendly introduction before diving into this guide. I touch on animation-ranges briefly in that article and here I’ll go deeper, covering the different values and what they mean. Let’s begin!

First, let’s do a quick recap of what animation-range is.

animation-range is used along with the view() timeline. That’s the timeline you would select if you wanted the animation to happen when the user is scrolling and your element is visible in the viewport.

But saying that my element is “visible in the viewport” can mean a lot of things. Here are the different ways in can be interpreted.

Let’s say that the element you want to animate is a photo with a height of 300px.

Does “visible in the viewport” mean you want to start the animation as soon as the first pixel comes into view? Or do you want to wait until the whole photo is visible? What if you want to start the animation when the photo is halfway through the page? Or if you want to stop the animation when it’s almost off the page, but not quite? So many options!

animation-range gives you the ability to be that specific, allowing you to be very particular in when exactly you start and stop your animation.

Definitions

animation-range is actually a shorthand for two properties: animation-range-start and animation-range-end.

animation-range-start allows us to specify when the animation will start and animation-range-end declares when the animation will end. For this article, we’re going to focus on the shorthand.

The shorthand can accept two kinds of values. The first is a timeline-range-name and the second is the length-percentage. Let’s dig into the timeline-range-name first.

The timeline-range-name defines the start and stop of the animation based on where the element is in relation to the viewport.

Let’s look at an example to illustrate.

Say you have an image that’s 300x200px in a container that’s 1000px wide. You want to start that image all the way to the right and have it slide over to the left of the container as you scroll.

Photo of a bird sitting on a branch on the right side of a 1000px rectangle with an arrow pointing towards the left

Here’s the CSS for that animation:

img {
  animation: slideIn;
  animation-timeline: view();
}

@keyframes slideIn {
  0% {
    transform: translateX(700px);
  }
  100% {
    transform; translateX(0px);
  }
}

Now you have to decide — at what exact point do you want to start your sliding animation?

Cover

If you want to start your animation as the first pixel of your image enters your viewport, then you want to use the range cover. If used alone, it will set the start of the timeline (the 0%) right as the image peeps its head into view.

img {
  animation: slideIn;
  animation-timeline: view();
  animation-range: cover;
}

And it will slide your photo over to the left, all the way until the end of the timeline (the 100%) which is defined by the moment when the very last pixel disappears from view.

This means that even when just a sliver of the photo is in view, it will be animated. And when half of the photo disappears, it will still be animated, all the way until the image is completely gone.

Diagram showing an image offscreen at 0% and becoming animated until it exits the viewport.

Contain

If you want the animation to begin once the image is in full view and end right before it starts to exit, you’ll need a new range: contain.

img {
  animation: slideIn;
  animation-timeline: view();
  animation-range: contain;
}

Let’s see what that looks like.

Diagram showing an image in the bottom right corner of the viewport being animated until it reaches the top left corner of the viewport.

Here, the animation doesn’t begin on the first pixel — it waits until it’s fully visible. And when your image has reached the top and the first pixel disappears, the animation stops.

Entry

What if you want the entire animation, the start of it and the end of it, to all happen only as your photo enters the viewport? That means that when the first pixel appears on the screen, the animation begins. And when the the last pixel finishes entering the viewport, that animation stops. That’s what entry is for.

img {
  animation: slideIn;
  animation-timeline: view();
  animation-range: entry;
}

Here’s what that looks like.

Diagram of image in the bottom right of the viewport right below the viewport being animated until it reaches the right bottom having fully entered the viewport.

Entry-crossing (vs. Entry)

There’s another entry-related range called entry-crossing. It’s similar to how entry works with a key difference. entry starts the animation when the first pixel of the image enters the viewport, which marks the 0% point in your timeline. It ends the animation, marking the 100% point in your timeline, when the last pixel has fully entered the viewport.

But what happens when the height of the image is bigger than the height of the viewport? Is the end of our timeline, the 100% point, set as the moment the image first takes up the available viewport even if part of the image is still hidden and hasn’t yet entered the viewport? Or do you wait until the last pixel has crossed the entry and all of the image has passed through the viewport?

You can specify both scenarios with entry and entry-crossing.

If you pick entry and your image height is taller than your viewport, you reach the end of your timeline, the 100%, and end the animation as soon as your image fills the viewport.

Here’s what that looks like:

Diagram of entry value showing a tall image positioned at the bottom right below the viewport animating until the top of the image reaches the top of the left side of the viewport.

But if you pick entry-crossing , the 100% is set to when the last pixel in the image has crossed the entry and entered the view port, like this:

img {
  animation: slideIn;
  animation-timeline: view();
  animation-range: entry-crossing;
}
Diagram of entry-crossing value showing a tall image positioned at the bottom right below the viewport animating until the bottom of the image reaches the bottom of the left side of the viewport.

Exit

exit follows the same idea as entry, but instead of setting the 0% and 100% when the image enters the viewport, it sets it when the image exits the viewport.

It looks like this:

Diagram visualizing exit where the image starts at the top right of the viewport full visible and gets animated as it moves to the left and exits the viewport fully.

Exit-crossing (vs. exit)

exit-crossing has the same idea as entry-crossing . The difference between exit-crossing and exit is easiest to appreciate when the height of your image is taller than the viewport, so let’s look at an example with a tall image to illustrate.

When you use exit for an image that’s taller than its viewport, the 0% is set when the last pixel has entered the viewport and the 100% is set when the last pixel has disappeared, leaving the viewport.

img {
  animation: slideIn;
  animation-timeline: view();
  animation-range: exit;
}

Like this:

Diagram visualizing exit where the image, which is taller than the viewport, starts at the bottom right of the viewport fully visible and gets animated as it moves to the left and exits the viewport fully.

But for exit-crossing , the 0% is set at the point where the first pixel begins to exit the viewport, crossing the viewport’s edge, and the 100% is set when the final pixel disappears, like this:

img {
  animation: slideIn;
  animation-timeline: view();
  animation-range: exit-crossing;
}
Diagram visualizing exit-crossing where the image, which is taller than the viewport, starts with the top lining up with the top right of the viewport and gets animated as it moves up and to the left out of the viewport.

That covers the different timeline-range-name s. They give you really great control of exactly when you want your animation to start and stop.

And for even more options, you can mix and match them. If you want to start your animation when the image first comes into full view but you want the animation to continue until the last pixel leaves, you can do this:

img {
  animation: slideIn;
  animation-timeline: view();
  animation-range: contain exit;
}

You might recall from earlier in this post that animation-range is a shorthand, so here I’ve provided the first value which is for my animation-range-start and the second is for my animation-range-end . And that’ll get me what I’m looking for.

Length-percentage

But let’s say you want to switch things up a bit. You don’t want to start your timeline until the image is fully visible, so you’re going to keep your first value as contain , but you want the animation to start halfway through your timeline, at 50%.

That means you need to explicitly set your <length-precentage> value in your animation-range , like this:

img {
  animation: slideIn;
  animation-timeline: view();
  animation-range: contain 50% exit;
}

And here’s what that might look like:

Diagram visualizing an image starting on the right halfway down the viewport and moving to the left until it is fully out of the viewport

The <length-precentage> value type that can take a percentage or a length of any unit, giving you even more options and flexibility.

The options

So, if we wanted to customize every aspect of our animation-range value, we could define a animation-range-start and animation-range-end, declaring a timeline-range-name and length-percentage value for each.

And as a recap, the values for timeline-range-name are:

  • cover
  • contain
  • entry
  • entry-crossing
  • exit
  • exit-crossing

If we don’t declare any <length-percentage> values, they default to a start of 0% and an end of 100%. And if we don’t declare a timeline-range-name, it’ll default to a start of entry and an end of exit.

What will you animate with scroll-driven animations?

Let us know. Send me, Saron Yitbarek, a message on BlueSky, or reach out to our other evangelists — Jen Simmons, on Bluesky / Mastodon, and Jon Davis, on Bluesky / Mastodon. You can also follow WebKit on LinkedIn. If you find a bug or problem, please file a WebKit bug report.

]]>
Two lines of Cross-Document View Transitions code you can use on every website today https://webkit.org/blog/16967/two-lines-of-cross-document-view-transitions-code-you-can-use-on-every-website-today/ Wed, 21 May 2025 14:00:54 +0000 https://webkit.org/?p=16967 Arguably, the most profound thing about the web is the ability to link one page to another. Click or tap a link, and another webpage opens in the browser window. Click again, and there’s another web page. In 2025, this is so completely obvious, it feels like nothing. But back in the 1980s when the web was conceived, the idea of navigating from one page to another through a link was revolutionary.

If you surfed the early web, you probably remember how very long it would take for a page to load. You were on a webpage, then you’d click a navigation link, and wait… wait… wait… The first page disappeared completely, leaving you starting at a blank white browser window (originally gray). Then slowly the next page started to load, from top to bottom. Bit by bit.

Over the years, internet connections sped up, and page loading got faster and faster. But for a long time you could still see a flash of white between pages. First page. Blank white flash. Next page. Even if a large portion of the second page was the same as the first — the same header, the same sidebar, same layout — it was clear that everything was being dumped and repainted.

Now days, the flash of white (or off-black in dark mode) is rarely seen. Sometimes headers and sidebars will repaint, but often, they do persist across the transition. Yet, earlier times still shape the way many people experience the web.

But now, you can add two lines of code to your website and fundamentally change the behavior of what happens when people navigate from one page to another on your site.

@view-transition {
  navigation: auto;
}

This activates cross-document View Transitions. It ensures any element that’s the same from one page to the next stays exactly in place. It keeps the background color of the site constant. And then anything that’s different — new elements appearing, old elements disappearing, page background changing to a different color — by default, all of these changes happen in a crossfade.

Yes, a crossfade, like a dissolve. The website fades the changes in content between the old page and the new page. Here’s what it looks like.

Notice how the header is present all of the time. It never blinks or moves. And notice how the image and other main content fades in and fades out as we switch from one web page to another. In some ways, it doesn’t feel like we’ve navigated to another web page at all.

Some people think of this as a way to make their website feel “more app like”. Others will simply marvel about how different the web in 2025 is from the web in 1995. And how just two lines of CSS fundamentally changes the behavior of what happens when people navigate from one page to another on your site. The crossfade replaces the cut to blankness and cut to new content that happens without it.

Anytime you are introducing animation to your web page, you should consider whether the animation could be problematic for users with motion sensitivity. Use a prefers-reduced-motion media query to determine whether to modify or stop motion triggers like parallax effects, dimensionality, or depth simulations like zooming/scaling.

But in this case, the dissolve animation doesn’t introduce motion. Simple crossfades are not known to cause adverse effects in those with motion sensitivity. Of course, you should make your own assessment about which motion-driven animations should be reduced — but simply removing 100% of animation on the web in the name of accessibility does not yield truly helpful results. Think through the type of motion you are introducing, and what the overall experience is like using our devices. It’s certain kinds of motion, or degrees of motion that start to cause problems for some users, not simply the presence of any animation. Learn much more about when to use prefers-reduce-motion by reading our article, Responsive Design for Motion.

What about browser support? Cross-document View Transitions are supported in Safari 18.2, Chrome 126, and Edge 126. Which means about 85% of users globally are using a browser with support. You can use View Transitions for this purpose with absolute confidence today, since the fallback behavior is to simply do nothing. Browsers without support act exactly the same as if you did not use this code. So why not use it? It will do something for the majority of your users, while the rest see no change at all.

You can do much more with View Transitions. It’s a very powerful and complex API. But doing more will take more than two lines of code. And more than 800 words to explain. So, for this coffee-break/snack-sized article, we’ll stop here.

Oh, do you want to know more? Well… you can switch from a simple dissolve to something else, like a slide animation. Or you can use same-document View Transitions to easily animate something from one place on the page to another. Dive into the power of View Transitions reading MDN Web Docs.

]]>
How to have the browser pick a contrasting color in CSS https://webkit.org/blog/16929/contrast-color/ Tue, 13 May 2025 17:00:45 +0000 https://webkit.org/?p=16929 Have you ever wished you could write simple CSS to declare a color, and then have the browser figure out whether black or white should be paired with that color? Well, now you can, with contrast-color(). Here’s how it works.

Imagine we’re building a website or a web app, and the design calls for a bunch of buttons with different background colors. We can create a variable named --button-color to handle the background color. And then assign that variable different values from our design system in different situations.

Sometimes the button background will be a dark color, and the button text should be white to provide contrast. Other times, the background will be a lighter color, and the text should be black. Like this:

Two buttons side by side. White text on dark purple for the first, black text on pink background for the second.

Now, of course, we could use a second variable for the text color and carefully define the values for --button-color and --button-text-color at the same time, in pairs, to ensure the choice for the text color is the right one. But, on a large project, with a large team, carefully managing such details can become a really hard task to get right. Suddenly a dark button has unreadable black text, and users can’t figure out what to do.

It’d be easier if we could just tell our CSS to make the text black/white, and have the browser pick which to use — whichever one provides more contrast with a specific color. Then we could just manage our many background colors, and not worry about the text color.

That’s exactly what the contrast-color() function will let us do.

contrast-color()

We can write this in our CSS:

color: contrast-color(purple);

And the browser will set color to either black or white, whichever choice provides better contrast with purple.

Let’s style our button. We’ll set the button background color to our variable. And we’ll define the text color to be the contrasting black/white choice that pairs with that variable.

button {
  background-color: var(--button-color);
  color: contrast-color(var(--button-color));
}

Now we only need to define one color, and the other follows! When we change the button color, the browser will reconsider whether the text should be black or white, and choose fresh the option with more contrast.

For fun, let’s also define a hover color using Relative Color Syntax, and now one variable determines four colors — the default button color & the text to go with it, plus the hover color & the text to go with that.

:root {
  --button-color: purple;
  --hover-color: oklch(from var(--button-color) calc(l + .2) c h);
}
button {
  background-color: var(--button-color);
  color: contrast-color(var(--button-color));
  text-box: cap alphabetic; /* vertically centers the text */
}
button:hover {
  background-color: var(--hover-color);
  color: contrast-color(var(--hover-color));
}

Here’s a demo of the result. Try it in Safari 26.0 or later, or another browser with support, where you can change the button color dynamically.

Accessibility considerations and contrast algorithms

Now, it might be tempting to believe that contrast-color() will magically solve all contrast accessibility concerns all by itself, and your team will never have to think about color contrast again. Nope, that’s not the case. At all.

Using the contrast-color() function does not guarantee that the resulting pair of colors will be accessible. It’s quite possible to pick a color (in this case a background color) that will not have enough contrast with either black or white. It’s still up to the humans involved — designers, developers, testers, and more — to ensure there’s enough contrast.

In fact, if you try out our demo in Safari Technology Preview now (as this article is published in May 2025), you’ll find many of the pairings with mid-tone background colors don’t result in enough contrast. It often seems like the wrong choice is being made. For example, this #317CFF blue returns a contrast-color of black.

Medium dark blue button with black text. The text is hard to see.

When white is clearly the better choice for perceptual contrast.

Same dark medium blue button, now with white text. Much easier to see what it says.

What is happening here? Why is the less-contrasting choice being made?

Well, the current implementation in Safari Technology Preview is using the contrast algorithm officially defined in WCAG 2 (Web Content Accessibility Guidelines version 2). If we put this color blue through a well-respected color contrast checker at WebAIM, it does clearly recommend using black for the text color, not white. WCAG 2 is the current authoritative standard for accessibility on the web, required by law in many places.

The WCAG 2 algorithm calculates black-on-#317CFF as having a contrast ratio of 5.45:1, while white-on-#317CFF has 3.84:1. The contrast-color() function is simply choosing the option with the bigger number — and 5.45 is bigger than 3.84.

Screenshots of the WCAG 2 color contrast checker, showing results of white on blue and black on blue. Black passes. White fails. But black is hard to read while white is easy to read.
Testing black versus white on a medium-dark blue in the WCAG 2 color contrast checker at Web AIM.

When machines run the WCAG 2 algorithm, the black text has higher contrast mathematically. But when humans look at these combinations, the black text has lower contrast perceptually. If you find this odd, well, you aren’t the only one. The WCAG 2 color contrast algorithm has long been a subject of criticism. In fact, one of the major driving forces for updating WCAG to level 3 is a desire to improve the contrast algorithm.

The Accessible Perceptual Contrast Algorithm (APCA) is one possible candidate for inclusion in WCAG 3. You can try out this algorithm today by using the APCA Contrast Calculator at apcacontrast.com. Let’s look at what it thinks about black vs white text on this particular shade of blue background.

Screenshot of APCA Contrast Calculator, showing the same tests of black on blue vs white on blue. White clearly wins.
Testing the same black versus white on a medium-dark blue in the APCA Contrast Calculator.

This contrast algorithm evaluates black-on-blue as having a score of Lc 38.7, while white-on-blue scores Lc -70.9. To know which has more contrast, ignore the negative sign for a moment, and compare 38.7 to 70.9. The bigger the number, the more contrast. The APCA test results say that white text is clearly better than black. Which feels exactly right.

(In the APCA scoring system, the negative number simply signifies that the text is lighter than the background. Think light mode = positive numbers, dark mode = negative numbers.)

Why is APCA giving such better results than WCAG 2? Because its algorithm calculates contrast perceptually instead of with simple mathematics. This takes into consideration the fact humans do not perceive contrast linearly across hue and lightness. If you’ve learned about LCH vs HSL color models, you’ve probably heard about how newer approaches to color mathematics do a better job of understanding our perception of lightness, and knowing which colors seem to be the same luminance or tone. The “Lc” marking the APCA score stands for “Lightness contrast”, as in “Lc 75”.

Luckily, the algorithm behind the contrast-color function can be swapped out. Support for this feature first shipped in March 2021, in Safari Technology Preview 122. (Also, at that time it was named color-contrast.) Back then, it was too early to choose a better algorithm.

The CSS standard still calls for browsers to use the older algorithm, but contains a note about the future: “Currently only WCAG 2.1 is supported, however this algorithm is known to have problems, particularly on dark backgrounds. Future revisions of this module will likely introduce additional contrast algorithms.” Debates over which algorithm is best for WCAG 3 are still ongoing, including discussion of licensing of the algorithms under consideration.

Meanwhile, your team should still take great care in choosing color palettes, keeping accessibility in mind. If you are choosing clearly-light or clearly-dark colors for the contrasting color, contrast-color() will work great even when backed by the WCAG 2 algorithm. It’s in evaluating contrast with mid-tones where the algorithms start to differ in their results.

Plus, the contrast-color() function alone will never guarantee accessibility, even when updated with a better algorithm. “This one has more contrast” is not the same thing as “this one has enough contrast”. There are plenty of colors that never have enough contrast with either black or white, especially at smaller text sizes or thinner font weights.

Providing enough contrast in the real world

While thinking about color contrast, we should remember another tool in our arsenal to ensure we provide good contrast for everyone — theprefers-contrast media query. It lets us offer alternative styling to those who want more contrast.

@media (prefers-contrast: more) {
  /* styling with more contrast */
}

Let’s think through how to use these tools in a real world situation. Imagine we are creating a website for a tree nursery whose main brand color is a particular shade of bright medium green. Our design team really wants to use #2DAD4E as the main button background.

To keep things simple, let’s also pretend we live in a future when a better contrast algorithm has replaced the WCAG 2 algorithm in CSS. (In this article, we’ll use the APCA algorithm.) This change will mean contrast-color() will return white for our text color against this medium green, not black.

But looking up this color combination, we see there might not be enough contrast for some users, especially if the text is small. This is where good design is important.

Testing white on medium green in the APCA contrast calculator. The interface has lots of options for adjusting the colors. And it's got a panel across the bottom with six sections of examples of white text on this color green, in various sizes and weights of fonts.

When using this shade of green as the background for white text, the APCA score is Lc -60.4.

You might remember that WCAG 2 evaluates contrast with a ratio (like “2.9:1”). However, APCA scores are a single number, ranging from Lc -108 to 106. Whether or not Lc -60.4 has enough contrast depends on how big the text is — and, new in APCA, how thick the font weight is.

There’s information about what’s considered a good target for Bronze, Silver, and Gold level conformance in the APCA Readability Criterion. These recommendations can really help guide designers to select the size and weight of text to ensure enough contrast, while allowing a range of beautiful color combinations. In fact, the WCAG 3 itself is being designed to provide flexible guidance to help you understand how to support all users, rather than binary judgments the way WCAG 2 does. Good accessibility isn’t about simply meeting a magical metric to check off a box on a list. It’s about understanding what works for real people, and designing for them. And what people need is complex, not binary.

You’ll notice that this particular APCA Contrast Calculator not only provides a score, but also evaluates the success of dynamic examples showing combinations of font size and font weight. In our case, for “Usage” it says “fluent text okay”. (For the black on blue example above, it instead says “Usage: spot & non text only”.) The Calculator is showing that white text on #2DAD4E works at 24px text if the font weight is 400 or bolder. If we want to use a font-weight of 300, then the text should be at least 41px. Of course, this will depend on which font-face we use, and we aren’t using the same font as that Contrast Calculator does, but there’s far more nuance in this guidance than tools for the WCAG 2 algorithm. And it helps our team come up with a plan for a beautiful design.

Our tree nursery website supports both light and dark mode, and our designers determined that #2DAD4E works as a button color for both light and dark mode for many users, as long as they carefully designed our buttons considering how font size and weight impacts contrast. But even with those considerations, Lc -60.4 is not quite enough contrast for all users, so for anyone who has set their accessibility preferences to ask for more contrast, we’ll replace the button background color with two options — a darker #3B873E green for light mode (with white text, scoring Lc -76.1), and a lighter #77e077 green for dark mode (with black text, scoring Lc 75.2).

Here’s the color palette our fictional design team wants us to accomplish in CSS:

A diagram of our color palette, explaining when to use which color combination. (All information is also articulated in the text of this article.)

When we define colors in variables, it’s incredibly easy to swap out color values for these various conditions. And by using contrast-color(), we only need to worry about the background colors, not the text color pairings. We’ll make the browser do the work, and get the paired colors for free.

To accomplish all of these things at once, we can just write this code (because, remember, we are pretending to live in a future when a better algorithm has replaced the WCAG 2 algorithm in CSS):

--button-color: #2DAD4E;  /* brand green background */ 

@media (prefers-contrast: more) {
  @media (prefers-color-scheme: light) {
    --button-color: #419543;  /* darker green background */
  }
  @media (prefers-color-scheme: dark) {
    --button-color: #77CA8B;  /* lighter green background */
  }
}

button {
  background-color: var(--button-color);
  color: contrast-color(var(--button-color));
  font-size: 1.5rem;  /* 1.5 * 16 = 24px at normal zoom */
  font-weight: 500;
}

In reality, since the WCAG 2 algorithm is the one driving contrast-color(), we probably couldn’t use it on this website. But if we had another project where the brand color was a darker green, and the choice between white/black was the correct one, it could be quite helpful today.

Using contrast-color() is especially helpful when defining colors for multiple states or options like enabled/disabled, light/dark mode, prefers-contrast, and more.

Beyond black & white

You might be wondering, “but what if I want the browser to choose a color beyond just black/white?” If you read about or tried out our original implementation in Safari Technology Preview 122 four years ago, you might remember that the original feature did much more. The newer contrast-color() function is greatly simplified from the original color-contrast().

Because a decision on which color-contrast algorithm to use for WCAG 3 is still being debated, the CSS Working Group decided to move forward with a tool that simply chooses black or white to contrast with the first color. Keeping it simple makes it possible to swap out the algorithm later. By hardcoding the list of options to be black/white, websites are far less likely to break when the WCAG 2 algorithm is replaced, giving the CSSWG the flexibility it needs to keep making needed changes, even as contrast-color ships into the hands of users.

In the future, more complex tools will come along to support more powerful options. Perhaps you’ll be able to list a set of custom color options and have the browser pick from those, instead of picking from black/white. Perhaps you’ll list a set of options, plus specify a contrast level that you want the browser to aim for, instead of having it picking the choice that yields maximum contrast.

In the meantime, often a simple choice between black and white is all you need. We wanted to get the simple version into your hands sooner, rather than waiting for a process that will take years.

And while all of the examples above show black/white text on a color background, contrast-color can be used for much more. You can use a custom color for your text, and make the background be black/white. Or not involve text at all, and define colors for borders, background — anything. There’s a lot you can do.

Continue the conversation

You can learn more about the APCA (Accessible Perceptual Contrast Algorithm) by reading documentation from the folks creating it. Including:

We’d love to hear your thoughts about contrast-color(). Your feedback on this tool can help shape its future. You can find me, Jen Simmons, on Bluesky / Mastodon. Or follow our other web evangelists — Saron Yitbarek on BlueSky, and Jon Davis on Bluesky / Mastodon. You can also follow WebKit on LinkedIn.

]]>
Easier layout with margin-trim https://webkit.org/blog/16854/margin-trim/ Thu, 01 May 2025 14:00:20 +0000 https://webkit.org/?p=16854 If you write a lot of CSS, you are familiar with those moments when you aren’t quite sure how to accomplish what you want to accomplish. Usually, you’ll turn to tutorials or documentation, and learn more about CSS to get your work done. But every once in a while, you realize there is no “proper” way to do what you want to do. So you come up with (or borrow) a solution that feels hacky. Maybe it requires a lot of complex selectors. Or maybe it works for the content you have at the moment, but you worry that someday, someone might throw different HTML at the site, and the solution you wrote will break.

CSS has matured a lot over the last decade. Many robust solutions filled in gaps that previously required fragile hacks. And now, there’s one more — margin-trim.

Margin trim

The margin-trim property lets you tell a container to trim the margins off its children — any margins that push up against the container. In one fell swoop, all of the margin space between the children and the container is eliminated.

diagrams of how margin-trim affects layout — before and after.

This also works when the margins are on the grandchildren or great grand-children, or great great great great grand-children. If there is space created with margins on any of the content inside the container, and that space buts up against the container, it’s trimmed away when margin-trim is applied to the container.

Another diagram of how margin trim affects layout before & after — this time  with grandchildren that have margins

Let’s imagine a practical example. Let’s say we have multiple paragraphs inside an article element, and those paragraphs have margins. Also at the same time, the container has padding on it.

article {
  padding: 2lh;
  background: white;
  p {
    margin-block: 1lh;
  }
}

This is very typical code. The padding on the container is supposed to create an even amount of space all the way around the box, but instead there’s extra white space above and below the content. Like this:

Four paragraphs of text in a white box on a tan background. The white box has a lot more space above and below the text than it does on the sides of the text.

By using 1lh for the margins between the paragraphs, and 2lh for the padding on the article box, we’re attempting to create a beautiful typographic layout. Let’s turn on some guides to better see where the extra space is coming from. The padding on the article box and the margins on the paragraphs are each marked in separate colors.

The same example of text in a box with margins, now with one color marking the padding, and another color marking the margins.

The margins on the first and last paragraphs (1lh) are being added to the padding (2lh) to create a space in the block direction that measures 3lh.

It will be better for the design if we get rid of the margin above the first paragraph and the margin below the last paragraph. Before we had margin-trim, we would attempt to remove the margins from the first and last paragraphs, or lessen the padding in the block direction… but any approach we take will be dependent on the content inside. Perhaps another instance of this article will start with a headline that has a different amount for a top margin. Or start with an image that has no margin.

Without being 100% sure of what kind of content will be in the box, it’s hard to guarantee the spacing will come out as desired. Until now.

The new margin-trim property gives us an easy way to ask directly for what we want. We can tell the box to eliminate any margins that are butting up against that box.

For example:

article {
  margin-trim: block;
  padding: 2lh;
  background: white;
  p {
    margin-block: 1lh;
  }
}

Now the browser automatically chops off any margins that touch the edge of the article box in the block direction — in this case the top and bottom of the box.

The same example again, now with the margins above and below the text chopped off. The colored stripes marking margins no longer exist above and below the content.

Note that while the margins are defined on the <p> element, you declare margin-trim on the <article> element. You always apply margin-trim to the container, not the element that has the margin in the first place.

Here’s the end result.

The same demo, without any guides, now seeing the clean text, and seeing that the space above & below the text, and the space on the sides is the same amount.

Try it yourself

You can try out margin-trim in this live demo, in Safari 16.4 or greater.

Screenshot of the demo on the web where people can try it out for themselves.

Browser Support

Support formargin-trim shipped in Safari over two years ago. But so far, Safari is the only browser with support. So what should you do for browsers without support? For our demo, you could write fallback code inside of feature queries, like this:

article { 
  margin-trim: block;
  font-size: 1.2rem;
  line-height: 1.3;
  padding: 2lh;
  p {
    margin-block: 1lh;
  }
}
@support not (margin-trim: block) {
  article { 
    :first-child {
      margin-block-start: 0;
    }
    :last-child {
      margin-block-end: 0;
    }
  }
}

This helps to clarify the difference between margin-trim and the older techniques we’ve been using.

When using :first-child and :last-child any element that’s the first or last direct child of the container will have its margins trimmed. But any content that either isn’t wrapped in an element, or that is nested further down in the DOM structure will not.

Another diagram showing how the interaction of margins and margin trim works — this time with three drawings, to show margins on children and grandchildren with no margin trim, and older technique for solving this, and using margin trim.

For example, if the first element is a figure with a top margin, and the figure contains an image that also has a top margin, both of those margins will be trimmed by margin-trim, while only the figure margin will be trimmed by :first-child.

<article>
  <figure style="margin-top: 1em">
    <img  style="margin-top: 1em" src="photo.jxl" alt="[alt]">
    <figcaption>[caption]</figcaption>
  </figure>
</article>  

The margin-trim property makes trimming such margins easier and more robust than older techniques.

Even though Safari is the only browser with support at the moment, it makes sense to use it today. Put the hackier layout code in a feature query for the browsers without support ( like@support not (margin-trim: block) { }), while using margin-trim for the browsers that do have it. Hopefully the less robust code will work. It’s the code you are going to have to write anyway. But meanwhile, browsers with support get a more robust solution. And as more and more browsers add support, more and more users will be guaranteed to have a layout that never breaks, no matter what’s thrown at it.

Options for Margin Trim

The values for margin-trim are all logical values, referring to the block and inline directions.

  • margin-trim: none
  • margin-trim: block
  • margin-trim: inline
  • margin-trim: block-start
  • margin-trim: block-end
  • margin-trim: inline-start
  • margin-trim: inline-end

If you want to trim in both directions at the same time, you can do so by combining long-hand values. For example:

margin-trim: block-start block-end inline-start inline-end;

In December 2024, the CSSWG resolved to also allow the shorter block and inline keywords in combination, allowing for syntax like this:

margin-trim: block inline;

The work has been done in WebKit to support this last option. Look for it in Safari Technology Preview soon. Follow this issue for more updates.

Let us know

CSS has never been better. It’s my hope you learn about small improvements like this one, and use it to write more robust code. Let me know what you think on Bluesky or Mastodon. I’d love to hear your stories, feature requests, and questions.

]]>