Every Layout’s Sidebar exemplifies a quantum CSS layout, perhaps better than any other layout we offer.
It is neither a two column layout, comprising elements with fixed and fluid widths, nor a single column layout comprising two vertically stacked elements. It’s both, and neither. It adapts, automatically, to context — with no @media
or @container
queries required.
The Sidebar is versatile too. It can set out the vertical navigation and main content of your page, and it can form intrinsically responsive media blocks, wherein the sidebar is an image.
But, even as I’m writing about it now, the name Sidebar bugs me. The layout, as specified, is not actually a sidebar. A sidebar is just a (relatively) narrow element. The Sidebar is a layout that includes a sidebar. Necessarily, there are three elements in total: a container, a designated sidebar element, and an accompanying element to take up the remaining space.
Consequently, the code uses the term with-sidebar
to define the container. Grammatically awkward.
- Code language
- css
.with-sidebar { display: flex; flex-wrap: wrap; gap: var(--sidebar-gap, 1rem); }
What if this class wasn’t needed? Ideally, I’d like to place a class="sidebar"
element anywhere on the page and the supporting meta-layout would assemble itself from the elements around it. This is now possible using the :has()
pseudo-class:
- Code language
- css
:has(> .sidebar) { display: flex; flex-wrap: wrap; gap: 1rem; }
Targeting the non-sidebar element is also eminently possible, using a combination of :has()
and :not()
. The complete code for the reusable layout would look something like this:
- Code language
- css
:has(> .sidebar) { display: flex; flex-wrap: wrap; gap: var(--sidebar-gap, 1rem); } .sidebar { /* ↓ The width when the sidebar *is* a sidebar */ flex-basis: var(--sidebar-size, 20rem); flex-grow: 1; } :has(> .sidebar) > :not(.sidebar) { /* ↓ Grow from nothing */ flex-basis: 0; flex-grow: 999; /* ↓ Wrap when the elements are of equal width */ min-inline-size: var(--sidebar-wrap-at, 50%); }
Demo
Note the custom properties with default values. You can still override these defaults (as I described in Dynamic CSS Components Without JavaScript) at the container level:
- Code language
- html
<div style="--sidebar-gap: 3rem"> <nav class="sidebar">...</nav> <main>...</main> </div>
Using style
with custom properties can get a little verbose, but it acts like a component prop, enabling parametric layout. By instantiating generic layouts, like Sidebar, with configuration, you can significantly reduce your CSS.
Layout testingpermalink
Despite doing away with some attribution, the Sidebar layout still requires a strict markup structure. We can test and report on incorrect structure directly in CSS, as I wrote in Testing HTML With Modern CSS. In development, you could include a stylesheet containing the following test:
- Code language
- css
:root { --error-outline: 0.25rem solid red; } :has(> .sidebar) > :only-child, :has(> .sidebar) > :nth-child(3) { outline: var(--error-outline); --error: 'Sidebar layouts must include exactly two child elements.'; }
The error outline style, saved on the :root
for reuse across tests, provides a visual regression.
When inspecting the outlined element in dev tools, the error message is available in a custom property.
If you prefer, you can use :not()
and turn this into a one-liner:
- Code language
- css
:has(> .sidebar) > :not(:nth-child(1), :nth-child(2)) { outline: var(--error-outline); --error: 'Sidebar layouts must include exactly two child elements.'; }
This is not to say you’d necessarily want to preclude multiple sidebars. Both of the following work insomuch as wrapping occurs without unintended white space appearing, but it depends what kind of layout configurations you’re happy with, at different wrapping points.
- Code language
- html
<div> <nav class="sidebar">...</nav> <main>...</main> <aside class="sidebar">...</aside> </div> <div> <nav class="sidebar">...</nav> <aside class="sidebar">...</aside> <main></main> </div>
With two sidebars of equal size on the left, here’s how wrapping occurs with a 50%
threshold (--sidebar-wrap-at
):
It’s the third configuration that is perhaps least desirable, since the larger (main) content has become something of a sidebar itself. It’s in this case that you might want to intervene with a @container
query.
Now that @container
queries are available, the object is not to use them everywhere but to apply them only where needed. Normal Flexbox wrapping behavior should still do most of the work.
Hypothetically, let’s say the critical point is 400px
.
- Code language
- css
:has(> .sidebar) { container-type: inline-size; } @container (max-inline-size: 400px) { :has(> .sidebar) > .sidebar { inline-size: 100cqw; } }
Ideally, we’d be able to store the breakpoint value in a custom property and maintain parametric reusability. The ability to use custom properties in dimensional @container
queries has been agreed in principle, but it is not yet available. In the future, you should be able to support calibration like so:
- Code language
- css
:has(> .sidebar) { container-type: inline-size; --single-column-break: 400px; } @container (max-inline-size: var(--single-column-break) { :has(> .sidebar) > .sidebar { inline-size: 100cqw; /* full width, using container query units */ } }
You can, of course, nest Sidebars. Primitives like Sidebar and its companions are designed to be composed into more complex layouts.
- Code language
- css
<div style="--sidebar-size: 8rem"> <div class="sidebar"> <div style="--sidebar-size: 3.5rem"> <div class="sidebar"></div> <div></div> </div> </div> <div></div> </div>
This is more likely to result in expected and predictable wrapping behavior.
Selector performancepermalink
Selector performance is one of those things you only need to worry about very infrequently, like terrestrial shark attacks. If you are repeatedly and frequently causing style recalculations, over a massively overpopulated DOM — made up of thousands of nodes — it is something you may want to look into, after you’ve optimised literally everything else. To put it another way: it won’t be your CSS selectors, it will probably be a JavaScript memory leak and/or a bloated DOM.
However, an unconstrained use of the :has()
pseudo-class, as in :has(> .sidebar)
, is assigned a little turtle icon in the Firefox dev tools inspector.
The turtle icon, indicating slow, against the selector in Firefox dev tools
Given that :has()
is the manifestation of the fabled “parent selector” — deemed a technical impossibility for years, citing anticipated performance issues — this felt like something I needed to test.
In Chrome’s performance profiler, I enabled CSS selector stats, and recorded myself triggering a style recalculation. I inspected the recalculation event and went to the Selector stats tab. Indeed, :has(> .sidebar)
appears as the slowest selector at… 0.005ms
in elapsed time.
- Code language
- css
:has(> .sidebar) {} /* 0.005ms */ :has(> .sidebar) > :not(.sidebar) {} /* 0.002ms */
If I’m understanding correctly, this means it would take 1 / 0.005
or 200 concurrent sidebar layouts using this selector to risk a 1ms
bottleneck on style recalculation. That or, I suppose, re-rendering the same sidebar layout every 0.005ms
, 200 times over.
These metrics are deceptive anyway. As Trys Mudford wrote, in I wasted a day on CSS selector performance to make a website load 2ms faster, just enabling CSS selector performance stats slows style recalculation down considerably (in their case, from 11.95ms
to 270ms
total).
This too is something of a quantum effect, albeit not a welcome one. In any case, don’t let the spectre of selector performance deter you from harnessing modern and powerful CSS selector patterns. There’s much more important optimizations you could be focusing on, such as reducing client-side JavaScript, keeping your DOM light, and reducing the number of render-blocking elements.