CSS – WebKit https://webkit.org Open Source Web Browser Engine Sun, 21 Apr 2024 02:12:22 +0000 en-US hourly 1 https://wordpress.org/?v=6.5.2 Help us invent CSS Grid Level 3, aka “Masonry” layout https://webkit.org/blog/15269/help-us-invent-masonry-layouts-for-css-grid-level-3/ Fri, 19 Apr 2024 22:00:55 +0000 https://webkit.org/?p=15269 If you’ve been making websites for years, you know how frustrating it was to lay out a web page with CSS floats. Managing sizes and placement was tedious and time consuming. Being creative was often impossible. CSS Grid greatly eased that pain with Grid Level 1 in 2017, and now with Grid Level 2, aka Subgrid. But even with the powerful CSS of today, not every layout imaged by designers is possible. In fact, when CSS Grid shipped, one of the most commonly asked questions was: “how do I write CSS to accomplish a masonry layout?” Sadly, for the last seven years the answer has been — you can’t.

What is masonry layout?

What do we mean by the term “masonry layout”? Basically it’s the pattern seen in the following image — where content packs together like a brick or stone wall. That’s where it gets the name “masonry”. It’s also frequently called “waterfall layout”, as a metaphor for how content flows down the page like a waterfall.

Two dozen photos of different aspect ratios laid out using a "masonry" pattern

This layout is popular because it solves a few problems that other layouts do not.

  1. It allows for content of different aspect ratios, and avoids the need to crop or truncate content in order to turn everything into uniform rectangles.
  2. It distributes content across the page (instead of flowing down each column, one by one). This follows the natural reading order as you scroll the page. And it lets the website lazy-load additional content at the bottom without moving existing content around.

This layout creates uniformly-sized columns, without any rows. It’s quite possible that because this layout has required JavaScript, anything more creative or complex has been too hard to pull off — and we’ve been left with an expectation that masonry layout should only ever be a simple pattern with uniformly-sized columns. Let’s see what’s possible if we build it into CSS instead.

Inventing masonry

A mechanism in CSS for “masonry layout” was first proposed by Mozilla in January 2020 as an extension of CSS Grid, and implemented as an experiment behind a flag in Firefox Nightly. In 2022, Apple started implementing this CSS Grid Level 3 proposal in Safari Technology Preview (where it’s currently on by default), and we’ve been helping to move the web standard along to reach maturity.

However, there are big questions still being asked about how CSS should handle masonry-style layouts. Some people remain skeptical that this capability should be part of CSS Grid, and want it to instead be its own separate display type. Others are questioning whether or not this kind of layout is needed on the web at all — they aren’t sure that well-known websites will use it. With such fundamental disagreements at play, no browser can ship. We must first come to consensus in the CSS Working Group.

This is where we need your help. We’d like real-world web designers and developers to weigh into the discussion, and express what it is that you want. Your input really can make a difference.

In this article, we’ll walk through how the CSS Grid Level 3 proposal works, and how you can use its new capabilities. We’ll show you why we believe these features should be part of CSS Grid, and explain what the alternative would be if the CSS Working Group creates display: masonry instead. And then, we’ll ask you to join the debate to help move us forward. Please do read to the end.

Four demos

To show why we at Apple believe this capability should be part of CSS Grid, we created four demonstrations. If you’d like, try them yourself at webkit.org/demos/grid3. View these demos in a browser that supports Grid Level 3 — currently Safari Technology Preview or Firefox after you’ve turned on the feature flag.

Note there’s a control panel for each demo, with the relevant layout code printed to the page. Turn on “Number items” to see the relationship between the HTML order of content and the layout placement of that content.

Screenshot showing the controls of the demo — here with the numbers turned on

Each demo has a multitude of variations. Switch between variations from the dropdown menu, which changes only the CSS. The HTML stays the same.

Screenshot showing the controls of the demo — here with the dropdown of various alternative layouts showing

Creating a classic masonry / waterfall layout

First, let’s take a look at how to build a classic masonry/waterfall layout. In this gallery of photos, each image is wrapped with a figure element, and the figures are direct children of a main element.

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

We start by applying display: grid to the main element to create the Grid container. Then we can define grid-template-columns however we’d like.

In this case, let’s use grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr)) to ask the browser to repeat a looped definition to create a set of flexible columns that are each of a minimum of 14rem. This results in uniformly-sized columns, typical of the classic masonry/waterfall layout. The gap: 1rem; rule creates a 1rem-wide space between the items — both between the columns, and horizontally between items.

And then, we’ll define the “rows” with the masonry value. (It’s likely the name of this value will change before this ships in browsers — more on that at the end of this article. For now, masonry is what works.)

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

The grid-template-rows: masonry rule tells the browser: “Please do not create rows. Instead pack the content into a masonry/waterfall-like pattern.”

That’s it! In four lines of CSS, with zero media queries or container queries, we’ve created a flexible layout that works on screens of all sizes. And there’s no need to crop content to force everything into same-sized boxes.

masonry layout of photos

In graphic design, a layout that has uniformly-sized columns and no rows is often called a “symmetrical columnar grid”. For centuries, columnar grids were the dominant type of grid used in page design.

Leveraging Grid’s full power to define columns

Now let’s dive into the advantages of combining the full power of CSS Grid with masonry/waterfall packing. CSS Grid provides many options for defining grid our columns. Using fr units to create a symmetrical grid is only one option of many.

How could these possibilities be used for a masonry/waterfall-style layout? Let’s try mixing fixed-sized columns with flexible columns. We could make the first and last column fixed-sized, while the middle columns are flexible, changing in both size and number.

Specifically, the first and last columns are exactly 14 characters wide, while the middle columns are flexible (at least 28 characters wide) and change in number to fill the available space.

main { 
  display: grid;
  grid-template-columns: 14ch repeat(auto-fill, minmax(28ch, 1fr)) 14ch;
  grid-template-rows: masonry;
  gap: 1rem;
}

This is just one of many, many possibilities.

CSS Grid allows for a lot of creativity with its options for defining grid tracks:

  • fixed sizes defined in any unit (px, em, rem, cqi, lh, ch, ic, cap, vw, svh, and many more)
  • max-content and min-content
  • the full power of fr units
  • minmax() functions
  • %-sized
  • auto

These options in CSS Grid allow you to create something much more dynamic and flexible in interesting ways. You can create two stages of flexibility, because the fr-unit sized columns grow and shrink in a separate stage from the minmax()-sized columns. The max-content and min-content values let you size the columns based on the content size, rather than sizing the content based on the column size. The fr units can easily be used to create compound or asymmetrical grids, where the columns are different sizes. The options are endless.

By adding the ability to pack content in a masonry/waterfall pattern to CSS Grid, we maintain the full power of Grid for defining our columns in whichever manner we like.

For example, let’s use grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr) minmax(16rem, 2fr)) minmax(8rem, 1fr) to create a pattern of alternating narrow and wider columns, where all the columns are flexible. More columns are added when there’s space. And there’s always an odd number of columns, ending with a narrow one.

three web browser windows next to each other, showing how the layout adjusts for narrow, medium and wider windows

Even when we define columns using only the simple fr unit, the full power provided in CSS Grid means different columns can be set to different sizes. For fun, let’s use fr units to define a set of columns sized to inject the vibes of the golden ratio by using the Fibonacci sequence in grid-template-columns: 1fr 1fr 2fr 3fr 5fr 8fr;

layout of photos where the columns on the left are very narrow, getting bigger and bigger as them move to the right, in a fibonacci sequence

In a more practical example, let’s use max-content when defining our columns. Content-based sizing is an incredibly power feature of CSS Grid. This demo of a mega menu layout uses grid-template-columns: repeat(auto-fill, minmax(max-content, 30ch)); to ensure that every column is big enough to fit every link without wrapping text.

a menu with a ton of links, like in a website footer, laid out with Grid Level 3

Mega menus have been hard to code, especially across multiple screen sizes. With CSS Grid Level 3, it becomes incredibly easy. A few lines of code creates a dynamic layout which adds and removes columns one at a time as space allows — without any media/container queries, and with wrapping prevention.

Many of these examples could never be created with masonry as a separate display type. The discussion of display: masonry is to only allow symmetrical columns (columns that are the same size as each other), much like multicolumn layout does today.

Leveraging Grid’s ability to let content span columns

CSS Grid also lets us span items across multiple columns. Let’s use that capability to see what interesting options might emerge. How about making every 5th image span two grid columns, while the rest of the images are span one column.

same photo layout, now with random photos being bigger

What if instead, we put a wider class on specifically on images that have a wider aspect-ratio, to make those images span multiple columns. We can also change the styling a bit, making the corners square instead of round, and reducing the grid gap to zero. This gives us a different way to pack photos of different aspect ratios together.

another layout of image, this time where wider images are wider

We also experimented with combining the classic masonry/waterfall layout of photos with View Transitions. When a user clicks/taps on any photo, it grows to span multiple columns. The browser automagically animates the transition. (This demo requires Safari Technology Preview 192 or later.)

These variations of the Photos and Mega Menu demos are just a small taste of all of the many possibilities you get when leveraging the full power of CSS Grid in the column direction, while simultaneously turning off rows.

Columnar vs. Modular Grids

What happens when we keep experimenting? Let’s let go of thinking about “masonry”, and start imagining Grid Level 3 purely as an expansion of Grid. At its core, CSS Grid Level 3 provides a mechanism for turning off rows. It lets us create a columnar grid — a grid that’s made up of columns alone.

By contrast, a modular grid is a grid where everything is lined up in both columns and rows. They became popular in the 20th century during the dominance of modernism in graphic design. CSS Grid Level 1 is really good at making modular grids… that’s what it wants to do. In fact, float-based layouts also encouraged the creation of modular grids on the web, since you had to make all your content the same height to get your floats to clear. Images need to be the same aspect ratio. Text has to be the same length. This is often accomplished on the back-end with policies enforced by the content management system, or on the front-end by CSS that truncates/crops the content.

It’s incredible common for websites to do some variation of this sort of modular grid, laid out here with CSS Grid Level 1.

layout of text, in ridged sets of boxes, row after row, all the same height

Of course, this example is overly simplistic. The article ledes look bare with no teaser images. The uniformity is so strict and formal, the design lacks life. Real websites find other ways to breathe life back into the design.

But what if the layout itself could also provide some vitality and interest? What will happen if we use CSS Grid to create a columnar grid as easily as it creates a modular grid? What if we don’t truncate content, and instead let it be the size that it wants to be — and get the layout to work for the content, rather than forcing the content to work for the layout?

A classic masonry/waterfall layout with various lengths of text looks like this, which is already more engaging since a user can read more about each article:

same article teaser text, this time laid out in columns set with masonry

Although, that’s still a bit visually repetitive. Symmetrical columnar grids often are. We need the rest of the power of CSS Grid to do something more interesting. Let’s make the latest article much bigger, and have it span four columns. A handful of other recent articles can be medium-sized and span two columns. While the older content is smaller, spanning one column.

