Improving CSS Architecture with Cascade Layers, Container Queries, Scope
Presenter: Miriam Suzanne
Duration: 6 min
CSS is intentionally contextual, adapting to a range of user needs and interfaces – and the cascade makes that possible. But as the Web becomes more powerful, it’s clear that design engineers need more tools to organize and maintain larger code-bases across larger teams.
- Cascade Layers will allow authors to manage their own internal cascade logic, without relying entirely on the specificity heuristic or source-order.
- Container Queries will make it possible for individual elements to adapt based on more localized context, rather than relying on overall viewport dimensions.
- Scope (still in the early design stage) will allow better targeting of selectors within a particular segment of the DOM, to help avoid naming conflicts across a larger project, or accidental overlap when nesting elements.
Slides & Video
I want to quickly show you three CSS features that we’re working on, to help teams manage the overall architecture of their CSS.
First, we wanted to address some of the pain points around selectors and specificity.
Specificity is based on the assumption that generic selectors are also lower priority, and more narrowly targeted selectors are higher priority.
This is a rough approximation of the layers in our code – But it’s not perfect.
And authors don’t have much direct control.
The act of layering is intertwined with the need to select elements.
Cascade Layers are designed to give authors more direct control, to describe our own custom layers of the cascade.
We can define a layer, give it a name, and add styles to it using either the at-layer rule.
Or by adding a layer function to imports, or both.
Layers stack in the order they were first defined, with un-layered styles at the bottom, and the last layer at the top.
Selectors inside the components layer here, will take priority over all the other layers.
But we don’t need to keep all our layered styles in that same order.
The layer priority is based on when a layer name first appears.
So we can even establish our desired layer order up-front, by using the layer rule without any styles – just a layer name or list of names.
Once they’re established, we can add to those layers in any order, and our code will slot into place.
We can also nest layers as needed – and reference nested layers directly with a dot notation.
Or create anonymous layers that are maintained in a single location.
This spec is currently a Working Draft, and has become fairly stable.
Both Firefox and Chrome already have prototypes available behind flags, and there’s also been significant progress on the Webkit issue.
The next feature is also about how selectors work.
With “scope”, we’re trying to address two closely related issues that come up regularly, and drive people to use tools & conventions like BEM syntax or CSS-in-JS.
The first goal is to avoid naming conflicts as our projects grow.
Which we can solve by focusing on our second goal: expressing “membership” or “ownership” in our selectors.
While nested selectors might seem like a way to express membership – in this case a title that is inside a post.
That’s not quite the same thing as a post-title.
The first one only describes a nested structure, but the second describes a more clear membership in a component pattern.
Not all the titles in a post, just the title that belongs to the post.
Another way to think about this is to say that some components have lower boundaries.
The component itself is a “donut” with a slot in the middle for content.
We should be able to style these donut components, without worrying that we might accidentally style everything inside them by mistake.
There are some similarities between scope and shadow-DOM encapsulation.
But Shadow boundaries are defined in the DOM, so that each element has a single scope, and styles are strongly isolated from getting in or out.
They're never allowed to overlap at all.
Where scopes are defined entirely in CSS – more fluid, able to overlap, and integrate more smoothly with global design systems.
The current proposal uses an at-scope rule, which accepts both a scope-root selector (in this case media) and a lower-boundary selector (in this case content).
Any selectors inside the at-rule only match elements between a matched root element and any lower-boundary descendants.
We’re also considering how scope proximity might become part of the cascade.
When two styles have the same specificity we could give priority to the “closer” scope-root before falling back to source-order.
There has also been talk about adding some form of lower-boundary or donut selector syntax, to write more targeted selectors without the at-rule or proximity weighting.
This is still in very early design discussions, but we’re happy to get any feedback.
And that brings us to Container Queries – one of the most requested features over the last decade.
While media queries let us respond to size of the viewport If we have those same elements in different size containers, we can see that the viewport doesn't give us the information we want So we want some way to reference the container itself and allow each of these cards to layout differently based on the container it's inside of.
But this type of query could lead to an infinite layout loop.
So the first thing we need to do is define our containers – any element the we want to query – and turn off content-based sizing on those elements.
We can do something similar with the contain property, but the current options are a little too heavy-handed.
We usually want to contain only the width of an element, or the inline-dimension, and allow the height or block dimension to grow and shrink with the content.
This isn’t entirely solved, yet, but we do have a prototype, and we’re confident that there’s a path forward.
But authors wont set that containment directly.
Instead, we’ll define the type of container we want.
What dimensions we need to query – And then any element can query the container it is in – its nearest ancestor that’s been defined as a container.
This container-query looks exactly like a media-query, but with at-container instead of at-media.
And if you don’t want to rely on the nearest container, you can also give containers names, and only query containers with a specific name.
Or only query a specific container type.
Chrome Canary already has a prototype, and you can start playing with it behind a feature flag.
Max Böck has created this bookstore demo with self-contained web components.
Each host element is a container, and everything inside the component adjusts based on available size.
We’re also working on container-relative units, similar to vw, vh, vmin, vmax, but a percentage of the container size rather than the viewport.
These are also supported in the Chrome Canary prototype.
Here’s a demo from Scott Kellum showing query units in action.
We’re also working on queries that aren’t about the container size.
The exact syntax is not established yet, but we might be able to query the actual value of a property.
Or the current state of a container.
Is it position-sticky, and currently in a “stuck” state?
This is an editor’s draft, just waiting on the details of single-axis containment before we move it to First Public Working Draft, and start looking for more implementations.
All of these features are designed to work together, building on the existing features of CSS.
we’re excited to see what people build with these new tools, and always open to your feedback.