Style queries for nested breakpoints

Previous post

Next post

initial-scale no longer needed Books aren't dead

About this site and me

Blog homepage RSS feed Mastodon Bluesky About me

This blog uses a new RSS feed. Please update the old QuirksBlog feed you used to follow.

Tag archives

Archives 3 CSS 6 Conferences 6 Monetisation 1 Personal 3 Safari 2 Site 2 Thidrekssaga 1 Web development 2

Monthly archives

September 2026 3 August 2026 2 May 2026 6 April 2026 9 2004-2021 blog

A few months ago I thought I had a brilliant style query idea that would help me rewrite the CSS for a few older sites in addition to dazzling everyone with my deep and abiding intelligence.

When I revisited my notes in the past week I found the technique has its uses as a polyfill but is not quite as brilliant as I thought. Still, it might give someone else a bright idea. Finally, I learned some valuable style and container query lessons that I’m going to inflict on you as well.

Centralised breakpoint definition

I’m going to have to update antique CSS files, and I want a way of centrally defining a few @media breakpoints. Ideally we’d do this with custom properties:

html {
	--smallSize: 240px;
	--mediumSmallSize: 480px;
	--mediumLargeSize: 960px;
	--largeSize: 1280px;
}

div.header {
	@media (width >= var(--smallSize)) {
		/* styles */
	}
}

div.layout {
	@media (width >= var(--mediumSmallSize)) {
		/* styles */
	}
}

div.footer {
	@media (width >= var(--largeSize)) {
		/* styles */
	}
}

Alas, custom properties don’t work in media queries — yet. So I turned to style queries to fill the gap.

The idea

This is the basic idea. Set a custom property with media queries that gives us a number for the current "width category" of the layout viewport.

html {
	--screenSize: 1;

	@media (width >= 20em) {
		--screenSize: 2;
	}

	@media (width >= 40em) {
		--screenSize: 3;
	}
	
	/* and so on */
}

While we’re at it we can also concentrate all other CSS support detection here. Say I want to use the column-filled grids I posted about earlier. It needs one support detect, so we could add that here as well.

html {
	--cfg: false;
	
	@supports (order: sibling-count()) and (order: calc(1cqw/1px)) {
		--cfg: true;
	}
}

Now I use these custom properties in style queries wherever I need them.

div.layout {
	@container style(2 <= --screenSize <= 4) {
		display: flex;
		flex-wrap: wrap;
		& > * {
			flex-grow: 1;
		}
	}
	
	@container style(var(--screenSize) >= 5) {
		display: grid;
		grid-template-columns: repeat(auto-fit,minmax(250px,1fr));
		@container style(--cfg: true) {
			grid-template-columns: 1fr;
			grid-auto-columns: 1fr;

			& > * {
				/* See here */
			}
		}
	}
}

As you can see I mix them with CSS nesting. I absolutely adore CSS nesting and want to use the principle as much as possible. So I want to place all styles for div.layout in one block, using nested selectors and media queries as appropriate.

As a test I nested selectors and style queries to about seven levels, and I encountered no difficulties. But then, I don’t think that browsers really nest these selectors and queries. They more like rewrite the selectors — or something. To Be Researched.

Browser compatibility

There are two possible syntaxes for the style queries:

/* perfect support */
@container style(--screenSize: 3) 

/* Safari 26 doesn't support these */
@container style(--screenSize = 3) 
@container style(--screenSize >= 3) 
@container style(var(--screenSize) >= 3)

The equal/greater/lesser syntax is not supported in Safari 26, but the 27 beta/Mac that I tested it in supports them totally fine. I’m assuming 27/iOS will also support them, but I haven’t tested it.

It will take another year or so for Safari 27 to be sufficiently widely spread. Still, custom properties in media queries aren’t supported anywhere yet. So this technique will be useful from Safari 27’s market maturity to that of custom properties in media queries.

If you only use colon syntax, like --screenSize: 2, you can use this technique today. But that’s not where it shines.

Custom properties are inherited

It’s probably worth stressing that the reason that this technique works is that custom properties are inherited, just like font or color. Thus, --screenSize will have the same value in every descendant of the HTML element.

<html>
	<body>
		<main>
			<section>
				<div class="layout"></div>
			</section>
		</main>
	</body>
</html>

html {
	--screenSize: 1;

	@media (width >= 20em) {
		--screenSize: 2;
	}
}

div.layout {
	/* --screenSize is still 1 or 2, the same as in the HTML element */
}