same text, this time laid out with much more dynamic use of 'masonry-style layout plus spanning columns

Now this otherwise visually boring text is starting to look fairly lively. If we were to add images to each of these articles, it would be it even more dynamic.

Let’s experiment with mixing images and text together on a webpage for a museum. The first grid item is a header that introduces the museum, and provides navigation to other resources. The rest of the content consists of pieces of artwork and their information: title, artist, year, medium, catalog number and location.

Because the paintings are gorgeous, the content looks pretty great in a classic masonry/waterfall layout.

layout of cards, each with a painting and text

Let’s see what else we can do by utilizing two more powerful features of CSS Grid — subgrid and explicit placement.

Using subgrid and explicit placement

The functionality provided by subgrid in CSS Grid Level 2 is incredible, and it’s finally supported in most browsers.

Instead of listing the painting’s metadata in a single left-aligned column, let’s see how we might better use the available space. By using subgrid, we can put the year and catalog number on the right of each card — and line up this data for one painting with the same data for the other paintings.

close-up of this layout, with the Grid Inspector showing, with lines marking columns showing how subgrid works

By adding this new functionality to CSS Grid Level 3, we get the benefit of existing developer tools. You can use the Grid Inspector in Safari Technology Preview today as you try out grid-template-rows: masonry.

If masonry is its own display type, and not part of CSS Grid, it will not get the benefit of subgrid.

We can also use the power of CSS Grid Level 1 to explicitly place the header into the last two columns, moving it to the top right corner of the page with grid-column: -3 / -1.

In just a few lines of layout code, we are using the full power of CSS Grid Levels 1, 2, and 3 to create flexible columns that change in number to accommodate the available size — without using any media queries or container queries.

Hopefully you can see the advantages of fully combining a mechanism for masonry/waterfall layouts with CSS Grid — providing many more creative possibilities than masonry alone.

The Debate

So let’s get into the debate that’s been blocking the CSS Working Group from moving forward. Our hope is that web designers and developers chime in (post to social media, write blog posts) with your thoughts about which direction CSS should take.

Some people, including those of us at Apple, like having “Masonry” be part of CSS Grid. We believe this functionality is a mechanism to expand CSS Grid — allowing it to finally create columnar grids as well as modular grids. And we want this functionality to be mixed with all the other features of Grid, including the powerful options for defining a columns, track spanning, explicit placement, and subgrid.

Other people instead believe Masonry should be its own separate display type. At first glance, defining Masonry with a new display type might make a lot of sense. You do get a tidy separation between layout types.

display: block;
display: inline;
display: flexbox;
display: grid;
display: masonry;

The CSS Working Group has not discussed how the syntax for a separate Masonry display type would work, but perhaps it would be patterned after Multicolumn layout.

main { 
  display: masonry;
  columns: 28ch;
}

Or perhaps the syntax would be patterned after Grid, but with significant limitations:

main {
  display: masonry;
  masonry-columns: repeat(5, minmax(28ch, 1fr)); 
                   /* where only one repeating width is allowed */
}

Either way, it’s clear that advocates of this option want Masonry to be limited to a symmetrical grid — where all the columns are the same size as each other. None of the rest of CSS Grid’s track sizing capabilities would be allowed.

Making masonry a simple and separate layout type would avoid the work necessary to keep Grid and Masonry working together in combination — both now and in the long term. Doing this would simplify the layout model, make it easier to implement in browsers, reduce the potential for performance traps, and allow the feature sets of Grid and Masonry to diverge.

Conversely, we believe the effort needed to add this capability to CSS Grid is worth the many benefits to be had. The CSS Grid Level 3 specification has already been written, and implemented in two browser engines. And yes, while making CSS Grid more complex will make it harder to extend in the future, we believe there’s an advantage to having these two types of grid layouts intertwined. This way the CSS Working Group will always define all new additions for both modular and columnar grids. There won’t be something added to display: grid that will be left out of display: masonry, or vice versa. For example, many developers want CSS Grid Level 4 to provide a mechanism for styling grid areas and grid lines — perhaps a way to add a background color to a track, or create a rule line in a gap. It’d be great to ensure that will work for both modular and columnar grids from Day 1.

Another argument made by advocates of display: masonry is that that masonry is conceptually a fundamentally different layout type from CSS Grid, and therefore should have its own display type. They often describe CSS Grid as inherently being about “lining things up in two-dimensions”, and since masonry only lines things up in one dimension, “it’s not a grid”. (In fact, some have advocated that Masonry is more like Flexbox, since “both line things up in one direction”.)

In many ways, your perspective on this question might depends on what you imagine a grid is.

What is a grid?

Grids are an incredibly important aspect of graphic design. Grids are used to line up text, images and other content in a regular pattern. They help readability and usability by making things predictable.

You can trace their use through thousands of years of history.

two photos of book pages, showing historic columnar layouts from long ago
[Left] Antoine Vérard published a French translation of Boccaccio in 1498. Seen in Thirty Centuries of Graphic Design, by James Craig and Bruce Barton, 1987. [Right] A Russian illustrated sheet proclaiming that ‘Hops are head above all other fruit’. Seen in Books: A Living History by Martyn Lyons, 2011.

It wasn’t until the 20th century that European and American modernists started promoting the idea that “proper” graphic design grids should line content up in both directions — in rows as well as columns.

spread from a book, showing two different newspaper home pages, where articles line up in a modular grid — in both row and column directions
Massimo Vignelli is especially well-known for promoting the idea that lining things up in both columns and rows is a superior practice of graphic design. Two examples from The Vignelli Canon, 2010.

Even today, there is a lot of debate about which type of grid is the best grid or the only legitimate grid. Many designers claim a 12 column grid is the only correct way to design a web page — or 12 columns for “desktop”, 8 columns for “tablet”, and 4 columns for “phones”. At times designers have gotten quite religious about their ideas of what a “proper grid” looks like.

Mark Boulton argued for years that symmetrical columnar grids are incredibly formulaic and boring. He promoted the use of asymmetrical compound grids in design for the web. Today, luckily CSS Grid Level 1 makes it incredibly easy to create both asymmetrical grids and compound grids, giving designers the freedom to create what they want. But only if they also want all their grids to be a modular grids.

Both modular and columnar grids are in fact grids. And CSS Grid deserves the ability to also create columnar grids.

photograph of an open book, showing many many hand-drawn diagrams of how different layouts can use different numbers of columns
Ideas for designing columnar grids, in Editing by Design, by Jan V. White.

We believe there’s an opportunity for CSS to enable a rich history of design grids to come to the web — and would be greatly disappointed to see the new masonry feature limited to only allowing symmetrical columnar grids.

But what do you think?

We want to hear from you

This is where you come in. Try some demos of your own. Write about your thoughts on your own blog. Comment at the CSS Working Group in this issue.

  • Should “masonry”/“waterfall” be part of CSS Grid?
  • Do you want the capabilities to define a columnar grid with CSS Grid — to use subgrid, spanning, explicit placement, and all the many options for track sizing? Or do you only want the ability to define a classic masonry layout with equal-sized columns?
  • Will you use this? What might you create with it?
  • Do you have links to demos you’ve made? We want to see your ideas and use cases.
  • Are there things you want to do that you can’t do with this model?

Often, thinking about something theoretically and actually seeing it in use can be very different. To make sure the CSSWG gets the design of this feature correct, we need developers to gain some hands-on experience, and articulate your thoughts about what it’s like.

The WebKit team has been working on Masonry for a year and a half. It was first enabled by default in Safari Technology Preview 163 in February 2023. There’s a bit more polish needed, and details to work out (naming being one). But we would like to ship this feature soon. To do so, these fundamental questions need to be resolved.

Thank you for your help!

P.S. About the name…

It’s likely masonry is not the best name for this new value. Names in CSS are usually simple words that directly describe the result they create — like center, under, contain, clip, revert, ltr, always, break-word, hidden, allow-end, scale-down, wrap, smooth.

The word “masonry” is more of a metaphor, where the meaning has to be explained with a backstory. Such a term is harder to remember for developers who do not speak English. And arguably, the syntax could just as easily be grid-template-rows: waterfall instead, since that’s the dominant word for this layout used in certain regions, not masonry.

Plus, once you start to write a lot of code using this feature, it’s likely you’ll come to the realization that we did — this really isn’t about the layout used by Pinterest or other similar sites. This is a mechanism for telling the browser, “please create a grid, but without any rows.

Perhaps the best syntax could be grid-template-rows: none; to convey “please do not give me any rows”. Sadly, it’s too late to use this name, because none is the default value for grid-template-* and means “please give me only implicit rows, no explicit ones”.

Instead we could use the name off to convey “please turn off the grid in the row direction, and give me only columns”.

main { 
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
  grid-template-rows: off; 
}

The CSSWG is debating this name in this issue. If you have ideas or preferences for a name, please join that discussion.

Meanwhile, masonry is the value that is currently implemented in Safari Technology Preview, since that’s what the Editor’s Draft currently uses. And so that’s what we used in our demos above, and what you should use in yours. But do expect the name of this value to change in the future. And perhaps prepare for a future where we call this “columnar grid” or “Grid Level 3” instead of “Masonry”.

]]>
Implementing Vertical Form Controls https://webkit.org/blog/15190/implementing-vertical-form-controls/ Mon, 18 Mar 2024 15:00:35 +0000 https://webkit.org/?p=15190 Safari 17.4 adds vertical writing mode support for form control elements across macOS, iOS, iPadOS, and visionOS.

all the form controls, typeset vertically

Setting written text vertically is commonly observed in East Asian languages. For example, Chinese, Japanese, and Korean (CJK) may be written vertically and read top-to-bottom, flowing in lines from right to left. Similarly, Traditional Mongolian is a vertical script that flows in lines from left to right.

Support for vertical text has been available in browsers for several years using the CSS writing-mode property. However, until recently, support for the vertical-lr and vertical-rl values for form controls was inconsistent among all browsers. Consequently, as part of Interop 2023, the industry committed to working towards vertical writing mode support in form controls. We are excited to see cross-browser support improve considerably. And we are proud that Safari 17.4 brings support for vertical writing modes to form controls everywhere you find Safari.

Using vertical writing modes with form controls

Adopting vertical form controls is as simple as adding a CSS declaration using the writing-mode property and applying it to your controls. For a right-to-left block flow direction, as observed in CJK languages, use writing-mode: vertical-rl. Or, for a left-to-right block flow direction, use writing-mode: vertical-lr.

button, textarea, progress, meter, input, select {
    writing-mode: vertical-rl;
}

Support for vertical writing mode is available for the following elements: <button>, <textarea>, <progress>, <meter>, <input>, and <select>. WebKit is committed to supporting vertical writing modes on any new controls moving forward, including <input type="checkbox" switch>, which was also added in Safari 17.4.

