CSS Style Query Explainer

Table of Contents
Archived:

🚚 This content is being maintained elsewhere. Our notes are likely out-of-date.

See the published specification for details.

Authors

Miriam Suzanne

Participate

These features are already defined in the CSS Containment Module Level 3 Working Draft.

Initial CSSWG issues:

Currently open CSSWG issues:

Deferred issues for level 4:

Related links:

Introduction

In a world of design systems and custom components, authors need a way to define simple parameters that control multiple properties. The current workaround used by many web component authors is to define custom attributes (often data-*) that are passed in via HTML.

However, the TAG guidelines state that custom attributes should not be used for styling, and authors should rely on custom properties instead. While it’s true in theory that custom properties would be a better solution – since they exist entirely in CSS, and cascade the same as other style properties – custom properties are currently limited to carrying a single value. It’s hard to achieve any more complex impact on other properties, beyond simple substitution.

Meanwhile, container queries allow authors to ‘query’ some set of conditions on an ancestor element, in the same ways that media queries allow us to query various conditions of the overall viewport, browser, and interface.

Also similar to media queries, the majority of discussion has historically focussed on size-based queries — especially the width of the viewport or container. But much like device-interface and user-preference media queries, there are a number of other powerful ‘container features’ that would be useful to query. We’ve categorized these roughly into two types:

  1. Style features (already specified in CSS Containment Level 3) allow querying the computed styles of a container.
  2. State features would allow querying various aspects of the container’s current state — such as a position:sticky container being currently ‘stuck’, or an overflow:auto container currently ‘overflowing’. These feature would likely need to be defined one-at-a-time, and require more research.

This document is an explainer of possible container query style features, which would allow authors to use custom properties (and existing properties) to create higher-level patterns and controls.

Goals

The goal of this proposal is to allow authors to define conditional rules based on their container context – and the cascaded/computed values of container properties. For example:

This proposal aims to give authors those tools, such that the resulting conditional styles:

Neither of those is possible to achieve with inline conditional statements.

Non-goals

In some use-cases, authors only want to change a single property based on an inherited style. For example, an author may want to un-italicize the em element when the context is already italic. While that’s possible using the container query syntax proposed here, an inline conditional or toggle() function is likely to provide better ergonomics.

Ideally authors would have both inline-value and at-rule level syntax available for conditional statements – and the two should integrate smoothly where possible. However, this proposal is entirely focused on the block level solution.

It’s also likely that we will want to provide additional style query features, particularly range-based style queries (e.g. (padding-inline > 1em)), or arbitrary style comparisons (e.g. (padding-inline > margin-inline) or (1em > 2vw)). See issue #7068 in csswg-drafts, which has been deferred to level 4 of the specification.

In the meantime, we think the basic computed-value match provides a powerful first version of the functionality, while avoiding some of the more difficult questions. Equality comparisons for computed values are already well-defined, and give us a framework to build on.

Proposed solutions

Container queries work by defining container elements on the page, and then allowing their descendent elements to apply conditional styles based on their ancestor containers.

The initial release of this feature (now available in Blink and WebKit browsers) was primarily interested in size-based or dimensional query features – containing the size/layout of the container, so that we can measure and respond to it’s height, width, aspect ratio, or orientation.

At the same time (and in the same specification) we left room for additional container/query types, and defined the following syntax for container query style features.

Querying computed styles: @container style()

The container query syntax relies on the @container rule:

@container <name>? <conditions> {
/* conditional styles */
}

For dimensional queries, that often looks very similar to a media query:

@container layout (min-width: 30em) {
.card { padding: 1em; }
}

However, the condition has to be resolved individually for each element matched by a selector (e.g. .card) inside the query. For each matched element, we determine the proper container to query by filtering ancestor elements for:

If a container is found, the conditions are resolved against that container. If multiple containers are found, the ‘nearest’ relative container takes precedence. If no container is found, the query returns unknown (similar to false, but also false when negated).

Style queries would use the same underlying syntax and logic, but differentiated by a style() function syntax, which accepts any valid style declaration:

@container style(font-style: italic) {
em {
background: var(--highlight);
color: var(--highlight-text);
}
}

@container style(--button: pill) {
button {
border-radius: 50%;
padding-inline: 1em;
}
}

@container colors style(background-color: black) {
a:any-link {
color: var(--link-on-dark);
}
}

Since we are comparing computed values, we can use custom properties and relative units on either side of the condition declaration – and the replaced values will be used for comparison:

/* --bg-dark is resolved on the container before comparing values */
@container theme style(background-color: var(--bg-dark)) {
a:any-link { color: powderblue; }
}

