slides.oddbird.net/csswg/f2f2102/

CSSWG Proposals

@ Virtual Face-to-Face
@media (width > 45em) -
diagram of two different card layouts
on either side of a media-query breakpoint
(viewport width > 45em) -
cards both using large layout,
but one is in a small sidebar container
(container width > 45em) -
cards each using the appropriate layout
for the container it is in

2020 Proposals

  • David Baron: @container
    limited by containment
  • Brian Kardell: switch()
    limited to paint

Establishing Containers

.sidebar, main, .grid-item {
contain: size layout;
}

Size containment Is Too Restrictive

1D Size Containment

(proposed by David Baron)

  • inline-size (most useful & most possible)
  • block-size?
  • width?
  • height?

DBaron’s @container Rule

@container <selector> (<container-media-query>) {
/* ... rules ... */
}
@container .sidebar, main, .grid-item (width > 40em) {
.card { /* ... card layout ... */ }
h2 { /* ... responsive type? ... */ }
}
/* query *nearest contained ancestor* */
@container (width > 40em) {
.card { /* ... */ }
h2 { /* ... */ }
}
.card {
display: grid;
grid-gap: 0.5em;
}

@container (min-width: 40ch) {
.card {
grid-template: "img text" auto / minmax(10em, 30%) 1fr;
grid-gap: 1em;
}
}
h2 {
font-size: 1.2em;
line-height: 1.4;
}

@container (min-width: 40ch) {
h2 {
font-size: 1.6em;
line-height: 1.2;
}
}

Rules in @container apply when…

  • There’s an ancestor with appropriate containment
  • The laid out size of the most immediate container
    matches the query condition

Query against computed values

  • em queries can resolve based on computed font size?
  • Can we query more than just dimensions?

Potential issues

  • Difficult to query grid-track
  • Container is always external

Alternative selector syntax

@container (inline-size < 40em) {
.card { /* ... */ }
}

.card:container(inline-size < 40em) {
/* ... */
}

With nesting syntax

.card:container(inline-size < 40em) {
& h2 { /* ... */ }
}

Other Questions

The Scope Goal

  1. Avoid Naming Conflicts

    (across large teams & projects)
  2. By Expressing Membership

    (through lower boundaries & proximity)
wireframe of a site, with multiple nested components
.title { /* global */ }
.post .title { /* nested */ }

.post__title { /* BEM */ }
.light-theme a { color: purple; }
.dark-theme a { color: plum; }
<div class="dark-theme">
<a href="#">plum</a>
<div class="light-theme">
<a href="#">also plum???</a>
</div>
</div>
Diagram shows a widget with solid boundaries,
which cannot be penetrated
in either direction
(global styles can't get in, widget styles can't get out)

Encapsulation is High-Impact & DOM-Centric

(non-goals for this proposal)

  • 1:1 relationship widget/scope
  • Boundaries defined in the DOM
  • Styles are isolated by default
  • Proximity overrides specificity

Conventions and build-tools Provide Scoped Styles

BEM, CSS Modules, Vue, JSX, Stylable, etc

Diagram shows a component with porous boundaries,
all styles can penetrate, or establish their own lower boundaries

Scope is Low-Impact & Style-Centric

(goals for this proposal)

  • Scopes are defined in CSS
    • …can be re-used across components
    • …can overlap & cascade together
  • Priority is mostly handled by ownership
  • Specificity overrides proximity

Expressing Membership

.post__title { /* BEM */ }
.title[data-scope=post] { /* … */ }
.x1234 { /* … */ }

Proposal… @scope & :scope

The @scope rule

/* @scope (<root>#) [to (<boundary>#)]? { … } */
@scope (.tabs) to (.panel) { /* … */ }

The :scope selector

@scope (.tabs) to (.panel) {
:scope { /* targeting the scope root */ }
.light-theme :scope .tab { /* contextual styles */ }
}

Implied :scope ancestor

@scope (.tabs) {
.tab { /* :scope .tabs */ }
}

For boundary-matching

@scope (.tabs) to (.panel) {
.panel { /* lower boundary is in-scope */ }
}

For boundary-matching

@scope (.tabs) to (.tabs) {
/* to (:scope .tabs) */
}

@scope (.tabs) to (:scope) {
/* single-element scope (needs use-cases?) */
}

For selector-matching

@scope (.tabs) to (.panel, .tabs) {
.tab { /*
:scope .tab:not(:scope :is(.panel, .tabs) .tab)
*/
}
.tabs { /*
:scope .tabs:not(:scope :is(.panel, .tabs) .tabs)
*/
}
}

For specificity

@scope (.tabs, #options) {
.tab { /*
specificity: [1, 1, 0]
as-though: :is(.tabs, #options) .tab
*/
}
}

For proximity

@scope (.light-theme) { a { color: purple; } }
@scope (.dark-theme) { a { color: plum; } }
<div class="dark-theme">
<a href="#">plum</a>
<div class="light-theme">
<a href="#">purple</a>
</div>
</div>

For proximity

@scope (.light-theme) { a { color: purple; } }
@scope (.dark-theme) { a { color: plum; } }
<div class="dark-theme light-theme">
<a href="#">plum (source order)</a>
</div>

Specificity takes precedence

@scope (.hero) { #call-to-action { text-decoration: none; } }
@scope (.typeset) { a { text-decoration: undeline; } }
<header class="hero">
<div class="typeset">
<a id="call-to-action">purple (specificity)</a>
</div>
</header>

Potential “Donut” Selector?

concept suggested by Lea Verou in TAG review

@scope (.tabset) to (.panel) { .tab { /* ... */ }  }
.tab:in(.tabset / .panel) { /* ... */ }