Note that in all browsers, any popup UIs associated with form controls, such as the menu for a <select> element, or the color picker for an <input type="color"> are still displayed horizontally.

Implementation

Adding vertical writing mode support for form control elements in WebKit was a large undertaking that involved over 10 unique types of controls. Below are some of the changes that were needed to support this feature in WebKit.

Style

WebKit’s user-agent stylesheet made frequent use of physical properties, such as width, height, and margin. In order to support vertical writing modes, this usage was updated to use logical properties, such as inline-size and block-size, where appropriate.

Layout

Form controls in WebKit make heavy use of custom layout code. While the interfaces were designed to support logical properties, the implementations often assumed a horizontal writing mode. We updated the methods to compute logical widths to ensure they considered the writing mode.

Additionally, custom baseline adjustment logic, which makes controls such as checkboxes and radio buttons look great alongside text, was updated to use the central baseline for vertical writing modes. This ensures that controls continue to look great alongside text in a vertical writing mode.

Rendering

Rendering form controls with a vertical writing mode required unique changes depending on the control and system capabilities.

On macOS, WebKit’s form controls match the look and feel of the operating system. However, macOS itself does not support vertical writing modes. Consequently, some controls, such as <progress>, are simply rotated after obtaining the system image in order to support vertical rendering.

two progress bars, laid out horizontally and vertically

Other controls cannot simply be rotated due to details such as shadows, for example <select>, so we used a custom fallback rendering approach.

the select menu typeset vertically

If a control was already custom painted in WebKit, it was updated to use logical coordinates rather than physical coordinates. We used this approach throughout the iOS rendering code and it was also necessary in our rewrite of the listbox (<select multiple>) on macOS.

the select multiple form control, typeset vertically

Finally, some controls, such as checkbox and radio buttons, did not require any rendering changes. As “glyph-like” controls, they look the same in all writing modes.

checkboxes and radio buttons on iOS typeset vertically

WebKit remains committed to improving internationalization support in our engine. By adding vertical writing mode support for form control elements in Safari 17.4, we hope to empower authors to create the best content for their local and global communities.

A special thanks goes to Tim Nguyen, who kicked off the vertical form controls project in WebKit, and Alan Baradlay, whose deep knowledge of CSS layout proved invaluable in driving the project to the finish line.

Feedback

We love hearing from you. Send a tweet to @webkit to share your thoughts on this feature. Find us on Mastodon at @jensimmons@front-end.social and @jondavis@mastodon.social. You can also follow WebKit on LinkedIn. If you run into any issues, we welcome your WebKit bug reports on WebKit features like this. Reporting issues makes an enormous difference.

You can also download the latest Safari Technology Preview to try out new web platform features like this before they appear in a Safari beta.

]]>
CSS Nesting and the Cascade https://webkit.org/blog/14571/css-nesting-and-the-cascade/ Thu, 28 Sep 2023 17:00:46 +0000 https://webkit.org/?p=14571 You might have noticed that Safari Technology Preview 179 includes an update to CSS Nesting that adds support for the new “relaxed parsing behavior”.

What does this mean? It means you no longer have to worry about whether or not each nested selector starts with a symbol. It means that now nested CSS like this will work just fine:

article {
  h1 { 
    font-size: 1.8rem;
  }
  p {
    font-size: 1.2rem;  
  }
}

(If you didn’t realize there was a previous limitation and are curious about what it was, you can read about it in Try out CSS Nesting today, from Feb 2023. But also, you can ignore this limitation since it’s going away soon.)

This is fantastic news. After many months of debates over how CSS Nesting could work, we ended up with the best possible solution. In the end, browser engineers figured out how to make the parsing engine handle nested type selectors (element selectors).

How is browser support? In late August 2023, Firefox 117 shipped support for Nesting using the relaxed parsing behavior from the beginning. Safari 16.5 shipped the original version of Nesting in May 2023, and Safari Technology Preview 179 brought the update to the relaxed parsing behavior in September 2023. Chrome is tracking their coming update in this issue.

By the way, any code written with an & will continue to work. In fact, & is an important tool for CSS like this:

ul {
  padding-left: 1em;
  article & {
    padding-left: 0;
  }
}

Which is the equivalent of:

ul {
  padding-left: 1em;
}
article ul {
  padding-left: 0;
}

The & gives you a way to say “this is where I want the nested selector to go”. It still does the same job, it’s simply no longer required before an element selector.

Another question

There is one more thing about CSS Nesting that’s still up for debate. We still have time to make the change if we do so very soon.

Let us ask you a question. If you wrote this nested CSS, which color would you want the article text to be?

article {
  color: blue;
  @supports (text-shadow: 0 0) {
    color: red;
  }
  color: yellow;
}

Do you want it to result in this unnested equivalent, Option 1, where color: red wins?

article {
  color: blue;
  color: yellow; 
}
@supports (text-shadow: 0 0) {
  article {
      color: red;
  }
}

Or do you want it be computed to be this equivalent, Option 2, where color: yellow wins?

article {
  color: blue;
}
@supports (text-shadow: 0 0) {
  article {
      color: red;
  }
}
article {
  color: yellow; 
}

Currently, the Nesting specification says Option 1 is correct, and so that’s how it’s implemented in browsers. This is how all of the preprocessors work: Less (demo), Sass (demo), Stylus (demo), PostCSS (demo), and more. Perhaps matching over fifteen years of third-party tooling is the best way to go.

But many people find this to be an unexpected gotcha, since it seemingly reorders styles. It makes something that’s earlier in the cascade override something that’s later in the cascade. These folks expect Option 2 to be how it works, where the fundamentals of the cascade stay fully intact — when two declarations have the same specificity, the later one always wins.

We ran a survey to find out what you want. The results were remarkable consistent across the first 48 hours.

Option 1: 38%
Option 2: 62%

Your input helps the CSS Working Group make a final decision on how code like this should work. It’s still not fully clear if Option 2 is possible, but before embarking on a deeper effort to find out, it helps to know what web developers want.

Thanks for participating!

]]>
Try out CSS Nesting today https://webkit.org/blog/13813/try-css-nesting-today-in-safari-technology-preview/ Wed, 08 Feb 2023 18:00:02 +0000 https://webkit.org/?p=13813 Back in December, we wrote an article detailing three different options for CSS Nesting. In it, we explained the differences between Option 3, Option 4 and Option 5, demonstrating how each would work through a series of examples. Then we asked a simple question: “Which option is best for the future of CSS?”

Web developers responded to the poll with great clarity. Option 3 won in a landslide.

And so now, both Safari and Chrome have implemented Option 3. Two weeks ago, on January 25th, CSS Nesting shipped in Safari Technology Preview 162, on by default. If you have a Mac, simply download and open Safari Technology Preview, write some nested CSS, and experience how it works!

How CSS Nesting Works

Imagine you have some CSS that you’d like to write in a more compact way.

.foo {
  color: green;
}
.foo .bar {
  font-size: 1.4rem;
}

With CSS Nesting, you can write such code as:

.foo {
  color: green;
 .bar {
    font-size: 1.4rem;
  }
}

If you’ve been nesting styles in Sass, you will find this very familiar.

Unlike Sass, however, this kind of nesting will not always work. Because of limitations in browser parsing engines, you must make sure the nested selector (.bar in the above example) always starts with a symbol.

NOTE: This limitation is no longer true. Safari Technology Preview 179+ does not require a symbol at the beginning of a nested element selector. But jump down past this to learn more about what’s handy about nesting.

If it’s a class, ID, pseudo-class, pseudo-element, attribute selector, or any selector that uses a symbol at the beginning — you’ve succeeded. For example, all of these will be fine. All of the following nested selectors start with a symbol — . # : [ * + > ~ — not a letter:

main {
 .bar { ... }
 #baz { ...}
 :has(p) { ... }
 ::backdrop { ... }
 [lang|="zh"] { ... }
 * { ... }
 + article { ... }
 > p { ... }
 ~ main { ... }
}

There is one kind of selector that starts with a letter, however — a nested element selector. This example will not work:

main {
 article { ... }
}

That code will fail, because article begins with a letter, and not a symbol. How will it fail? The same way it would fail if you’d misspelled article as atirlce. The nested CSS which depends on that particular selector is simply ignored.

You have several options for what to do about this limitation. Let’s start by looking at the solution that you’ll probably use most often. You can simply put an & before the element selector, like this:

main {
 & article { ... }
}

The & signals to the browser “this is where I want the selector from outside this nest to go”. By using an & before any element selectors, you succeed at starting the nested selector with a symbol, not a letter. Therefore, it will work.

aside {
 & p { ... }
}

is the nested equivalent to:

aside p { ... }

The & is also super handy for other use cases.

Imagine you have this unnested code:

ul {
  padding-left: 1em;
}
.component ul {
  padding-left: 0;
}

You’ll notice that the intended selector is .component ul — where the ul is second.

To write nested rules that yield such a result, you can write:

ul {
  padding-left: 1em;
  .component & {
    padding-left: 0;
  }
}

Again, the & gives you a way to say “this is where I want the nested selector to go”.

It’s also handy when you don’t want a space between your selectors. For example:

a {
  color: blue;
  &:hover {
    color: lightblue;
  }
}

Such code yields the same result as a:hover {. Without the &, you’d get a :hover { — notice the space between a and :hover — which would fail to style your hover link.

But what if you have this unnested code?

ul {
  padding-left: 1em;
}
article ul {
  padding-left: 0;
}

You do not want to write the nested version like this:

ul {
  padding-left: 1em;
  & article & {
    padding-left: 0;
  }
}

Why not? Because that will actually behave in the same way as these unnested rules:

ul {
  padding-left: 1em;
}
ul article ul {
  padding-left: 0;
}

Two unordered lists in ul article ul? No, that’s not what we want.

So, what do we do instead, since we need to start article & with a symbol?

We can write our code like this:

ul {
  padding-left: 1em;
  :is(article) & {
    padding-left: 0;
  }
}

Any selector can be wrapped by an :is() pseudo-class and maintain the same specificity and meaning (when it’s the only selector inside the parentheses). Put an element selector inside :is(), and you get a selector that starts with a symbol for the purposes of CSS Nesting.

In summary, CSS Nesting will work just like Sass, but with one new rule: you must make sure the nested selector always starts with a symbol.

Investigations are currently underway to see if this restriction can be relaxed without making the parsing engine slower. The restriction may very well be removed — whether sometime very soon or years in the future. Everyone agrees Nesting will be much better without any such restriction. But we also all agree that web pages must appear in the browser window right away. Adding even the slightest pause before rendering begins is not an option.

What is an option? You being able to structure your nested code however you’d like. You can nest more than one layer deep — nesting CSS inside already-nested CSS — in as many levels as you desire. You can mix Nesting with Container Queries, Feature Queries, Media Queries, and/or Cascade Layers however you want. Anything can go inside of anything.

Try out CSS Nesting today and see what you think. Test to your code in both Safari Technology Preview and Chrome Dev (after flipping the “Experimental Web Platform features” flag) to make sure it yields the same results. This is the best time to find bugs — before this new feature has shipped in any browser. You can report issues at bugs.webkit.org or bugs.chromium.org. Also, keep an eye out for the release notes for next several versions of Safari Technology Preview. Each might add polish to our CSS Nesting implementation, including efforts that add support for CSSOM or other updates to match any potential spec changes made by the CSS Working Group.

A lot of people across multiple companies have been working to bring nesting to CSS for almost five years. The syntax has been hotly debated, in long conversations about the pros and cons of many different solutions. We hope you find the result immensely helpful.

]]>
Help choose the syntax for CSS Nesting https://webkit.org/blog/13607/help-choose-from-options-for-css-nesting-syntax/ Thu, 15 Dec 2022 18:00:45 +0000 https://webkit.org/?p=13607 The CSS Working Group is continuing a debate over the best way to define nesting in CSS. And if you are someone who writes CSS, we’d like your help.