Style conditions that query a shorthand property are true if the computed values match for each of its longhand properties.

Defining style containers: container-type and container-name

Dimensional queries require css containment on the size, layout, and style of the container in order to prevent layout loops. Containment is an invasive thing to apply broadly, so it was important that authors have careful control over what elements are (or are not) size containers.

Style-based queries don’t have the same limitation. There is already no way in CSS for descendant styles to have an impact on the computed styles of an ancestor. So no containment is required, and there are no invasive or unexpected side-effects in establishing an element as a style query container.

Still, there are two important (and somewhat distinct) use-cases to consider when querying styles:

This has been discussed in great detail while determining the initial value of container-type.

Default containers for inherited properties

When querying inherited properties, the most relevant ‘container’ context will generally be the direct parent of the querying element.

For example, if we want to change the em styles based on the surrounding context. For most em elements, we would add italics, but when the context is italic already we may want to apply a background instead:

@container style(font-style: italic) {
em {
background-color: var(--highlight-bg);
color: var(--highlight-text);
}
}

In order for that direct parent context to be available consistently, we’ve defined that as the default: all elements are style containers, no matter what value is set in container-type. Since there are no negative side-effects involved with establishing style containers, we think that’s the best path for author usability.

If we need a way for authors to turn that off in some cases, we can consider adding a container-type: none value to remove it from the list of style containers. However, we haven’t seen any need for it in the use-cases so far. This default container-type logic is discussed in more detail below.

Named containers for non-inherited properties

With dimensional queries, we can know that the nearest size/inline-size container has a relevant size to query, and with inherited properties we can assume the direct parent has a relevant property/value to query. But we can’t reliably make any assumptions about any container having a relevant value for non-inherited properties

If we want to query the current padding on a container, or its background-color, neither the direct parent nor the nearest explicit container would be reliable. In this case, authors should be much more explicit about what containers can provide relevant information.

That’s already possible with the existing container-name property, which accepts any number of optional/reusable names for a container. This allows authors to establish custom container patterns across multiple components:

.theme {
container-name: theme;
}
.grid {
container-name: layout;
}
.card {
container-name: card layout theme;
}

By establishing container name conventions, authors can ensure that a style query is always targeting an appropriate container – no matter their ancestor/descendant proximity, or intervening (but irrelevant) containers. To query the padding on a ‘card’ component, always query a ‘card’ container. To query the current color theme, always query the nearest ‘theme’ container:

@container card style(padding: var(--small)) {
/* nearest 'card' ancestor has --small padding */
}

@container theme style(background-color: var(--bg-dark)) {
/* nearest 'theme' ancestor has --bg-dark background-color */
}

Key scenarios

Setting parameters in web components

One of the primary use-cases mentioned by Lea Verou in her request for higher level custom properties, is that web component authors can expose parameters to consumers of those components, without needing to rely on custom attributes.

Given a media-object component with the following HTML structure:

<template>
<article>
<div part="img">
<slot name="img"></slot>
</div>
<div part="content">
<slot name="title"></slot>
<slot name="content"></slot>
</div>
</article>
</template>

We can use the :host element as a container that accepts various parameters:

:host {
container: media-host / inline-size;
--media-location: before;
--media-style: square;
--theme: light;
}

Elements inside the component can query the parameters set on the media-host container:

  article {
display: grid;
grid-template: var(--default-template);
}

@container media-host style(--media-location: after) {
article {
grid-template: var(--reverse-template);
}
}

