This becomes the mission statement of the web –
web for all, web on everything.
That includes assistive devices –
And non-visual media –
always with the end-user in control of the outcome.
Provide hints that the browser may or may not use.
–
Håkon Lie
We provide hints and suggestions,
semantic clues,
but only the browser
can put it all together.
The Cascade
And the cascade describes that process –
An ordered list (cascade) of style sheets …
can be referenced from the same document.
–
Håkon Lie
by accepting style sheets
from everyone involved –
The user/browser specifies initial preferences
and hands the remaining influence over to the document.
–
Håkon Lie
Browsers & users establish
global defaults and preferences across the web,
and then we fill in the details
of our particular site
Cascade Origins
🎨 Author (Document)
👥 User Preferences
🖥 User Agent (Browser)
These are the primary “cascade origins” –
each one representing a
different set of needs and concerns,
different perspectives,
sometimes in conflict.
The Cascade
Resolves Merge Conflicts
The rules of cascade & inheritance
describe how to merge all three,
and resolve any conflicts.
🎨 Author (Document)
👥 User Preferences
🖥 User Agent (Browser)
By default
user preferences override the browser defaults,
and (for better or worse)
we’re allowed to override everyone.
If conflicts arise the user should have the last word,
but one should also allow the author to attach style hints.
–
Håkon Lie
But when things really get heated,
when it really matters,
the user and browser can insist –
❗important
A balance of power
That some styles are
more important than others –
❗🖥 User Agent Important
❗👥 User Important
❗🎨 Author Important
🎨 Author Styles
👥 User Preferences
🖥 User Agent Defaults
Creating important origins
that cascade in reverse order:
Important author styles aren’t that special –
that’s us in the middle –
but users can override us when they need to,
and the browser finally decides
what’s out of bounds,
what’s possible on this device,
and what features are supported in what ways.
2. StyleSheets
Make Styles Reusable
The second goal of CSS
is to make our design objects reusable…
And selectors create another potential conflict
for the cascade to resolve.
Since we can use multiple different selectors
to target the same element –
Selector Specificity
The cascade needs to determine a winner,
using a clever heuristic called specificity –
based on how narrowly a selector has been targeted.
Again, each selector type
represents a different goal.
universal/type »
Global Defaults
The most generic selectors,
help us paint in broad strokes
to establish low-priority defaults –
attrs/classes »
Common Patterns
Classes and attributes
allow us to describe
higher-priority patterns,
and make up the majority of our styles –
ID/style »
Singular Overrides
Then one-off ID’s are both
the most narrowly targeted,
and the highest priority.
Unique#IDs
Reusable.classes & [attributes]
Elementtypes
Universal*
One ID will always override any number of attributes,
and on down the list.
It’s not perfect,
but it’s an approximation
of the layers in our code.
Hover,
We Have A 💥Problem💥
Until things get complicated…
Especially “At Scale”
As our projects become larger and more complex
with more distributed teams
and third-party integrations,
there are a lot of situations
that don’t fit the rule –
So that brings us to our first new feature:
Cascade Layers.
Jen Simmons and I suggested this at the end of 2019,
it was approved by the CSS Working Group last February,
and I expect browsers to start implementing it this year.
Originally…Custom Origins
Originally we called this feature
“custom origins”
Because we’re again creating layers
that represent different perspectives,
from different parts of a system,
and potentially different teams on a project.
Stacked in Layers
Components?
Themes?
Frameworks?
Resets?
But we get to define the layers ourselves as authors –
for things like resets, defaults,
frameworks, themes, components,
utilities –
anything we want,
in whatever order we need.
❗Important Resets
❗Important Themes
❗Important Components
Components
Themes
Resets
And the important flag works as intended,
when it becomes necessary for a lower layer
to insist on something,
and punch above it’s weight.
But we’re not actually adding new origins here,
so it may be better to think of them as
customizable layers of specificity.
@importurl(headings.css)layer(default);
We can define a layer,
give it a name,
and add styles to it
using either a layer function on the import rule –
– Or both. Here we’re creating a “default” layer
with the headings.css import,
and using the at-rule
to add a few more styles to the same
“default” layer.
Layers stack in the order they were first defined,
with the highest layer taking precedence,
no matter what specificity is used
for the selectors inside.
Specificity only matters inside each layer.
But we don’t need to keep all our styles in that order.
Once a layer has been established,
we can add to it from anywhere in our code.
The priority is based on when the layer name
first appears.
One of the goals here
is to make it so that we as authors
get to define exactly where
third-party tools belong
in our layering.
No matter what specificity those tools use internally,
or whatever layers they create,
we can always override them
without resorting to specificity hacks.
Either directly,
or by wrapping those layers into a contained namespace.
We can create or access
“nested” or “name-spaced” layers
using a dot-notation to combine the names.
@layer tools{@layer custom{/* tools.custom */}}
Or we can actually nest the layer rules.
Unlayered Styles Win
Of course,
we don’t have to put all our styles in a layer.
Un-layered styles will work the same way they always have,
and will always belong to an implied
“highest layer”
above all the others.
More Cascade Control
This gives us a lot more control
over our corner of the cascade,
so we’re not totally reliant
on selector specificity
and code-order to determine
what takes precedence.
Fewer Hacks
Hopefully allowing us to replace
all our specificity & importance hacks
with more clearly defined patterns.
The next feature is also about how selectors work.
With “scope”,
we’re trying to address two issues
that come up regularly,
and drive people to use tools & conventions
like BEM syntax or CSS-in-JS.
1. AvoidNaming Conflicts
(across large teams & projects)
The first goal is to avoid
naming conflicts as our projects grow.
2. By
Expressing Membership
(through lower boundaries & proximity)
Which we can solve
by focusing on our second goal:
expressing “membership” or “ownership”
in our selectors.
.title{/* global */}.post .title{/* nested */}
While nested selectors
might seem like a way to
express membership –
in this case
a title that is inside a post –
.title{/* global */}.post .title{/* nested */}.post__title{/* BEM */}
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.
.post__title{/* BEM */}.title[data-JKGHJ]{/* Vue */}
We don’t have a good way to convey that
using our current CSS selectors,
unless we invent a new unique name
for every kind of title,
based on what it belongs to –
either manually using a convention like BEM,
or automated with JavaScript compilers.
<h2class="title post__title">
And if we want some global title styles,
we end up using multiple classes –
and hoping the more targeted pattern will override
the global pattern.
Another way to think about this is
to say that some components
have lower boundaries –
the component itself is a “donut”
with a hole in the middle for content.
We should be able to style a tab component,
or a media-object,
without worrying that we might accidentally
style everything inside it by mistake.
Different from
Shadow-DOM Encapsulation
This might sound similar to shadow-DOM encapsulation,
and there is certainly cross-over
between scope & encapsulation.
But the Shadow-DOM is designed
for more highly-isolated widgets.
This creates a 1-to-1 relationship,
where boundaries are defined in the DOM,
each component has a single scope,
and styles are isolated from getting in or out.
Build-tools
Provide Scoped Styles
BEM, CSS Modules, Vue, JSX, Stylable, etc
While encapsulation can be useful,
it’s very different from the lighter-touch
“scope” that we get from existing
build-tools and conventions –
Where scopes reference the DOM,
but they are able to overlap,
and integrate more smoothly
with global design systems.
Different styles can be given
different or overlapping boundaries,
while global styles continue to apply globally.
This provides us with a much lower-impact alternative,
where scopes are defined in CSS,
and can be re-used across components,
or overlap & cascade together.
@scope(.media) to (.content){img{/* only images that are "in scope" */}}
So we’re proposing an at-scope rule,
that 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 apply between the root
and the lower-boundary.
In this case we’re styling images inside media,
unless they are also inside the media content.
We can also talk about this
in terms of proximity.
These two selectors apply to links
inside a light-theme or dark-theme class.
And that works great,
as long as we never nest one theme inside the other.
Since our selectors both have the same specificity,
and ancestor proximity is not part of the cascade –
dark-theme will always override light theme
in nested situations.
@scope([data-theme=light]) to ([data-theme]){a{color: purple;}}@scope([data-theme=dark]) to ([data-theme]){a{color: plum;}}
We can solve that problem using lower-boundaries,
so that themes never bleed into each other –
But I think it would also make sense for
scope proximity to be added as part of the scope feature.
When specificity is equal,
we would default to using the “closer” scope-root.
This part of the spec is still being debated.
There’s a lot more to the proposal,
which you can look into if your interested.
The CSSWG has expressed interest,
feedback is welcome,
and Chrome plans to prototype this soon,
for more testing.
And that brings us to the real reason we’re here.
Container Queries.
People have been asking for this feature
since Media Queries were implemented
more than 10 years ago.
Media Queries allow us
to change the layout of a component
when the viewport is above or below a particular size –
But if we put that same component
inside different-size containers…
That’s not what we want.
Ideally, each component
should be able to
respond to the container it’s in –
At first this seemed impossible,
but there’s been a lot of people over the years,
laying the groundwork to make it happen –
and last year two proposals emerged,
showing different ways we might pull this off.
Both are interesting,
but David Baron’s approach has the most momentum right now,
and I’ve been working on it
to flesh out some of the details,
and start writing a specification.
This proposal has two parts:
the containers, and the queries.
So the first thing we need to do
is define our containers.
And make this feature possible,
without creating infinite loops,
containers need to be… contained.
CSS layoutContext & Content
One of the coolest responsive features in CSS,
which we don’t talk about nearly enough,
is the way we calculate layout
based on both context and content.
Add more content,
and a container with try to grow,
but only as much as the context allows.
That’s very cool,
but if you add container queries,
it becomes an infinite loop.
1. Establishing Containers
.container{contain: size layout;}
So we have a way to turn that off,
using the contain property.
Size containment turns off content-based sizing,
so our containers need an explicit
or context-based size.
2D size containment
Is Too Restrictive
That would be real limiting
if we always had to contain both
height & width.
1D size containmentcontain: inline-size
But most layouts work by containing the width,
or the inline-dimension,
and allowing the height to grow or shrink
with content.
So we’re adding an option to make that explicit.
Contain inline-size.
Now we can write queries,
and they look exactly like media-queries,
but with at-container instead of at-media.
And each element will query
the size of it’s nearest ancestor container.
But why don’t I just show you?
I’ve set up two containers on the page,
each with one card using a media-query,
and one card using a container query.
The media queries all trigger at the same time,
but the container queries
depend on the size of the container.
In some cases,
like inside flexbox or grid,
there is no outside container
that will tell us the actual space available
for each item.
But we can get around that by adding a container
around each component –
in this case div.card is wrapping each article.
The outer div establishes a container,
and the inner article can query it.
Chrome already has a prototype,
and you can start playing with it
behind a feature flag.
I’ve started collecting codepen demos
to help you get started.
What Jen Simmons calls
“Intrinsic Web Design” –
not forcing everything to be an exact percentage
on a 12-column grid,
but allowing for different components
to manage their own intrinsic sizing.
Modular CSSResponsive Components
There’s already been a lot of progress in this space, with tools like grid & flexbox & aspect-ratios – now layers, scope, and container queries – but also color-functions, nesting and more.
Our medium is not done.
Our medium is still
going through radical changes.
–
Jen Simmons, Designing with Grid
“Our medium is not done.
Our medium is still
going through radical changes.”