Nesting is a super-popular feature of tools like Sass. It can save web developers time otherwise spent writing the same selectors over and over. And it can make code cleaner and easier to understand.

Unnested CSS

.class1 {
  color: green;
}
.class1 .class2 {
  border: 5px solid black;
}

Nesting in Sass

.class1 {
  color: green;
  .class2 {
    border: 5px solid black;
  }
}

Everyone wishes CSS nesting could use the same kind of simple syntax that Sass does. That’s impossible, however, because of the way browser parsing engines work. If they see a sequence like element:pseudo, they’ll parse the entire style rule as if it were a property: value declaration.

So, a lengthy discussion began about what to do instead. Earlier this summer, the CSSWG debated between Option 1, Option 2 and Option 3. Of those, Option 3 won. Since then, two more options have been proposed, Option 4 and Option 5. If you remember various details discussed across 53+ issues, put all of those older ideas aside. At this point, we are only debating between Option 3, 4 and 5, as described in this article through the set of examples below.

To help us decide which option to choose, we’d like you to read these examples and answer a one-question survey.

Please look at these examples, consider how you write and maintain style sheets, and think deeply about which syntax you prefer. Then vote for the one you think is best.

In all three options, the & symbol is a marker that means “put the selector that’s outside of the nest [here]”. For example…

This…

.foo {
  & .bar {
    color: blue;
  }
}

… becomes this.

.foo .bar {
  color: blue;
}

And this…

.foo {
  .bar & {
    color: blue;
  }
}

… becomes this.

.bar .foo {
  color: blue;
}

Got it?

In many of the following examples, the & is optional. Some developers will use it because it helps make their code more readable. Other developers will opt to leave it out, especially when copying & pasting code from unnested contexts. When the & is left out, the outside selector is added at the beginning, as an ancestor. (When we surveyed authors, their opinions on use of optional & were split 50/50.)

But here’s the key trick to Option 3 (and only Option 3) — if you are using an element selector (like p, article, or div), the & is required. If you are using any other selector, like a class or ID, you can choose to leave the & out. The easiest way to remember this: the selector can never begin with a letter. It must begin with a symbol. Some people in the CSSWG believe this will be easy to remember and do. Others wonder, given that style rules placed anywhere else in CSS don’t have such a syntactic restriction, if it will create confusion, tripping up developers in a way that is hard to debug.

As you compare the options, think about how your whole team will handle nested code. Think about what it might be like to copy & paste code from one project to another. Which option will make it easy to code inside and outside a nested context? Which will make it easiest to read, write, and edit large quantities of CSS?

Which is best for the future of CSS? When people write CSS thirty years from now — long after today’s habits and expectations are completely forgotten, when future generations have never heard of Sass — which option will make writing this language easy and elegant?

Do be sure to read through all the examples. One option might stand out as your favorite in one example, and yet, have problems that you don’t like in another example.

Comparing the Options

  • Option 5: Top-level @nest: Nested style rules are declared in a dedicated, independent at-rule that accepts only style rules. Declarations can be nested using & { .. }.
  • Option 4: Postfix block: Style rules allow for an optional, second block after the declaration block that contains only style rules.
  • Option 3: Non-letter start: Nested style rules can be added directly into a declaration block, but cannot start with a letter.

Example A: The Basics

Unnested CSS

article {
  font-family: avenir;
}
article aside {
  font-size: 1rem;
}

Option 5

@nest article {
  & {
    font-family: avenir;
  }
  aside {
    font-size: 1rem;
  }
}

Option 4

article {
  font-family: avenir;
} {
  aside {
    font-size: 1rem;
  }
}

Option 3

article {
  font-family: avenir;
  & aside {
    font-size: 1rem;
  }
}

Example B: With syntax variations

There are a lot of different ways developers like to structure their CSS — using tabs vs spaces, putting the { } braces on the same line as a rule or on separate lines, ordering rules in a certain order. This will also be true when writing nested CSS. Example B articulates a few of the possible variations of how you might format nested code for each option. It’s totally up to your personal preference.

Unnested CSS

.foo {
  color: red;
}
.foo .bar {
  color: blue;
}
.foo p {
  color: yellow;
}

Option 5

@nest .foo {
  & {
    color: red;
  }
  .bar {
    color: blue;
  }
  p {
    color: yellow;
  }
}

Or you can use this syntax:

@nest .foo {{
  color: red; }
  .bar {
    color: blue;
  }
  p {
    color: yellow;
  }
}

Option 4

.foo {
  color: red;
} {
  .bar {
    color: blue;
  }
  p {
    color: yellow;
  }
}

Or you can format it like this:

.foo {
  color: red; } {
  .bar {
    color: blue;
  }
  p {
    color: yellow;
  }
}

Option 3

.foo {
  color: red;
  .bar {
    color: blue;
  }
  & p {
    color: yellow;
  }
}

Or you can use this syntax:

.foo {
  color: red;
  & .bar {
    color: blue;
  }
  & p {
    color: yellow;
  }
}

Example C: Re-nesting inside an element selector

There is a situation for Option 3 where just using an & is not enough — when we want the parent selectors referenced by & to come after the nested selector. Since we can’t start with &, we need to use something like :is() or :where() in order to start with a symbol instead of a letter.

Unnested CSS

a:hover {
  color: hotpink;
}
aside a:hover {
  color: red;
}

Option 5

@nest a:hover {
  & {
    color: hotpink;
  }
  aside & {
    color: red;
  }
}

Option 4

a:hover {
  color: hotpink;
} {
  aside & {
    color: red;
  }
}

Option 3

a:hover {
  color: hotpink;
  :is(aside) & {
    color: red;
  }
}

Example D: Zero unnested declarations + various selectors

Unnested CSS

:has(img) .product {
  margin-left: 1rem;
}
:has(img) h2 {
  font-size: 1.2rem;
}
:has(img) > h3 {
  font-size: 1rem;
}
:has(img):hover {
  box-shadow: 10px 10px;
}
a:has(img) {
  border: none;
}

Option 5

@nest :has(img) {
  .product {
    margin-left: 1rem;
  }
  h2 {
    font-size: 1.2rem;
  }
  > h3 {
    font-size: 1rem;
  }
  &:hover {
    box-shadow: 10px 10px;
  }
  a& {
    border: none;
  }
}

Option 4

:has(img) {} {
  .product {
    margin-left: 1rem;
  }
  h2 {
    font-size: 1.2rem;
  }
  > h3 {
    font-size: 1rem;
  }
  &:hover {
    box-shadow: 10px 10px;
  }
  a& {
    border: none;
  }
}

Option 3

:has(img) {
  .product {
    margin-left: 1rem;
  }
  & h2 {
    font-size: 1.2rem;
  }
  > h3 {
    font-size: 1rem;
  }
  &:hover {
    box-shadow: 10px 10px;
  }
  :is(a&) {
    border: none;
  }
}

Example E: Nesting inside of nesting

Unnested CSS

table.colortable td {
  text-align: center;
}
table.colortable td .c {
  text-transform: uppercase;
}
table.colortable td:first-child,
table.colortable td:first-child + td {
  border: 1px solid black;
}
table.colortable th {
  text-align: center;
  background: black;
  color: white;
}

Option 5

@nest table.colortable {
  @nest td {
    & {
      text-align: center;
    }
    .c {
      text-transform: uppercase;
    }
    &:first-child,
    &:first-child + td {
       border: 1px solid black;
    }
  }
  th {
    text-align: center;
    background: black;
    color: white;
  }
}

Option 4

table.colortable {} {
  td {
    text-align: center; }{
    .c {
      text-transform: uppercase;
    }
    &:first-child,
    &:first-child + td {
       border: 1px solid black;
    }
  }
  th {
    text-align: center;
    background: black;
    color: white;
  }
}

Option 3

table.colortable {
  & td {
    text-align: center;
    .c {
      text-transform: uppercase;
    }
    &:first-child,
    &:first-child + td {
       border: 1px solid black;
    }
  }
  & th {
    text-align: center;
    background: black;
    color: white;
  }
}

Example F: Integration with Media Queries

Unnested CSS

ol, ul {
  padding-left: 1em;
}

@media (max-width: 30em){
  .type ul,
  .type ol {
    padding-left: 0;
  }
}

Option 5

@nest ol, ul {
  & {
    padding-left: 1em;
  }
  @media (max-width: 30em){
    .type & {
      padding-left: 0;
    }
  }
}

Option 4

ol, ul {
  padding-left: 1em;
} {
  @media (max-width: 30em){
    .type & {
      padding-left: 0;
    }
  }
}

Option 3

ol, ul {
  padding-left: 1em;
  @media (max-width: 30em){
    .type & {
      padding-left: 0;
    }
  }
}

Example G: Integration with Cascade Layers

Unnested CSS

@layer base {
  html {
    width: 100%;
  }
  @layer support {
    html body {
      min-width: 100%;
    }
  }
}

Option 5

@layer base {
  @nest html {
    & {
      width: 100%;
    }
    @layer support {
      body {
        min-width: 100%;
      }
    }
  }
}

Option 4

@layer base {
  html {
    width: 100%;
  } {
    @layer support {
      body {
        min-width: 100%;
      }
    }
  }
}

Option 3

@layer base {
  html {
    width: 100%;
    @layer support {
      & body {
        min-width: 100%;
      }
    }
  }
}

Now that you’ve had a chance to absorb examples of the three options, and maybe play around with a few of your own, which do you believe should become the way CSS is written in the future?

Which option is best for the future of CSS?

  • Option 5
    9%
     
  • Option 4
    5%
     
  • Option 3
    86%
     

If you’d like to describe more about your thoughts, reply to @webkit on Twitter, or @jensimmons@front-end.social on Mastodon.

Thank you for your feedback. There’s a lot that goes into designing a programming language, not just the results of a survey like this. But knowing what web developers think after reading examples will help us with our ongoing discussions.

