Anchor Positioning Is Disruptive
New layouts will be possible
The more I play with it, the more convinced I am that anchor positioning is going to unlock some surprising new layouts.
The complexities of containment, overflow, and ‘propagation’
I spoke about Container Queries at both Smashing Conference (San Francisco) and CSS Day (Amsterdam) – where I recommended setting up a root container to replace most media queries. Since then, Temani Afif pointed out a few issues with that approach, and sent me down a rabbit hole of overlapping specs and browser bugs.
While there’s a workaround (below)
that will allow you to query the root element,
it’s not a perfect solution.
We’ve found it simpler to apply containment
on all the major layout blocks
directly inside the body
–
things like nav
, header
, main
, and footer
.
That ensures every nested component
will have a container to query,
without creating problems on root.
It also allows you to put some things
intentionally outside a container,
if you need them to use the viewport
as a positioning context
(with position: fixed
, for example).
We were told for years that container queries would be impossible to implement – and for good reason. CSS layout depends on a powerful and complicated balancing of ‘content’ and ‘context’:
Introducing ‘container queries’ directly in this system would cause a sort of Observer’s Paradox. We want to measure the context so that we can make changes to the content – but the context size is based on the content size, and so we end up changing the thing we are trying to measure.
The solution is
containment,
which allows us to break
the outward flow of content information,
breaking the loop.
In order for container size queries to work,
we have to contain
the size, style, and layout
of our containers.
We can do that by specifying
a container-type
of size
(to contain both width and height)
or inline-size
(to only contain the inline axis).
We can only query the dimension(s) we contain.
Both container types
also get layout
and style
containment.
But containment comes with a price, and can have some surprising side effects:
z-index
) stacking context,
as well as containing floated
and fixed-position elements.size
containment
will collapse the box
unless we provide an external block-size.See the Pen Containment Examples by @miriamsuzanne on CodePen.
While there’s no problem generally
with defining lots of containers on a page,
we need to be somewhat cautious about
the impacts of containment.
As a general rule,
we want to use inline-size
for most containers,
and only use size
for containers
that will be allowed to overflow.
When we allow overflow on elements,
we generally also give them a way to size
based on something other than their contents.
The difference between the ‘intrinsic’ (content) size
and the ‘extrinsic’ size
is what causes overflow in the first place.
Container size queries
are mostly intended to solve the problem
of nested containers.
If we have a .card
element
which might be in the main body of the page,
might be in a narrow sidebar,
and might be in a responsive grid –
measuring the ‘viewport’ with @media
doesn’t tell us much
about the space that’s available for each card.
With @container
,
we can have each .card
query
the space that it lives in.
But container queries provide
a few additional features
that are not possible with @media
,
and might be useful
even when we’re measuring the outermost container.
Most importantly:
em
-based media queries,
we don’t have access to the user settings
or the actual font size we establish
on the root element –
the query will always use the ‘browser default’
(usually 16px
) as the size of an em
.The side-effects of containment
also seem like they should be minimal on the root element.
The root (html
) element
seems like a great candidate for size
containment:
With container-type: size
on the root,
we could replace most or all
@media
size queries with @container
across our sites.
That feels right to me!
👍🏼 It should be.
👎🏼 But it isn’t.
🤷🏻♀️ Unless you’re careful?
Currently,
adding a container-type
of size
to the root (html
) element
will make scrolling impossible:
See the Pen HTML containment & overflow (2d) by @miriamsuzanne on CodePen.
And using inline-size
instead
will fix the scrollbar issue,
but now fixed elements
scroll off the page:
See the Pen HTML containment & overflow (1d) by @miriamsuzanne on CodePen.
Neither behavior will work as a default for most sites.
Various people in that thread pointed to ‘root/body propagation’ as the cause. And they’re right – but as far as I can tell, they shouldn’t be. I was involved with several conversations in the CSS Working Group around containment and propagation, and this is not what we decided. It’s also not defined to work like this in the specification.
As far as I can tell, this is a browser bug – implemented in all browsers – that needs to be fixed.
For us as web authors,
the root of a document
is the html
element.
But for browsers,
there’s more context to worry about
– more root than the root –
such as
the viewport,
the document canvas,
the initial containing block,
and the initial layout block.
I am not an expert on the complexities of these browser concepts – but roughly, together, they describe the context immediately surrounding our web pages in the browser: the ‘viewport’ through which we’re looking, the ‘canvas’ our site is painted onto, and the ‘blocks’ that defines our initial positioning and layout context. For simplicity, I’m going to refer to all of this collectively as The Viewport.
We don’t have direct access
to The Viewport.
There’s no CSS syntax we can use
to select and style any aspect of The Viewport.
And yet, we style them all the time
using the somewhat quirky and esoteric magic
of ‘propagation’ –
where styles on one element
(in this case html
or body
)
bubble outward (‘propagate’)
and apply to a parent element instead.
For example,
when we set a background
on html
,
that background
propagates to the document canvas
instead.
If we don’t set a background on html
,
but we do set a background on body
,
then the body
background
propagates instead –
skipping over the root,
and straight up to the canvas!
Background isn’t the only element
that propagates out
from the html
or body
element
to the viewport.
I haven’t found a full list anywhere –
this is defined spec-by-spec
for individual properties –
but the one causing issues for us is overflow
.
Overflow Viewport Propagation is defined in CSS Overflow Module Level 3:
UAs must apply the overflow-* values set on the root element to the viewport when the root element’s display value is not none.
Unless we hide the root element, overflow properties will always propagate. But not always from the root:
However, when the root element is an HTML
<html>
element (including XML syntax for HTML) whose overflow value is visible (in both axes), and that element has as a child a<body>
element whose display value is also not none, user agents must instead apply the overflow-* values of the first such child element to the viewport. The element from which the value is propagated must then have a used overflow value ofvisible
.
If both the html
and body
elements are un-hidden (the default),
and the html
element has the default overflow (visible
),
then we propagate the overflow
from body
instead of html
.
Once that propagation happens,
the browser ignores the actual overflow values
on both body
and html
–
using a value of visible
instead.
This is very similar to how background propagation works, except for one final twist:
If
visible
is applied to the viewport, it must be interpreted asauto
.
So, in the default case –
before we apply any outside CSS –
the visible
default of the root html
element is ignored,
the visible
default of the body
element
propagates up to the viewport,
which ignores the propagated value,
treating it as auto
instead.
By default, after all that, we get scrollbars when content overflows the viewport. Simple. 🥴
There’s a reason the CSS Working Group has resolved that:
RESOLVED: No future properties should propagate from
<body>
to the ICB
(‘ICB’ is the Initial Containing Block.)
RESOLVED: deprecate any existing use of body propagation
Body propagation was a mistake. We’re stuck with it now, but we don’t have to protect it moving forward.
CSS Containment Module Level 2 does provide some caveats around root/body propagation and containment:
When any containments are active on either the HTML
<html>
or<body>
elements, propagation of properties from the<body>
element to the initial containing block, the viewport, or the canvas background, is disabled. Notably, this affects:
writing-mode
,direction
, andtext-orientation
(see CSS Writing Modes 3 § 8 The Principal Writing Mode)overflow
and its longhands (see CSS Overflow 3 § 3.3 Overflow Viewport Propagation)background
and its longhands (see CSS Backgrounds 3 § 2.11.2 The Canvas Background and the HTML<body>
Element)
(Maybe this is the complete list?! If you know of a better list, or want to put one together, please tell us about it.)
And then we get a clarification:
NOTE: Propagation to the initial containing block, the viewport, or the canvas background, of properties set on the html element itself is unaffected.
The logic, as I understood it (and taught it at these recent conferences) goeth thusly:
I think that logic makes good sense, and my expectations match the text of the spec as I read it. But all the browsers implemented something else.
According to browser engineers in the CSSWG, my explanation here wasn’t quite right.
The actual issue is that
overflow propagates as defined in the spec,
but containment remains on the root element.
Since the overflowing content is contained,
it is not visible to the viewport
(where overflow is now set).
Meanwhile, the root element
(which can see the overflowing content)
no longer has a specified value of overflow
.
That is all proper according to the current specification. Any solution has to ensure that overflow and containment are applied to the same element – either the root or the viewport. Root is simpler, but doesn’t provide a number of scroll optimizations. On the other hand, it’s not clear what it would even mean for containment to propagate as well.
In the meantime, the solution below still works.
I am not a browser engineer, but I’ve been trying to parse out how browsers got a different answer than I did.
I’m not sure
if browsers are ‘wrong’
when the root element has default visible
overflow.
The spec says to propagate from the body,
but it doesn’t say what to do
if containment breaks that propagation.
I would expect the viewport
to still default to visible
overflow,
which is then treated as auto
.
But that’s not stated explicitly,
and the spec may need some improvements.
However, it does seem clear that a non-default overflow on root should propagate to the viewport with or without containment. It works fine without containment:
See the Pen Root overflow propagates fine by @miriamsuzanne on CodePen.
But when we add containment, our non-default overflow no longer propagates:
See the Pen Root overflow propagation plus containment by @miriamsuzanne on CodePen.
I filed an issue with the CSSWG.
Yes, with caveats.
There’s a solution that works right now.
Instead of setting
height and overflow on the html
element,
we use the body
as our top-level scroll-container.
Here’s the code:
html {
/* a size or inline-size container */
container-type: size;
}
html, body {
/* body and html both size to the viewport */
block-size: 100%;
}
body {
/* body is the root scroller */
/* this value doesn't propagate */
overflow: auto;
}
I made a codepen example that allows you to play with various combinations here, and see how each one behaves. With the combination above, the body is able to scroll, with fixed elements remaining in place:
See the Pen Testing a Root Container by @miriamsuzanne on CodePen.
This is an acceptable solution in many cases,
but it comes with a trade-off.
Browsers provide a range of optimizations
for the ‘root scroller’
which won’t be applied to the body
element here.
Your mileage may vary.
But I would make one more change to the code above. By adding names to containers, we can better control what is being queried. I like to give containers both ID-style (unique) names, along with class-like (shared) names:
html {
container: root layout / size;
}
Then we can explicitly query the root any time we want:
@container root (inline-size > 30em) { /* … */ }
Or we can query the nearest layout container:
@container layout (inline-size > 30em) { /* … */ }
Happy querying!
New layouts will be possible
The more I play with it, the more convinced I am that anchor positioning is going to unlock some surprising new layouts.
Performance, scope, and fallbacks for the anchor positioning polyfill
Our sponsors are supporting the continued development of the CSS Anchor Positioning Polyfill. Here’s a summary of the latest updates.
Are we measuring what we meant to measure?
There’s been a lot of interest in the results of the annual State of CSS survey, but are we asking all the right questions?