[csswg-drafts] [css-conditional] [css-contain] Fleshing out @container queries with single-axis containment (#5796)

mirisuzanne has just created a new issue for https://github.com/w3c/csswg-drafts:

== [css-conditional] [css-contain] Fleshing out @container queries with single-axis containment ==
## Intro

Media-queries allow an author to make style changes based on the overall viewport dimensions -- but in many cases, authors would prefer styling modular components based on their context within a layout. Earlier this year, David Baron & Brian Kardell proposed two complementary approaches to explore: a [`@container` rule][dbaron-cq], and a [`switch()`][switch] function. Both seem useful in different situations.

[dbaron-cq]: https://github.com/dbaron/container-queries-implementability
[switch]: https://bkardell.com/blog/AllThemSwitches.html

This proposal builds on David Baron's `@container` approach, which works by applying size & layout containment to the queried elements. Any element with both size & layout containment can be queried using a new `@container` rule, with similar syntax to existing media-queries. Currently, size containment is all-or-nothing. In order to make that less restrictive for authors, there are proposed single-axis (`inline-size` & `block-size`) values for the `contain` property.

This is a rough outline of the feature as I imagine it -- but there are a number of questions that could more easily resolved with a prototype. The purpose of this document is to flesh out some direction for more applied testing & exploration.

## Table of contents:

- [Goals](#goals)
- [Non-goals](#non-goals)
- [Proposed Solutions](#proposed-solutions)
  - [Single-axis containment (`inline-size` & `block-size` values)](#single-axis-containment-inline-size--block-size-values)
  - [Containment context](#containment-context)
  - [Container queries (`@container`)](#container-queries-container)
- [Key scenarios](#key-scenarios)
  - [Modular components in any container](#modular-components-in-any-container)
  - [Components with internal containers](#components-with-internal-containers)
  - [Component in a responsive grid track](#component-in-a-responsive-grid-track)
- [Detailed design discussion & alternatives](#detailed-design-discussion--alternatives)
  - [Single-axis containment issues](#single-axis-containment-issues)
  - [Implicit vs explicit containers](#implicit-vs-explicit-containers)
  - [Combining scope with container queries?](#combining-scope-with-container-queries)
  - [@-Rule or pseudo-class?](#-rule-or-pseudo-class)
  - [Questions to explore](#questions-to-explore)
- [References & acknowledgements](#references--acknowledgements)

## Goals

Often the layout of a page involves different "container" areas -- such as sidebars and main content areas -- with modular 
"component" parts that can be placed in any area. Those components can be complex (a full calendar widget) or fairly simple (basic typography), but should respond in some way to the size of the container.

This can happen at multiple levels of layout, even inside nested components. The important distinction is that the "container" is distinct from the "component" being styled. Authors can query one to style the other.

## Non-goals

Modern layouts provide a related problem with slightly different constraints. When using grid layout, for example, available grid-track size can change in ways that are difficult to describe based on viewport sizes -- such as shrinking & growing in regular intervals as new columns are added or removed from a repeating `auto-fit`/`auto-fill` grid. In this case there is no external "container" element to query for an accurate sense of available space.

There is a [reasonable workaround](#component-in-a-responsive-grid-track) in this proposal, but a more ideal solution might look more like Brian Kardell's [`switch()` proposal][switch]. It would be good to consider these approaches together, as they make complementary tradeoffs.

There are also proposed improvements to flexbox & grid -- such as [indefinite grid spans](https://github.com/w3c/csswg-drafts/issues/388) or [first / last keywords](https://github.com/w3c/csswg-drafts/issues/2402) -- which would help expand on the responsive nature of those tools.

And finally, authors often want to _smoothly interpolate values_ as the context changes, rather than toggling them at breakpoints. That would require a way to describe context-based animations, with breakpoint-like keyframes. Scott Kellum has been doing a lot of work in that area.

All of those would contribute towards a larger goal of "responsive components" in CSS. I don't think we can solve it all in a single feature.

## Proposed Solutions

### Single-axis containment (`inline-size` & `block-size` values)

_This is a proposed change to the [CSS Containment Module](https://drafts.csswg.org/css-contain/), specifically [size containment](https://drafts.csswg.org/css-contain/#containment-size) -- and already has [an issue thread for discussion](https://github.com/w3c/csswg-drafts/issues/1031)._

In order for container-queries to work in a performant way, authors will need to define container elements with explicit containment on their layout and queried-dimensions. This can be done with the existing `contain` property, using the `size` and `layout` values:

```css
.container {
  contain: size layout;
}
```

While that will work for some use-cases, the majority of web layout is managed through constraints on a _single (often inline) axis_. Intrinsic sizing on the cross (often block) axis is required to allow for changes in content, font size, etc. So this proposal would rely on one or two new single-axis values for `contain`, as discussed in #1031:

```css
.inline-container {
  contain: inline-size;
}

.block-container {
  contain: block-size;
}
```

Of these two values, it is clear that `block-size` has the fewer use-cases, and more potential implementation issues. Support for `block-size` would be great, but is not required to make container queries useful.

### Containment context

Ideally, container queries could be resolved against the _available space_ for any given element. Since size and layout containment are required, we instead need to define the _containment context_ for each element.

I'm proposing that any element with layout and size containment on a given axis generates a new _containment context_ in that axis, which descendants can query against:

```css
.two-axis-container {
  /* establishes a new containment context on both axis */
  contain: layout size;
}

.inline-container {
  /* establishes a new containment context on the inline axis */
  contain: layout inline-size;
}

.block-container {
  /* establishes a new containment context on the block axis */
  contain: layout block-size;
}
```

When size-containment is only available on a single axis, queries on the cross-axis will not resolve against that query context. When no containment context is established, there should be a fallback akin to the _Initial Containing Block_ which queries can resolve against.

### Container queries (`@container`)

This could be added to a future level of the [CSS Conditional Rules Module](https://drafts.csswg.org/css-conditional-3/). 

The `@container` rule can be used to style elements based on their immediate _containment context_, and uses a similar syntax to existing media queries. Where possible `@container` syntax should follow the established specifications for _conditional group rules_, and allow queries to be combined in a list:

```css
/* @container <container-query-list> { <stylesheet> } */
@container (width > 45em) {
  .media-object {
    grid-template: "img content" auto / auto 1fr;
  }
}
```

This would target any `.media-object` whose _containment context_ (nearest ancestor with containment applied) is greater-than `45em`. When no containment context is established, the _Initial Containing Block_ can be used to resolve the query.

Unlike media-queries, each element that is targeted by a conditional group rule will need to resolve the query against its own containment context. Different elements targeted by the same selector within the same query may still resolve differently based on context. Consider the following CSS & HTML together:

```css
/* css */
section {
  contain: layout inline-size;
}

div {
  background: red;
}

@container (width > 500px) {
  div {
    background: yellow;
  }
}

@container (width > 1000px) {
  div {
    background: green;
  }
}
```

```html
<!-- html -->
<section style="width: 1500px">
  <div>green background</div>

  <section style="width: 50%">
    <div>yellow background (resolves against inner section)</div>
  </section>
</section>
<section style="width: 400px">
  <div>red background</div>
</section>
```

Each `div` resolves differently based on its own immediate context.

#### Container features

Like media-queries, `@container` needs a well defined list of "features" that can be queried. The most essential container features are the contained dimensions:

- physical dimensions: `width` / `height`
- logical dimensions: `inline-size` / `block-size`

When containment is applied on both axis, we might also be able to query dimensional relationships such as:

- `aspect-ratio`
- `orientation`

Since container queries resolve against styled elements in the DOM, it may also be possible to query other aspects of the container's computed style?

- `inline-content-box`
- `font-size`
- etc.

This needs more discussion and fleshing-out.

## Key scenarios

### Modular components in any container

Page layouts often provide different layout "areas" that can act as containers for their descendant elements. These can be nested in more complex ways, but let's start with a sidebar and main content:

```html
<body>
  <main>...</main>
  <aside>...</aside>
</body>
```

We can establish a responsive layout, and declare each of these areas as a _containment context_ for responsive components:

```css
body {
  display: grid;
  grid-template: "main" auto "aside" auto / 100%;
}

@media (width > 40em) {
  body {
    grid-template: "aside main" auto / 1fr 3fr;
  }
}

main,
aside {
  contain: layout inline-size;
}
```

Now components can move cleanly between the two areas -- responding to container dimensions without concern for the overall layout. For example, some responsive defaults on typographic elements:

```css
h2 {
  font-size: 120%;
}

@container (width > 40em) {
  h2 {
    font-size: calc(130% + 0.5vw);
  }
}
```

Or "media objects" that respond to available space:

```css
.media-object {
  grid-template: "img" auto "content" auto / 100%;
}

@container (width > 45em) {
  .media-object {
    grid-template: "img content" auto / auto 1fr;
  }
}
```

### Components with internal containers

A more complex component, like a calendar, might reference external context while also defining nested containers:

```html
<section class="calendar">
  <div class="day">
    <article class="event">...</article>
    <article class="event">...</article>
  </div>
  <div class="day">...</div>
  <div class="day">...</div>
</section>
```

```css
.day {
  contain: layout inline-size;
}

/* despite having different containers, these could share a query */
@container (width > 40em) {
  /* queried against external page context */
  .calendar {
    grid-template: repeat(7, 1fr);
  }

  .day {
    border: thin solid silver;
    padding: 1em;
  }

  /* queried against the day */
  .event {
    grid-template: "img content" auto / auto 1fr;
  }
}
```

### Component in a responsive grid track

In some situations, there is no clear "container" element defining the available space. Consider the following HTML & CSS:

```html
<section class="card-grid">
  <div class="card">...</div>
  <div class="card">...</div>
  <div class="card">...</div>
  <div class="card">...</div>
</section>
```

```css
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(20em, 1fr));
}

.card {
  display: grid;
  /* we want to change this value based on the track size */
  grid-template: "image" auto "content" 1fr "footer" auto / 100%;
}
```

The size of `.card-grid` does not accurately reflect the available space for a given card, but there is no other external "container" that `.card` can use to adjust the `grid-template`. Authors using this feature would need to add an extra wrapping element -- so that the card component has an external track-sized container to query:

```html
<section class="card-grid">
  <div class="card-container"><div class="card">...</div></div>
  <div class="card-container"><div class="card">...</div></div>
  <div class="card-container"><div class="card">...</div></div>
  <div class="card-container"><div class="card">...</div></div>
</section>
```

```css
/* the outer element can get containment… */
.card-container {
  contain: layout inline-size;
}

/* which gives .card something to query against */
@container (width > 30em) {
  .card {
    grid-template: "image content" 1fr "image footer" auto / 1fr 3fr;
  }
}
```

There are already many similar situations in CSS layout, so this might be a viable solution for most use-cases -- but the extra markup is not ideal.

## Detailed design discussion & alternatives

### Single-axis containment issues

The existing [issue thread](https://github.com/w3c/csswg-drafts/issues/1031) has a proposal for moving forward with single-axis containment despite the known issues.

### Implicit vs explicit containers

In conversations leading to this proposal, there has been some concern about the dangers of establishing context _implicitly_ based on the value of `contain`. Similar behavior for positioning & stacking has sometimes been confusing for authors.

[David Baron's proposal][dbaron-cq] included a selector for querying a container more explicitly:

```css
/* syntax */
@container <selector> (<container-media-query>)? {
  /* ... */
}

/* example */
@container .media-object (width > 45em) {
  .media-object {
    grid-template: "img content" auto / auto 1fr;
  }
}
```

Since all known use-cases attempt to query the _most immediate available space_, I don't see any need for querying containers with an explicit syntax, or any way to "skipping over" one container to query the next.

Adding a selector to the query would also raise new problems:

- Explicitly targeted queries are less modular, so components would not be able to query _whatever contasiner they happen to be in_.
- It adds potential confusion about what selectors are allowed in the block. Authors would not be able to style the container itself, unless we limited the properties allowed -- similar to the `switch()` proposal.

However, it might be helpful to consider a more explicit way of defining the containers initially, to make this more clear for authors -- such as `query`, `inline-query`, & `block-query` values that would apply both layout and size containment. This needs more discussion & consideration.

### Combining scope with container queries?

David Baron's proposal also uses the explicit container selector to attach the concept of `scope` to containers -- only matching selectors inside the query against a subtree of the DOM. This might be useful for use-cases where a component both:

- Establishes its own containment context, and
- Establishes its own selector scope

But in my exploration of use-cases, it seems common that components will want to query _external_ context, while establishing _internal_ scope. There is also a mis-match where authors expect to style the root element of a given scope, but should not be able to style the root of a container-query. For those reasons, I think the two features -- container queries and scope -- should remain distinct, and be addressed separately.

### @-Rule or pseudo-class?

Many proposals & Javascript implementations use a pseudo-class rather than an @-rule.

```css
/* pseudo-class */
.selector:container(<query >) {
  /* ... */
}
```

I think the @-rule block provides several advantages:

- The @-rule syntax matches more closely with existing conditional rules, and builds on existing query-list syntax.
- It's likely that a responsive component will have multiple moving parts, and each might require unique selectors based on the same query. These can be grouped in an @-rule.
- We avoid the issues mentioned above with having an explicit selector attached to the query.

I also think the syntax can lead to confusion. It's not immediately clear what these different selectors would mean:

```css
:container(width < 40em) {
  font-size: small;
}
.media-object:container(width < 40em) {
  font-size: small;
}
:container(width < 40em) .media-object {
  font-size: small;
}
```

### Questions to explore

- Is it possible to apply containment within a container query? In what cases might that lead to cyclic dependencies?
- Will a default root fallback container feel like expected behavior, or should we require explicit containment in order to match queries?
- Are there special considerations required for a shadow-DOM `@container` to resolve queries against contained host element?
- Do we need a more explicit value for establishing query context, apart from existing `contain` values?
- What features can we support besides dimension-queries?

## References & acknowledgements

This proposal is based on the previous work of many people:

- Brian Kardell: [All Them Switches][switch]
- David Baron: [Thoughts on an implementable path forward][dbaron-cq]
- Mat Marquis: [A rough proposal for syntax](https://github.com/WICG/container-queries/issues/2)
- Matthew Dean: [2019 Proposal/Solution for Container Queries](https://github.com/WICG/container-queries/issues/12)
- Viktor Hubert: [Container Query Plugin](https://github.com/ZeeCoder/container-query/blob/master/docs/syntax.md#Queries)
- WICG: [Use Cases and Requirements](https://wicg.github.io/cq-usecases/)
- And more: [Who is Working on Container Queries](http://whoisworkingoncontainerqueries.com/)

Thanks also for valuable feedback and advice from:

- Adam Argyle
- Amelia Bellamy-Royds
- Anders Hartvoll Ruud
- Chris Coyier
- Christopher Kirk-Nielsen
- Eric Portis
- Ethan Marcotte
- Florian Rivoal
- Geoff Graham
- Gregory Wild-Smith
- Ian Kilpatrick
- Jen Simmons
- Martin Auswöger
- Martine Dowden
- Mike Riethmuller
- Morten Stenshorne
- Nicole Sullivan
- Rune Lillesveen
- Scott Jehl
- Scott Kellum
- Tab Atkins
- Theresa O’Connor
- Una Kravets

(sorry if I missed anyone… I don't mean to imply all these people has _signed off_ on my proposal, or even seen the whole thing - but they have all provided useful feedback along the way)

Please view or discuss this issue at https://github.com/w3c/csswg-drafts/issues/5796 using your GitHub account


-- 
Sent via github-notify-ml as configured in https://github.com/w3c/github-notify-ml-config

Received on Tuesday, 15 December 2020 23:11:20 UTC