Please pass along this survey to other people you know who also write CSS, post it to social media, and help us get the word out. The more people who voice their opinion, the better.

We especially want to hear from developers working on a wide variety of projects — from websites to web apps, native apps to digital signage to printed books. Whether large and small projects, huge or tiny teams, brand-new or decades-old codebases. Built on any variety of frameworks, CMSes, build systems… dynamic or static, rendered server-side or client-side… there are a lot of ways people use CSS, all around the globe. We need to consider them all.

]]> Using :has() as a CSS Parent Selector and much more https://webkit.org/blog/13096/css-has-pseudo-class/ Thu, 18 Aug 2022 15:30:51 +0000 https://webkit.org/?p=13096 It’s been a long-standing dream of front-end developers to have a way to apply CSS to an element based on what’s happening inside that element.

Maybe we want to apply one layout to an article element if there’s a hero image at the top, and a different layout if there is no hero image. Or maybe we want to apply different styles to a form depending on the state of one of its input fields. How about giving a sidebar one background color if there’s a certain component in that sidebar, and a different background color if that component is not present? Use cases like these have been around for a long time, and web developers have repeatedly approached the CSS Working Group, begging them to invent a “parent selector”.

Over the last twenty years, the CSS Working Group discussed the possibility many, many times. The need was clear and well understood. Defining syntax was a doable task. But figuring out how a browser engine could handle potentially very complex circular patterns, and get through the calculations fast enough seemed impossible. Early versions of a parent selector were drafted for CSS3, only to be deferred. Finally, the :has() pseudo-class was officially defined in CSS Selectors level 4. But having a web standard alone didn’t make :has() a reality. We still needed a browser team to figure out the very real performance challenge. In the meantime, computers continued to get more powerful and faster year after year.

In 2021, Igalia started advocating for :has() among browser engineering teams, prototyping their ideas and documenting their findings regarding performance. The renewed attention on :has() caught the attention of engineers who work on WebKit at Apple. We started implementing the pseudo-class, thinking through possibilities for the needed performance enhancements to make this work. We debated whether to start with a faster version with a very limited and narrow scope of what it could do, and then try to remove those limits if possible… or to start with something that had no limits, and only apply restrictions as required. We went for it, and implemented the more powerful version. We developed a number of novel :has-specific caching and filtering optimizations, and leveraged the existing advanced optimization strategies of our CSS engine. And our approach worked, proving that after a two decade wait, it is finally possible to implement such a selector with fantastic performance, even in the presence of large DOM trees and large numbers of :has() selectors.

The WebKit team shipped :has() in Safari Technology Preview 137 in December 2021, and in Safari 15.4 on March 14, 2022. Igalia did the engineering work to implement :has() in Chromium, which will ship in Chrome 105 on August 30, 2022. Presumably the other browsers built on Chromium won’t be far behind. Mozilla is currently working on the Firefox implementation.

So, let’s take a step-by-step hands-on look at what web developers can do with this desperately desired tool. It turns out, the :has() pseudo-class is not just a “parent selector”. After decades of dead-ends, this selector can do far more.

The basics of how to use :has() as a parent selector

Let’s start with the basics. Imagine we want to style a <figure> element based on the kind of content in the figure. Sometimes our figure wraps only an image.

<figure>
  <img src="flowers.jpg" alt="spring flowers">
</figure>

While other times there’s an image with a caption.

<figure>
  <img src="dog.jpg" alt="black dog smiling in the sun">
  <figcaption>Maggie loves being outside off-leash.</figcaption>
</figure>

Now let’s apply some styles to the figure that will only apply if there is a figcaption inside the figure.

figure:has(figcaption) {
  background: white;
  padding: 0.6rem;
}

This selector means what it says — any figure element that has a figcaption inside will be selected.

Here’s the demo, if you’d like to alter the code and see what happens. Be sure to use a browser that supports :has() — as of today, that’s Safari.

See the Pen
:has() demo — Figure variations
by Jen Simmons (@jensimmons)
on CodePen.

In this demo, I also target any figure that contains a pre element by using figure:has(pre).

figure:has(pre) { 
  background: rgb(252, 232, 255);
  border: 3px solid white;
  padding: 1rem;
}

And I use a Selector Feature Query to hide a reminder about browser support whenever the current browser supports :has().

@supports selector(:has(img)) {
  small {
    display: none;
  }
}

The @supports selector() at-rule is itself very well supported. It can be incredibly useful anytime you want to use a feature query to test for browser support of a particular selector.

And finally, in this first demo, I also write a complex selector using the :not() pseudo-class. I want to apply display: flex to the figure — but only if an image is the sole content. Flexbox makes the image stretch to fill all available space.

I use a selector to target any figure that does not have any element that is not an image. If the figure has a figcaption, pre, p, or an h1 — or any element at all besides img — then the selector doesn’t apply.

figure:not(:has(:not(img))) {
  display: flex;
}

:has() is a powerful thing.

A practical example using :has() with CSS Grid

Let’s look at a second demo where I’ve used :has() as a parent selector to easily solve a very practical need.

I have several article teaser cards laid out using CSS Grid. Some cards contain only headlines and text, while others also have an image. I want the cards with images to take up more space on the grid than those without images.

I don’t want to have to do extra work to get my content management system to apply a class or to use JavaScript for layout. I just want to write a simple selector in CSS that will tell the browser to make any teaser card with an image to take up two rows and two columns in the grid.

The :has() pseudo-class makes this simple:

article:has(img) {
  grid-column: span 2;
  grid-row: span 2;
}

See the Pen
:has() demo — teaser cards
by Jen Simmons (@jensimmons)
on CodePen.

These first two demos use simple element selectors from the early days of CSS, but all of the selectors can be combined with :has(), including the class selector, the ID selector, the attribute selector — and powerful combinators.

Using :has() with the child combinator

First, a quick review of the difference between the descendant combinator and the child combinator (>).

The descendant combinator has been around since the very beginning of CSS. It’s the fancy name for when we put a space between two simple selectors. Like this:

a img { ... }

This targets all img elements that are contained within an a element, no matter how far apart the a and the img are in the HTML DOM tree.

<a>
  <figure>
    <img src="photo.jpg" alt="don't forget alt text" width="200" height="100">
  </figure>
</a>

Child combinator is the name for when we put an > between two selectors — which tells the browser to target anything that matches the second selector, but only when the second selector is a direct child of the first.

a > img { ... }

For example, this selector targets all img elements wrapped by an a element, but only when the img is immediately after the a in the HTML.

<a>
  <img src="photo.jpg" alt="don't forget alt text" width="200" height="100">
</a>

With that in mind, let’s consider the difference between the following two examples. Both select the a element, rather than the img, since we are using :has().

a:has(img) { ... }
a:has(> img) { ... }

The first selects any a element with an img inside — any place in the HTML structure. While the second selects an element only if the img is a direct child of the a.

Both can be useful; they accomplish different things.

See the Pen
:has() — descendant combinator vs child combinator
by Jen Simmons (@jensimmons)
on CodePen.

There are two additional types of combinators — both are siblings. And it’s through these that :has() becomes more than a parent selector.

Using :has() with sibling combinators

Let’s review the two selectors with sibling relationships. There’s the next-sibling combinator (+) and the subsequent-sibling combinator (~).

The next-sibling combinator (+) selects only the paragraphs that come directly after an h2 element.

h2 + p
<h2>Headline</h2>
<p>Paragraph that is selected by `h2 + p`, because it's directly after `h2`.</p>

The subsequent-sibling combinator (~) selects all paragraphs that come after an h2 element. They must be siblings, but there can be any number of other HTML elements in between.

h2 ~ p
<h2>Headline</h2>
<h3>Something else</h3>
<p>Paragraph that is selected by `h2 ~ p`.</p>
<p>This paragraph is also selected.</p>

Note that both h2 + p and h2 ~ p select the paragraph elements, and not the h2 headlines. Like other selectors (think of a img), it’s the last element listed that is targeted by the selector. But what if we want to target the h2? We can use sibling combinators with :has().

How often have you wanted to adjust the margins on a headline based on the element following it? Now it’s easy. This code allows us to select any h2 with a p immediately after it.

h2:has(+ p) { margin-bottom: 0; }

Amazing.

What if we want to do this for all six headline elements, without writing out six copies of the selector. We can use :is to simplify our code.

:is(h1, h2, h3, h4, h5, h6):has(+ p) { margin-bottom: 0; }

Or what if we want to write this code for more elements than just paragrapahs? Let’s eliminate the bottom margin of all headlines whenever they are followed by paragraphs, captions, code examples and lists.

:is(h1, h2, h3, h4, h5, h6):has(+ :is(p, figcaption, pre, dl, ul, ol)) { margin-bottom: 0; }

Combining :has() with descendant combinators, child combinators (>), next-sibling combinators (+), and subsequent-sibling combinators (~) opens up a world of possibilities. But oh, this is still just the beginning.

Styling form states without JS

There are a lot of fantastic pseudo-classes that can be used inside :has(). In fact, it revolutionizes what pseudo-classes can do. Previously, pseudo-classes were only used for styling an element based on a special state — or styling one of its children. Now, pseudo-classes can be used to capture state, without JavaScript, and style anything in the DOM based on that state.

Form input fields provide a powerful way to capture such a state. Form-specific pseudo-classes include :autofill, :enabled, :disabled, :read-only, :read-write, :placeholder-shown, :default, :checked, :indeterminate, :valid, :invalid, :in-range, :out-of-range, :required and :optional.

Let’s solve one of the use cases I described in the introduction — the long-standing need to style a form label based on the state of the input field. Let’s start with a basic form.

<form>
  <div>
    <label for="name">Name</label> 
    <input type="text" id="name">
  </div>
  <div>
    <label for="site">Website</label> 
    <input type="url" id="site">
  </div>
  <div>
    <label for="email">Email</label>
    <input type="email" id="email">
  </div>
</form>

I’d like to apply a background to the whole form whenever one of the fields is in focus.

form:has(:focus-visible) { 
  background: antiquewhite;
}

Now I could have used form:focus-within instead, but it would behave like form:has(:focus). The :focus pseudo-class always applies CSS whenever a field is in focus. The :focus-visible pseudo-class provides a reliable way to style a focus indicator only when the browser would draw one natively, using the same complex heuristics the browser uses to determine whether or not to apply a focus-ring.

Now, let’s imagine I want to style the other fields, the ones not in focus — changing their label text color and the input border color. Before :has(), this required JavaScript. Now we can use this CSS.

form:has(:focus-visible) div:has(input:not(:focus-visible)) label {
  color: peru;
}
form:has(:focus-visible) div:has(input:not(:focus-visible)) input {
  border: 2px solid peru;
}

What does that selector say? If one of the controls inside this form has focus, and the input element for this particular form control does not have focus, then change the color of this label’s text to peru. And change the border of the input field to be 2px solid peru.