Like font or color, you can also redefine --screemSize somewhere in the inheritance chain.

html {
	--screenSize: 1;

	@media (width >= 20em) {
		--screenSize: 2;
	}
}

section {
	--screenSize: 10;
}

div.layout {
	/* Now --screenSize is 10 */
}

Numbers are better than names

I want to use numbers, not names. I want to be able to say "for screen size 3 and above." That’s how media queries work, and that’s how I want these style queries to work.

--screenSize: 1;

@media (width >= 20em) {
	--screenSize: 2;
}

@media (width >= 40em) {
	--screenSize: 3;
}

@container style(--screenSize >= 3)

Booleans are OK as well, if the question is a simple Yes/No one.

--cfg: false;

@supports (order: sibling-count()) and (order: calc(1cqw/1px)) {
	--cfg: true;
}

Essentially, use anything but strings. Strings can lead to cluttering (mediumLarge2­_smaller) and unclarity ("My brother in tech, that’s not medium-large, it’s medium-small.")

The parent as container

Style queries use @container, but we haven’t used any container-like property yet. Don’t we need container-type or something? Or aren’t style queries about containers at all?

To be honest, this was my biggest source of confusion throughout this project. It didn’t seem like I needed containers as a concept at all.

It took intervention by Miriam to make me realise that style containers are the rule, and size containers the exception. My confusion stems from learning about containers in the wrong order — and you likely have the same problem. So let me set you straight.

By default, the container of an element is its parent element.

In this example, the value of --screenSize on the <section> as the parent of div.layout determines whether the div becomes a flexbox or not.

<section>
	/* section's --screenSize is 3 */
	<div class="layout"></div>
</section>

div.layout {
	@container style(2 <= --screenSize <= 4) {
		display: flex;
		/* style query fires, so it becomes a flexbox */
	}
}

You can set --screenSize on the layout div itself if you like. That doesn’t influence style container queries because the layout div is not its own container. In the next example, --screenSize remains 3 for the purposes of style queries.

<section>
	/* section's --screenSize is 3 */
	<div class="layout"></div>
</section>

div.layout {
	--screenSize: 5;
	@container style(2 <= --screenSize <= 4) {
		display: flex;
		/* style query fires, so it becomes a flexbox */
	}
}

It is not possible to fire the style container query based on the --screenSize value in the layout div. It simply doesn’t work that way. (You could use an if() instead, but that’s a quite different beast. See below.)

container-name

But what if you don’t want to use the parent as a container, but some earlier ancestor? In that case you use container-name.

<main>
	<section>
		<div class="layout"></div>
	</section>
</main>

main {
	container-name: main;
	/* inherits --screenSize 3 from html */
}

section {
	--screenSize: 5;
}

div.layout {
	@container main style(2 <= --screenSize <= 4) {
		display: flex;
		/* style query fires, so it becomes a flexbox */
	}
}

The style query now takes the value 3 that it finds in the closest ancestor with the name main. Ancestor. The layout div itself still doesn’t count.

Size containers

These rules about parents and names also go for size containers — the original container queries with cqw and such — but there’s one extra rule.

In order to be a valid container for a size query an element also has to have container-type: inline-size. Now the width of the container is computed without reference to the widths of its children. That’s a vital necessity if you want to set the width of a child to 20cqw or something: the child width is not allowed to change the width of the container, or you’d enter an infinite loop.

However, this does not apply to style containers because they don’t allow you to use the container width. There is no danger of infinite loops, so they don’t need a container-type, either.

This container-type requirement is not a rule for all containers; only for size containers. We unconsciously learned to think of them as the default, though. They aren’t — style queries are. Time we update our mental model.

if()

The most consistent remark during my research time was: "Use if()!" Certainly not! I shall do no such thing!

.iftest {
	background: if(
		style(--screenSize: 3): #f6d781;
		else: #f4607b;
	)
}

Granted, I wasn’t being very clear about what I was doing, and if() does allow you to read properties from the element itself instead of its container. That’s offset by two serious issues:

  1. if() has much worse compatibility than style queries. Right now it’s only supported by Chromium, and I can’t find any mention in the Safari 27 or Firefox 155 and 156 release notes.
  2. It only allows you to set an individual value, not an entire style rule.

And once you start thinking about it, style queries are also sort-of ifs, right?

@container style(--screenSize >= 4) /* I am an if() in disguise */

"If the --screenSize is 4 or larger, add these styles." Sort of. If you ignore that it can’t find out about itself.