@container media-host style(--theme: fancy) {
article {
background:
linear-gradient(to bottom right, #FFC0CBBB, #EEEC, #B0E0E6BB),
conic-gradient(red, orange, yellow, green, blue, indigo, violet, red);
color: var(--media-color--dark);
border: medium solid mediumvioletred;
border-top-left-radius: 70% 60%;
border-top-right-radius: 30% 40%;
border-bottom-right-radius: 30% 60%;
border-bottom-left-radius: 70% 40%;
padding: 3em;
}
}

@container media-host style(--media-style: round) {
[part='media'] {
border-radius: 100%;
}
}

See the web component style query parameters demo on CodePen.

Contextual configuration without custom elements

While custom elements helpfully provide a wrapping element that can be used as a container – the same general approach can be used to establish contextual parameters with normal light-DOM elements. We can do that either with more generally-applied (or even ‘global’) configuration.

If we rely on inheritance for the contextual parameters, there’s no need to establish a container at all:

main {
--theme: blue;
background: #223;
color: snow;
}

@container style(--theme: blue) {
.card {
background: royalblue;
border-color: navy;
color: white;
}

a:any-link {
color: powderblue;
}

button {
border-color: navy;
background-color: dodgerblue;
color: white;
}
}

Or we can add wrapper elements by hand, when necessary. Since this is required for dimensional queries in relation to grid and flexbox tracks, the same wrappers can often be reused:

.card-container {
container: card / inline-size;
}

@container card style(--theme: blue) {
.card { /* dark theme card styles */ }
}

@container card (inline-size > 30em) {
.card { /* larger space card styles */ }
}

There are various codepen demos that explore use-cases along these lines:

Parameters for generated content

Much like custom elements, pseudo-elements such as ::before and ::after also come with a built-in container – the element on which they are generated. When using a pseudo-element to create an ‘arrow’ on a tooltip, we can use style queries to set the style and position of the arrow:

.bubble {
--arrow-position: end end;
container: bubble;
border: medium solid green;
position: relative;
}

.bubble::after {
content: "";
border: 1em solid transparent;
position: absolute;
}

@container bubble style(--arrow-position: end end) {
.bubble::after {
border-block-start-color: inherit;
inset-block-start: 100%;
inset-inline-end: 1em;
}
}

@container bubble style(--arrow-position: start end) {
.bubble::after {
border-block-start-color: inherit;
inset-block-start: 100%;
inset-inline-start: 1em;
}
}

We can also combine queries to avoid repeated properties:

@container bubble style(--arrow-position: start start) or style(--arrow-position: end start) {
.bubble::after {
border-block-end-color: inherit;
inset-block-end: 100%;
}
}

@container bubble style(--arrow-position: start end) or style(--arrow-position: end end) {
.bubble::after {
border-block-start-color: inherit;
inset-block-start: 100%;
}
}

@container bubble style(--arrow-position: start start) or style(--arrow-position: start end) {
.bubble::after {
inset-inline-start: 1em;
}
}

@container bubble style(--arrow-position: end start) or style(--arrow-position: end end) {
.bubble::after {
inset-inline-end: 1em;
}
}

See the queries with pseudo-classes demo on CodePen.

Simple value cycles

One of the common use-cases seems to come with the best alternative solution, at least in it’s simplest form. This is cycling one property-value based on the value of the same property on the parent.

For example, we can cycle the font-style between italic and normal values as we nest:

em, i, q {
font-style: italic;
}

@container style(font-style: italic) {
em, i, q {
font-style: normal;
}
}

Now our em, i, and q tags will be italic by default, but will revert to normal when nested inside an italic parent – for example an em inside a q.

However, there’s an existing proposal & spec for handling this use-case with a function, currently called toggle():

em, i, q {
font-style: toggle(italic, normal);
}

Complex value adjustments

In a case where the cycled styles are limited to a single property, the toggle() function is clearly a simpler solution. But it has pretty strict limitations:

Instead of simply cycling between italic and normal values, we may want to give the nested version a new background color, or underline, or other styles that make it stand out, besides simply toggling the italics.

This is not possible with the functional approach, but it becomes trivial with style queries:

@container style(font-style: italic) {
em, i, q {
background: lightpink;
}
}

Queries also allow us to use multiple property conditions:

@container style((font-style: italic) and (--color-mode: light)) {
em, i, q {
background: lightpink;
}
}

Or apply the same query condition to multiple properties:

@container style(font-style: italic) {
em, i, q {
/* clipped gradient text */
background: var(--feature-gradient);
background-clip: text;
box-decoration-break: clone;
color: transparent;
text-shadow: none;
}
}

None of those variations are possible using the proposed toggle() function.

Querying non-inherited properties

There are various use-cases that involve querying non-inherited properties.

Using var() in container queries

Light/dark themes are a consistently useful example of contextual styling that may depend on multiple inputs. A few examples might include querying the background-color of a container to determine if the context has a light or dark theme:

main, aside {
container: theme;
}

main {
background: var(--bg-dark);
}

aside {
background: var(--bg-light);
}

@container theme style(background-color: var(--bg-dark)) {
/* styles for our dark theme */
a:any-link { color: powderblue; }
}

@container theme style(background-color: var(--bg-light)) {
/* styles for our light theme */
a:any-link { color: navy; }
}

Given the code above, links in the aside and main elements will respond contextually to the background colors established in each.

Another example might involve lists and other ideally-outdented content to query available padding on any typesetting container that they are in:

body {
container: typeset;
padding: var(--gap-small);
}

/* This would ideally use a range query in the future */
@container typeset style(padding: var(--gap-large)) {
ul {
padding-inline-start: 0;
color: green;
}
}

There are several CodePen demos showing this sort of behavior, although they currently require an extra custom property, since the Chromium prototype doesn’t yet support queries on non-custom properties:

Replacing the ‘variable space toggle’ hack

The ‘variable space toggle’ hack has gained some attention as a workaround for simple true/false value conditions. See, for example:

In many cases, a container style query could be used to provide similar functionality. Some of these have already been demonstrated above. However, the restriction against self-querying can sometimes complicate those use-cases:

If declaration-level conditional functions are able to work around that limitation, authors would have more options for balancing the limitations of each approach.

Detailed design discussion & alternatives

Alternatives to an at-rule syntax

There have been several proposals for using custom properties as higher-level controls.

Inline conditional functions

Inline conditional functions would provide a declaration-level control. There are several proposals, most notably Lea Verou’s inline if() proposal, which seem useful to pursue in parallel to this rule-block level feature.

Inline functions have some limitations that can only be addressed at this higher level:

We don’t see these as competing approaches, but features that would complement each other.

Global constants

Allowing selectors to access arbitrary custom properties creates a difficult condition loop – where selectors and declarations both depend on the other to resolve first. One way around that would be to add global constants that can be defined at the root of a document (including web components), but not altered in selectors. To quote from the proposal:

These custom properties:

This would require a secondary property-definition syntax, and clear distinction for authors between ‘types’ of custom properties with different cascading features. It also severely limits the author’s ability to use the cascade in establishing the variables to begin with. Since the values are global within a context, authors would need to rely on web components specifically, in order to establish nested contexts with different ‘global’ parameters.

Conditional pseudo-class

One of the more obvious syntax solutions for a rule-block level condition would be a functional pseudo-class. However, that introduces a dependency between the selectors themselves and the property-values defined in the selector rule block.

Implementors are understandably hesitant to add the multiple passes required to resolve those conflicts – but one workaround would be disallowing the pseudo-class on the subject compound (as suggested by Anders Hartvoll Rudd):

/* Not valid */
.subject:const(--x:1) { /* ... */ }

/* Valid */
.container:const(--x:1) .subject { /* ... */ }

This is a viable solution, but it stands out to me as having the same limitations as a container query approach (for the same reasons) – while providing a bit less clarity and flexibility.

While there are other pseudo-classes that are only meaningful on specific elements, I don’t know of existing cases where a pseudo-class is invalid in a specific selector position. In this case, the limitation is not obvious in the syntax itself, and could be difficult to teach or debug. On the other hand, an at-rule makes the distinction more clear: a ‘container’ (target of the at-rule) exists outside the ‘subject’ (target of the selector) which will apply the conditional styles.

That has several advantages:

All of those issues are possible to work around using a nested syntax – but that relies on authors to provide clarity, rather than supplying it directly in the syntax provided.

Building on container queries

Meanwhile, the existing Container Query syntax already provides many of the requirements for making style queries on all properties both functional and convenient, without limiting them to global constants:

The primary downside is that elements are not able to query themselves. This limitation is also unfortunate in dimension-based queries – and would likely limit pseudo-classes as well. The workaround can require extra HTML in some cases, but that is less of an issue in web components where the :host can often act as a container for the elements within:

:host { container-name: host; }

@container host style(--orientation: landscape) {
/* conditions using custom property parameters on the host element */
}

This syntax should also be extensible in the future, to allow:

Additionally, it would complement an inline if(), so that both could rely on similar condition syntax, and rules for comparison – but the inline version would provide a different set of tradeoffs:

Default container types

There have been several relevant discussions about the proper initial value for container-type, specifically in relation to the needs of style queries:

In the end, browsers have shipped an initial container-type value of normal – which allows us to expand the meaning of that as we add new query types.

The current plan is for all existing container-typesnormal, size, and inline-size – to support style queries. Without that feature, it becomes much more difficult to query inherited properties. Authors would likely apply a style type universally, and then need to be careful not to override that value when establishing size containers:

* { container-type: style; }

/* Would accidentally remove the style type */
main { container-type: inline-size; }

If we find that there are reasons to override that all-elements-are-style-containers default behavior, we can provide an explicit none value. At this point, we haven’t found any clear need for it.

Stakeholder Feedback / Opposition

References & acknowledgements

The CSS Containment spec is co-authored by Tab Atkins and Florian Rivoal. Elika Etimad was also involved in specifying style queries. Many of the goals and use-cases are based on the work of Lea Verou and others in the CSSWG-drafts Github issues, along with Una Kravets and others linked above.

It has also been helpful to have the Chromium prototype for experimentation, with feedback from Nicole Sullivan, and the browser engineers involved: Rune Lillesveen, and Anders Hartvoll Ruud.