You can see this code in action in the following demo by clicking inside one of the text fields. The background of the form changes, as I described earlier. And the label and input border colors of the fields that are not in focus also change.

See the Pen
:has() demo: Forms
by Jen Simmons (@jensimmons)
on CodePen.

In this same demo, I would also like to improve the warning to the user when there’s an error in how they filled out the form. For years, we’ve been able to easily put a red box around an invalid input with this CSS.

input:invalid {
  outline: 4px solid red;
  border: 2px solid red;
} 

Now with :has(), we can turn the label text red as well:

div:has(input:invalid) label {
  color: red;
}

You can see the result by typing something in the website or email field that’s not a fully-formed URL or email address. Both are invalid, and so both will trigger a red border and red label, with an “X”.

Dark mode toggle with no JS

And last, in this same demo I’m using a checkbox to allow the user to toggle between a light and dark theme.

body:has(input[type="checkbox"]:checked) {
  background: blue;
  --primary-color: white;
}
body:has(input[type="checkbox"]:checked) form { 
  border: 4px solid white;
}
body:has(input[type="checkbox"]:checked) form:has(:focus-visible) {
  background: navy;
}
body:has(input[type="checkbox"]:checked) input:focus-visible {
  outline: 4px solid lightsalmon;
}

I’ve styled the dark mode checkbox using custom styles, but it does still look like a checkbox. With more complex styles, I could create a toggle in CSS.

In a similar fashion, I could use a select menu to provide a user with multiple themes for my site.

body:has(option[value="pony"]:checked) {
  --font-family: cursive;
  --text-color: #b10267;
  --body-background: #ee458e;
  --main-background: #f4b6d2;
}

See the Pen
:has() Demo #5 — Theme picker via Select
by Jen Simmons (@jensimmons)
on CodePen.

Any time there’s an opportunity to use CSS instead of JavaScript, I’ll take it. This results in a faster experience and a more robust website. JavaScript can do amazing things, and we should use it when it’s the right tool for the job. But if we can accomplish the same result in HTML and CSS alone, that’s even better.

And more

Looking through other pseudo-classes, there are so many that can be combined with :has(). Imagine the possibilities with :nth-child, :nth-last-child, :first-child, :last-child, :only-child, :nth-of-type, :nth-last-of-type, :first-of-type, :last-of-type, :only-of-type. The brand new :modal pseudo-class is triggered when a dialog is in the open state. With :has(:modal) you can style anything in the DOM based on whether the dialog is open or closed.

However, not every pseudo-class is currently supported inside :has() in every browser, so do try out your code in multiple browsers. Currently the dynamic media pseudo-classes don’t work — like :playing, :paused, :muted, etc. They very well may work in the future, so if you are reading this in the future, test them out! Also, form invalidation support is currently missing in certain specific situations, so dynamic state changes to those pseudo-classes may not update with :has().

Safari 16 will add support for :has(:target) opening up interesting possibilities for writing code that looks at the current URL for a fragment that matches the ID of a specific element. For example, if a user clicks on a table of contents at the top of a document, and jumps down to the section of the page matching that link, :target provides a way to style that content uniquely, based on the fact the user clicked the link to get there. And :has() opens up what such styling can do.

Something to note — the CSS Working Group resolved to disallow all existing pseudo-elements inside of :has(). For example, article:has(p::first-line) and ol:has(li::marker) won’t work. Same with ::before and ::after.

The :has() revolution

This feels like a revolution in how we will write CSS selectors, opening up a world of possibilities previously either impossible or often not worth the effort. It feels like while we might recognize immediately how useful :has() will be, we also have no idea what is truly possible. Over the next several years, people who make demos and dive deep into what CSS can do will come up with amazing ideas, stretching :has() to its limits.

Michelle Barker created a fantastic demo that triggers the animation of Grid track sizes through the use of :has() and hover states. Read more about it in her blog post. Support for animated grid tracks will ship in Safari 16. You can try out this demo today in Safari Technology Preview or Safari 16 beta.

The hardest part of :has() will be opening our minds to its possibilities. We’ve become so used to the limits imposed on us by not having a parent selector. Now, we have to break those habits.

That’s all the more reason to use vanilla CSS, and not limit yourself to the classes defined in a framework. By writing your own CSS, custom for your project, you can fully leverage all the powerful abilities of today’s browsers.

What will you use :has() for? Last December, I asked on Twitter what use cases folks might have for :has(), and got lots of replies with incredible ideas. I can’t wait to see yours.

]]>
Customizing Color Fonts on the Web https://webkit.org/blog/12662/customizing-color-fonts-on-the-web/ Wed, 25 May 2022 14:00:27 +0000 https://webkit.org/?p=12662

Color fonts provide a way to add richness to your designs without sacrificing any of the many benefits of using plain text. Regardless of how decorative a color font is, the underlying text is always searchable, copy/paste-able, scalable, translatable, and compatible with screen readers.

WebKit now supports CSS @font-palette-values. With this at-rule, you can access predefined color palettes provided by the font designer, and you can customize them in order to make the color font a perfect match for the colors in your designs.

ONCE upon a time in the middle of winter, when the flakes of snow were falling like feathers from the clouds, a Queen sat at her palace window, which had an ebony black frame, stitching her husband’s shirts. While she was thus engaged and looking out at the snow she pricked her finger, and three drops of blood fell upon the snow. Now the red looked so well upon the white that she thought to herself, “Oh, that I had a child as white as this snow, as red as this blood, and as black as the wood of this frame!” Soon afterwards a little daughter came to her, who was as white as snow, and with cheeks as red as blood, and with hair as black as ebony, and from this she was named “Snow-White.” And at the same time her mother died.

THIS answer so angered the Queen that she became quite yellow with envy. From that hour, whenever she saw Snow-White, her heart was hardened against her, and she hated the little girl. Her envy and jealousy increased so that she had no rest day or night, and she said to a Huntsman, “Take the child away into the forest. I will never look upon her again. You must kill her, and bring me her heart and tongue for a token.” The Huntsman listened and took the maiden away, but when he drew out his knife to kill her, she began to cry, saying, “Ah, dear Huntsman, give me my life! I will run into the wild forest, and never come home again.”

You can try out @font-palette-values today in Safari 15.4 or later.

Color palettes work great in WebKit with the COLRv0 font file format, and we are investigating other formats like SVG.

Background on the font above

The font in this demo is a revival of Bradley, a “fairytale blackletter” originally released in 1895. The typeface came with a special set of ornate Initial caps meant for drop caps (see also ::initial-letter) and other titling uses, which David digitized this past December for his Font of the Month Club, just in time for the holidays.

Each glyph is made up of a handful of distinct layers (letterform, backdrop, ornate linework, letter outline, and border). Making the layers different-yet-coordinated colors adds depth to the design, taking it beyond what a simple foreground/background can provide. It felt like the perfect use case for a color font with multiple color palettes, and a unique opportunity for a 127-year-old font to play a small part in an emerging font technology.

Palettes in CSS

Fonts can define one or more of their own color palettes inside the CPAL table inside the font file. The palettes are ordered, and so they are identified by index. For example, a font might define color palette #3 that uses blues and greens, but another palette #5 might use reds and oranges. Colors within a palette inside the font are also identified by index – all palettes contain the same number of colors within themselves.

These color palettes can be tweaked or overridden in CSS using font-palette-values. An example looks like this:

@font-palette-values --lilac-blossom {
    font-family: "Bradley Initials DJR Web";
    base-palette: 7;
    override-colors: 0 #fff, 1 #F3B0EB;
}

This example means “Make a color palette named Lilac Blossom, that, when applied to Bradley Initials DJR Web, is just like the 7th palette in the font, but overrides color #0 in that palette to be white, and color #1 in the palette to be #F3B0EB.” If you don’t want to override any colors, that’s no problem – just delete the entire override-colors descriptor.

You can then apply this color palette by simply supplying it to the font-palette property like this:

font-palette: --lilac-blossom;
The Round Table

 

Progressive Enhancement

If you’re using an older version of Safari, or a different browser which doesn’t understand the font-palette property, it will render the default (0th) color palette in the font. Here’s what the above example would look like in such a browser:

The Round Table

 

If the fallback behavior is undesirable for your particular font, you can detect browsers that understand CSS color palettes in your stylesheet by using the @supports media query, like so:

@supports (font-palette: --lilac-blossom) {
    .lilacblossom {
        font-palette: --lilac-blossom;
    }
}

@font-palette-values --lilac-blossom {
    font-family: "Bradley Initials DJR Web";
    base-palette: 7;
    override-colors: 0 #fff, 1 #F3B0EB;
}

Dark Mode

Not all color palettes are clearly visible on all backgrounds. Without color fonts, the color used to render text was entirely determined by the CSS author, but now that fonts can have color palettes defined inside them, it’s up to CSS authors to pick or create a color palette that is legible on the background it’s being rendered on. This can be particularly tricky when font fallback occurs, or when the user has blocked some fonts from loading.

Fonts such as Bradley Initials DJR Web have an extra tool for helping with this, though. Fonts can indicate that certain palettes inside them are usable with light backgrounds or usable with dark backgrounds, and these palettes are hooked up to the font-palette property. You don’t even have to use @font-palette-values!

So, if you want to use a color palette on a dark background, you can simply say font-palette: dark, like this:

ABCDEFG

And the same thing for a light background: font-palette: light:

ABCDEFG

Because the font-palette property has no effect on non-color fonts, it’s safe to set it in conjunction with the prefers-color-scheme media query, like this:

@media (prefers-color-scheme: dark) {
    :root {
        background: black;
        color: white;
        font-palette: dark;
    }
}

Fallback

Because @font-palette-values blocks are scoped to a specific font, you can make multiple of them that share a name. This is really powerful – it means you can define a single color palette name, and have it applied differently to whatever font happens to be rendered with it. Here’s an example:

@font-palette-values --lilac-blossom {
    font-family: "Bradley Initials DJR Web";
    base-palette: 1;
}

@font-palette-values --lilac-blossom {
    font-family: "Megabase";
    base-palette: 2;
}

<div style="font-palette: --lilac-blossom;">
    <div style="font-family: 'Bradley Initials DJR Web';">Pizza is amazing!</div>
    <div style="font-family: 'Megabase Web';">Is there any food better than pizza?</div>
</div>

This will have Bradley Initials DJR Web’s Lilac Blossom palette applied to Bradley Initials DJR Web, and Megabase’s Lilac Blossom palette applied to Megabase. And you only had to specify the font-palette property once!

Contextual color

In addition to pulling colors from palettes, some color fonts set aside special shapes that are connected to the foreground color of the current element. This makes them extra flexible, but it also means that these shapes operate independently from font-palette. In these cases, you can simply use the color property to change their color, like in this demo using Megabase.

<div style="font-family: 'Megabase Web';">
    <div style="color: black;">They were just pushed into space.</div>
    <div style="color: blue;">As much as I care about you.</div>
</div>

They were just pushed into space.

As much as I care about you.

Conclusion

Of course, with power comes responsibility; just because you can change colors, doesn’t mean you always should. Often the colors in a palette are coordinated to harmonize aesthetically, so it’s good to have a sense of how they are meant to relate to one another. You can look at the font’s predefined color palettes to see how the font designer assigned the roles for each color in the palette, and tweak accordingly.

It is also important to choose colors that contrast strongly against the background in order to keep your text readable and your webpage accessible. Colors in the palette that are used to form the base of the letters should typically “pop” against the background, while supporting layers like shadows, outlines, and decorative elements might contrast less in order to keep them from overpowering the letterforms.

Color fonts are a great improvement over graphic images, because they work by default with screen readers, copy/paste, and find-in-page. Also, they gracefully show fallback text if the font somehow fails to load, and they reflow if the browser window resizes. Not only that, color fonts are more flexible than graphic images, because they can incorporate the foreground color of the element using them into the design of the font.

Color fonts are not meant to take the place of single-color fonts. But used in moderation, at a big enough size in the right circumstance, they can be the perfect icing on the cake!

]]>
The Focus-Indicated Pseudo-class :focus-visible https://webkit.org/blog/12179/the-focus-indicated-pseudo-class-focus-visible/ Mon, 31 Jan 2022 17:00:42 +0000 https://webkit.org/?p=12179 The “focus indicator”, as its name suggests, visually indicates (often with a kind of an outline) that an element has focus. That sounds simple enough, and it perfectly describes what the old :focus selector makes possible. However, for decades now, whether a browser will actually display a focus indicator natively has been a considerably more complex affair.

Focus indicator on different elements in Safari
Focus indicator on different elements in Safari

Based on lots of feedback and study, browsers have long employed heuristics about both the type of element, and how it came to gain focus, in order to determine whether the focus indicator should be displayed. If the user is navigating the page with the keyboard, it should. In other situations, it depends. A text input, for example, will display an indicator regardless of how it received focus. This is important because all users need to know where their input data will be placed. Interfaces that do not employ those heuristics feel unnatural.

The goal of the old :focus selector was to allow authors to better style the focus indicator to be in tune with their overall design choices. The trouble is that using it meant losing the heuristics. The net result, unfortunately, has been that the most common use of the :focus selector has been to remove indicators altogether. This avoids the “false positive” focus styles that cause complaints from many users. The problem is that removing focus styling breaks website accessibility, causing trouble for people navigating the page using the keyboard.

Fortunately, a new CSS selector comes to the rescue, avoiding this kind of accessibility issue while providing the behavior web developers were looking for. The :focus-visible pseudo-class matches elements based on the browsers heuristics. It allows web authors to style the focus indicator only if it would be drawn natively.

Despite being a new feature that has recently landed on the web platform, it’s already being used by almost 1% of web pages (according to the Web Almanac by HTTP Archive).

Implementation

As of Safari Technology Preview 138, the :focus-visible selector has been added to WebKit, paying special attention to interoperability with other implementations. As part of the WebKit implementation, the Web Platform Tests test suite has been improved and expanded quite a lot, adding coverage to new cases and ensuring a better interop between the different implementations.

This work led to changes and improvements all around. Thanks to tests and discussions, all browsers now follow a common set of heuristics and have the same behavior in most situations. In WebKit, for example, clicking on a <div tabindex="0"> will no longer show a focus indicator by default (matching other browser engines).

In addition, the default User Agent style sheet in all browsers now uses the :focus-visible pseudo-class. This is a nice thing to have because it avoids the type of issues that happened with :focus in the first place, and circumvents the need of using some weird workarounds, like :focus:not(:focus-visible), to style the default focus indicator painted by the browser.

If you’re curious about the implementation details, you can read a series of blog posts written along the path to development at blogs.igalia.com/mrego, or watch this talk.

Examples

The good news after all the changes that have happened around :focus-visible lately, is that now you’d just need to use :focus-visible selector to style the focus indicator drawn by the browser engines.

On top of that, browsers are no longer showing a focus indicator when the user isn’t expecting it, like focusing a regular element via mouse click.

Web authors won’t need any kind of workaround to achieve the goal of styling the focus indicator shown by the web engine.

For example, if you want your focus indicator to have a thick magenta outline with a small offset, you just need to use the following CSS on your website:

:focus-visible {
    outline: solid thick magenta;
    outline-offset: 0.1em;
}
Customized focus indicator outline using :focus-visible

And you can use other properties, not only the outline related ones, to customize the focus indicator in your website. E.g.:

:focus-visible {
    outline: dotted thick green;
    background: lime;
    box-shadow: 0.3em 0.3em lightgrey;
}
Customized focus indicator changing outline, background and box-shadow with :focus-visible

Open Prioritization

It’s worth mentioning that this work was collectively chosen from among many, in numerous engines and partially funded by the public, through Igalia’s Open Prioritization campaign. This effort works to democratize the development of web platform features, giving groups of people and smaller organizations the chance to have a direct impact on the web ecosystem. If you want to learn more about this, Eric Meyer gave a talk explaining it at the W3C’s TPAC.

Thanks to many contributions, this experiment yielded not only :focus-visible development in WebKit, but a lot of interoperability work and alignment in other implementations and a sense that there was real demand for the feature. Thank you to everyone involved.

Feedback

:focus-visible pseudo-class has been enabled by default in Safari Technology Preview 138, please try it out and report any issues you might found on bugs.webkit.org. You can also send a tweet to @regocas or @webkit to share your thoughts about this feature.

]]>
Wide Gamut 2D Graphics using HTML Canvas https://webkit.org/blog/12058/wide-gamut-2d-graphics-using-html-canvas/ Tue, 14 Dec 2021 17:00:35 +0000 https://webkit.org/?p=12058 @media (prefers-color-scheme:dark) { figure .preserve-color, figure:hover .preserve-color { filter: none !important; } } figure.widescreen.inline-images img { display: inline; } article .byline { width: 210px; margin-left: -20px; } .nowrap-overflow-auto { white-space: nowrap; max-width: 100%; overflow: auto; } figure img { display: inline !important; } @media (max-width: 1180px) { article .byline { width: unset; margin-right: auto; } } #fillstyles img { background-color: #ddd; } @media (prefers-color-scheme: dark) { #fillstyles img { background-color: #444; } } #puzzle iframe { width: 350px; height: 250px; } @media (min-width: 500px) { #puzzle iframe { width: 500px; height: 310px; } } @media (min-width: 1000px) { #puzzle iframe { width: 1000px; height: 520px; } } @media (color-gamut: p3) { #gamut-warning { display: none; } }

Most colors used on the Web today are sRGB colors. These are the colors that you specify with the familiar #rrggbb and rgb(r, g, b) CSS syntax, and whose individual color components are given as values in the range [0, 255]. For example, rgb(255, 0, 0) is the most saturated, pure red in the sRGB color space. But the range of colors in sRGB — its color gamut — does not encompass all colors that can be perceived by the human visual system, and there are displays that can produce a broader range of colors.

sRGB is based on the color capabilities of computer monitors that existed at the time of its standardization, in the late 1990s. Since then, other, wider gamut color spaces have been defined for use in digital content, and which cover more of the colors that humans can perceive. One such color space is Display P3, which contains colors with significantly higher saturation than sRGB.

This browser reports that the display does not support Display P3 colors; figures in this post may not appear as intended.
A conic gradient showing a range of sRGB colors A conic gradient showing a range of Display P3 colors
Conic gradients showing fully saturated sRGB (left) and Display P3 (right) colors. Viewed in a browser and on a display supporting Display P3, the colors in the circle on the right will show as more intense than those on the left. (View as standalone page.)
For a more in depth introduction to color spaces, see Dean Jackson’s earlier post, Improving Color on the Web.

Today, there are many computer and mobile devices on the market with displays that can reproduce all the colors of the Display P3 gamut, and the Web platform has been evolving over the last few years to allow authors to make best use of these displays. WebKit has supported wide color images and video since 2016, and last year became the first browser engine to implement the new color syntax defined in CSS Color Module Level 4 where colors can be specified in a given color space (like color(display-p3 1 0 0), a fully saturated Display P3 red).

One notable omission in wide gamut color support, until now, has been in the HTML canvas element. The 2D canvas API was introduced before wide gamut displays were common, and until now has only handled drawing and manipulating sRGB pixel values. Earlier this year, a proposal for creating canvas contexts using other color spaces was added to the HTML standard, and we’ve recently added support for this to WebKit.

Drawing on a wide gamut canvas rendering context

The getContext method on a canvas element, which is used to create a rendering context object with 2D drawing APIs, accepts a new option to set the canvas backing store’s color space.

<canvas id="canvas" width="400" height="300"></canvas>
<script>
let canvas = document.getElementById("canvas");
let context = canvas.getContext("2d", { colorSpace: "display-p3" });
// ... draw on context ...
</script>

The default color space remains sRGB, rather than having the browser automatically use the wider color space, to avoid the performance overhead of color space conversions with existing content. The two explicit color spaces that can be requested are "srgb" and "display-p3".

Fill and stroke styles can be specified using any supported CSS color syntax.

let position = 0;
for (let green of [1, 0]) {
    for (let blue of [1, 0]) {
        for (let red of [1, 0]) {
            context.fillStyle = `color(display-p3 ${red} ${green} ${blue})`;
            context.fillRect(position, position, 40, 40);
            position += 20;
        }
    }
}
Colored squares that have been clamped to sRGB Colored squares using Display P3 colors that are outside the sRGB gamut
Display P3 colors used as fill styles on an sRGB (left) and Display P3 (right) canvas. Colors on the left are clamped to remain within the sRGB gamut. (View as standalone page.)

Any drawing that uses a color outside the color space of the canvas will be clamped so that it is in gamut. For example, filling a rectangle with color(display-p3 1 0 0) on an sRGB canvas will end up using a fully saturated sRGB red. Similarly, drawing on a Display P3 canvas with color(rec2020 0.9 0 0.9), an almost full magenta in the Rec.2020 color space, will result in pixels of approximately color(display-p3 1.0 0 0.923) being used, since that is the closest in the Display P3 color gamut.

const COLORS = ["#0f0", "color(display-p3 0 1 0)"];
for (let y = 20; y < 180; y += 20) {
    context.fillStyle = COLORS[(y / 20) % 2];
    context.fillRect(20, y, 160, 20);
}
A filled square of full sRGB green Stripes of full sRGB green and full Display P3 green
Stripes of interleaved Display P3 and sRGB colors on an sRGB (left) and Display P3 (right) canvas. Because colors are clamped to remain within the gamut of the canvas, the two shades of green are indistinguishable on the sRGB canvas. (View as standalone page.)
On macOS, you can use the ColorSync Utility to convert color values between sRGB, Display P3, Rec.2020, and some other predefined color spaces.

Wide gamut colors are usable in all canvas drawing primitives:

  • as the fill and stroke of rectangles, paths, and text
  • in gradient stops
  • as a shadow color

Pixel manipulation in sRGB and Display P3

getImageData and putImageData can be used to get and set pixel values on a wide gamut canvas. By default, getImageData will return an ImageData object with pixel values in the color space of the canvas, but it is possible to specify an explicit color space that does not match the canvas, and a conversion will be performed.

let context = canvas.getContext("2d", { colorSpace: "display-p3" });
context.fillStyle = "color(display-p3 0.5 0 0)";
context.fillRect(0, 0, 100, 100);

let imageData;

// Get ImageData in the canvas color space (Display P3).
imageData = context.getImageData(0, 0, 1, 1);
console.log(imageData.colorSpace);  // "display-p3"
console.log([...imageData.data]);   // [128, 0, 0, 255]

// Get ImageData in Display P3 explicitly.
imageData = context.getImageData(0, 0, 1, 1, { colorSpace: "display-p3" });
console.log(imageData.colorSpace);  // "display-p3"
console.log([...imageData.data]);   // [128, 0, 0, 255]

// Get ImageData converted to sRGB.
imageData = context.getImageData(0, 0, 1, 1, { colorSpace: "srgb" });
console.log(imageData.colorSpace);  // "srgb"
console.log([...imageData.data]);   // [141, 0, 0, 255]

The ImageData constructor similarly takes an optional options object with a colorSpace key.

let context = canvas.getContext("2d", { colorSpace: "display-p3" });

// Create and fill an ImageData with full Display P3 yellow.
let imageData = new ImageData(10, 10, { colorSpace: "display-p3" });
for (let i = 0; i < 10 * 10 * 4; ++i)
    imageData.data[i] = [255, 255, 0, 255][i % 4];

context.putImageData(imageData, 0, 0);

As when drawing shapes using colors of a different color space, any mismatch between the ImageData and the target canvas color space will cause putImageData to perform a conversion and potentially clamp the resulting pixels.

Serializing canvas content

The toDataURL and toBlob methods on a canvas DOM element produce a raster image with the canvas contents. In WebKit, these methods now embed an appropriate color profile in the generated PNG or JPEG when called on a Display P3 canvas, ensuring that the full range of color is preserved.

Drawing wide gamut images

Like putImageData, the drawImage method will perform any color space conversion needed when drawing an image whose color space differs from that of the canvas. Any color profile used by a raster image referenced by an img, and any color space information in a video referenced by a video (be it a video file or a WebRTC stream), will be honored when drawn to a canvas. This ensures that when drawing into a canvas whose color space matches the display’s (be that Display P3 or sRGB), the source image/video and the canvas pixels will look the same.

Here is an interactive demonstration of using canvas to make a sliding tile puzzle. The tiles are drawn by applying a clip path and calling drawImage pointing to the img element on the left, which references a wide gamut JPEG. Toggling the checkbox shows how the colors are muted when an sRGB canvas is used.

Sliding tile puzzle. Toggling the checkbox will change whether an sRGB or a Display P3 canvas is used. (View as standalone page.)

Web Inspector support

Web Inspector also now shows color space information for canvases to help ensure your canvases’ backing stores are in the expected color space.

In the Graphics tab, the Canvases Overview will display the color space for each canvas next to the context type (e.g. 2D) on each canvas overview tile.

After clicking on a Canvas overview tile to inspect it, the color space is shown in the Details Sidebar in the Attributes section.

Browser support

Wide gamut canvas is supported in the macOS and iOS ports of WebKit as of r283541, and is available in Safari on:

  • macOS Monterey 12.1 and above
  • iOS 15.1 and above

Safari is the first browser to support drawing shapes, text, gradients, and shadows with wide gamut CSS colors on Display P3 canvases. All other features, including getImageData, putImageData, and drawImage on Display P3 canvases, are supported in Safari and in Chrome 94 and above.

Feature detection

There are a few techniques you can use to detect whether wide gamut display and canvas support is available.

Display support: To check whether the display supports Display P3 colors, use the color-gamut media query.

function displaySupportsP3Color() {
    return matchMedia("(color-gamut: p3)").matches;
}

Canvas color space support: To check whether the browser supports wide gamut canvases, try creating one and checking the resulting color space.

function canvasSupportsDisplayP3() {
    let canvas = document.createElement("canvas");
    try {
        // Safari throws a TypeError if the colorSpace option is supported, but
        // the system requirements (minimum macOS or iOS version) for Display P3
        // support are not met.
        let context = canvas.getContext("2d", { colorSpace: "display-p3" });
        return context.getContextAttributes().colorSpace == "display-p3";
    } catch {
    }
    return false;
}

CSS Color Module Level 4 syntax support: To check whether the browser supports specifying wide gamut colors on canvas, try setting one and checking it wasn’t ignored.

function canvasSupportsWideGamutCSSColors() {
    let context = document.createElement("canvas").getContext("2d");
    let initialFillStyle = context.fillStyle;
    context.fillStyle = "color(display-p3 0 1 0)";
    return context.fillStyle != initialFillStyle;
}

Future work

There are a few areas where wide gamut canvas support could be improved.

  • 2D canvas still exposes image data as 8 bit RGBA values through ImageData objects. It may be useful to support other pixel formats for a greater color depth, such as 16 bit integers, or single precision or half precision floating point values, especially when wider color gamuts are used, since increased precision can help avoid banding artifacts. This has been proposed in an HTML Standard issue.
  • The two predefined color spaces that are supported are sRGB and Display P3, but as High Dynamic Range videos and displays that support HDR become more common, it’s worth consdering allowing 2D canvas to use these and other color spaces too. See this presentation at the W3C Workshop on Wide Color Gamut and High Dynamic Range for the Web from earlier this year, which talks about proposed new color space and HDR support.
  • Canvas can be used with context types other than 2D, such as WebGL and WebGPU. A proposal for wide gamut and HDR support in these contexts was presented at that same workshop.

In summary

WebKit now has support for creating 2D canvas contexts using the Display P3 color space, allowing authors to make best use of the displays that are becoming increasingly common. This feature is enabled in Safari on macOS Monterey 12.1 and iOS 15.1.

If you have any comments or questions about the feature, please feel free to send me a message at @heycam, and more general comments can be sent to the @webkit Twitter account.

Further reading

]]>
CSS Individual Transform Properties https://webkit.org/blog/11420/css-individual-transform-properties/ Mon, 14 Dec 2020 18:00:21 +0000 https://webkit.org/?p=11420 CSS Transforms appeared on the Web along with CSS Animations and CSS Transitions to add visual effects and motion on the Web. Those technologies have been a staple of the Web platform and Web developers’ toolkit for well over a decade. In fact, the CSS transform property first shipped in Safari all the way back in July 2008 when iPhone OS 2.0 shipped. You can find some historical posts about initial support in WebKit from October 2007, and another post from July 2009 focusing on 3D transforms when CSS Transforms shipped in Mac OS X Leopard.

And now, there is some news in the world of CSS Transforms: individual transform properties are enabled by default in Safari Technology Preview 117. This means that, as in Firefox and Chrome Canary, you can now use the new translate, rotate and scale CSS properties to specify what have so far been functions of the transform property, including 3D operations.

Using these properties is simple and should make Web developers feel right at home. Consider these two equivalent examples:

div.transform-property {
    transform: translate(100px, 100px) rotate(180deg) scale(2);
}

div.individual-properties {
    translate: 100px 100px;
    rotate: 180deg;
    scale: 2;
}

But why would you use these new properties over the transform property? One reason is convenience, as you might deem it simpler to write scale: 2 rather than transform: scale(2) when all you intend to do is scale an element.

But I think the main draw here is that you are now free to compose those various transform properties any way you see fit. For instance, you can easily write a CSS class to flip an element using the scale property without worrying that you might override other transform-related properties:

.flipped {
    scale: -1;
}

Your flipped class will work just fine even if a rotate or transform property applies a rotation to the element.

This feature also comes in handy when animating transforms. Let’s say you’re writing an animation that scales an element up over its entire duration but also applies a rotation for the second half of that animation. With the transform, property you would have had to pre-compute what the intermediate values for the scale should have been when the rotation would start and end:

@keyframes scale-and-rotate {
    0%   { transform: scale(1) }
    50%  { transform: scale(1.5) rotate(0deg) }
    100% { transform: scale(2) rotate(180deg) }
}

While this may not look like such a big deal when you look at it, making any further changes to those keyframes would require recomputing those values. Now, consider this same animation written with the individual transform properties:

@keyframes scale-and-rotate {
    0%   { scale: 0 }
    50%  { rotate: 0deg } 
    100% { scale: 1; rotate: 180deg; }
} 

You can easily change the keyframes and add other properties as you like, leaving the browser to work out how to correctly apply those individual transform properties.

But that’s not all; there is also the case where you want separate animations to apply to an element at the same time. You could split out this single set of keyframes into two different sets and tweak the timing instead:

.animated {
    /* Apply the scale keyframes for 1s and the rotate
       keyframes for 500ms with a 500ms delay. */
    animation: scale 1s, rotate 500ms 500ms;
}

@keyframes scale {
    from { scale: 0 }
    to   { scale: 1 }
}

@keyframes rotate {
    from { rotate: 0deg }
    to   { rotate: 180deg }
}

Now keyframes applying to transforms are not only easier to author, but you can better separate the timing and the keyframes by composing multiple transform animations. And if you are a seasoned CSS Animations developer, you’ll know how important this can be when you factor in timing functions.

Additionally, animating the new individual transform properties retains the same great performance as animating the transform property since these properties support hardware acceleration.

But what about the transform property? How does it relate to those new individual transform properties?

First, remember that the transform property supports transform functions that are not represented as individual transform properties. There are no equivalent CSS properties for the skew(), skewX() and skewY() functions and no property equivalent to the matrix() function.

But what happens when you specify some of the individual transform properties as well as the transform property? The CSS Transform Level 2 specification explains how individual transform properties and the transform-origin and transform properties are composed to form the current transformation matrix. To summarize, first the individual transform properties are applied – translate, rotate, and then scale – and then the functions in the transform property are applied.

This means that there’s a clear model to use those individual transform properties and the transform property together to enhance your ability to transform content on the Web platform.

And before you start using these new properties, it is important that you know how to detect their availability and use transform as a fallback. Here, the @supports rule will allow you to do what you need:

@supports (translate: 0) {
    /* Individual transform properties are supported */
    div {
        translate: 100px 100px;
    }
}

@supports not (translate: 0) {
    /* Individual transform properties are NOT supported */
    div {
        transform: translate(100px, 100px);
    }
}

We encourage you to start exploring how to use those three new properties in Safari Technology Preview in your projects and file bug reports on bugs.webkit.org should you encounter unexpected issues. You can also send a tweet to @webkit or @jonathandavis to share your thoughts on individual transform properties.

]]>