<!-- Generated by scripts/gen-skill.mjs from src/showcases/registry.ts. Do not edit by hand. -->

# Accessible implementations, feature by feature

Every excerpt keeps semantics, honours user preferences, and degrades to
something usable. They are free of this project’s tokens and mixins, so they
are copy-paste ready. Where an entry names a guard, copy it with the guard.
An entry marked "Feature test (not needed to ship)" is widely available and
safe without one; check its tier in `modern-css.md`.

## Container queries

**Feature test (not needed to ship):** `@supports (container-type: inline-size)`

```html
<!-- The wrapper is the container; the card inside responds to it.
     An element can't query itself — only its descendants can. -->
<div class="card-cell">
  <article class="card">…</article>
</div>
```

```css
.card-cell {
  container-type: inline-size;
}

/* The card reflows based on its container's width, not the viewport. */
@container (min-width: 20rem) {
  .card {
    grid-template-columns: 3rem minmax(0, 1fr);
  }
}

/* Paired with an intrinsic grid, this replaces most media queries. */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
}
```

## Container query units

**Feature test (not needed to ship):** `@supports (width: 1cqi)`

```css
.stage {
  container-type: inline-size;
}

/* 1cqi = 1% of the container's inline size. Spacing scales with the
   component's own space, so a narrow container, even one made narrow
   by zoom, leaves more room for the text. */
.card {
  padding: clamp(0.75rem, 6cqi, 2.5rem);
}

/* Fluid type needs two rules. A rem term in the middle value: with cqi
   alone, whether zoom or the user's font size makes the text bigger
   depends on the browser and the layout, and the rem term makes sure
   both always do. And a maximum within 1.5 times the minimum: the floor
   is in rem, so by 300% zoom, Safari's limit, the floor alone doubles
   the text. Body text stays on plain rem sizes. */
.card-stat {
  font-size: clamp(2.25rem, 1.75rem + 4.25cqi, 3.25rem);
}
```

## :has() relational selector

**Feature test (not needed to ship):** `@supports (selector(:has(a)))`

```html
<li class="row">
  <label>
    <input type="checkbox" />
    Review pull request
  </label>
</li>
```

```css
/* Persistent: style the row from its checkbox's checked state — something no
   upward selector can do, and which :focus-within can't keep after blur. */
.row:has(:checked) {
  border-color: royalblue;
  background: #eef2ff;
}

/* Transient focus is the lighter job :focus-within is actually for. */
.row:focus-within {
  background: #f3f4f6;
}
```

## Count-aware layouts (quantity queries)

**Feature test (not needed to ship):** `@supports (selector(:has(> :nth-child(2))))`

```html
<ul class="gallery">
  <li><img src="…" alt="…" /></li>
  <!-- the CSS below re-composes as more photos arrive -->
</ul>
```

```css
/* ":nth-child(n):last-child" = the nth child is also the last —
   i.e. the gallery holds EXACTLY n photos. */

/* Exactly 2 — split the bubble. */
.gallery:has(> :nth-child(2):last-child) {
  grid-template-columns: 1fr 1fr;
}

/* Exactly 3 — a lead photo plus a stacked pair. */
.gallery:has(> :nth-child(3):last-child) {
  grid-template-columns: 3fr 2fr;
  grid-template-areas:
    'lead top'
    'lead bottom';
}

.gallery:has(> :nth-child(3):last-child) > :first-child {
  grid-area: lead;
}

/* 5 or more — mosaic: first photo leads, the rest flow densely. */
.gallery:has(> :nth-child(5)) {
  grid-template-columns: repeat(3, 1fr);
  grid-auto-flow: dense;
}

.gallery:has(> :nth-child(5)) > :first-child {
  grid-column: span 2;
  grid-row: span 2;
}

/* Keep it gapless: the lead uses 4 cells, so n photos fill n + 3 cells,
   and any remainder shows up as short rows at the tail. Give each short
   row one wide photo: count ≡ 2 (mod 3) needs one, ≡ 1 (mod 3) needs two. */
.gallery:has(> :nth-child(5)):has(> :nth-child(3n + 2):last-child) > :nth-last-child(2),
.gallery:has(> :nth-child(5)):has(> :nth-child(3n + 1):last-child) > :nth-last-child(2),
.gallery:has(> :nth-child(5)):has(> :nth-child(3n + 1):last-child) > :nth-last-child(3) {
  grid-column: span 2;
}
```

## Subgrid

**Feature test (not needed to ship):** `@supports (grid-template-rows: subgrid)`

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

/* Each card adopts THREE of the parent grid's rows, so title / body / footer
   line up across cards no matter how long each card's content runs. */
.card {
  display: grid;
  grid-row: span 3;
  grid-template-rows: subgrid;
}
```

## Sliding selection indicator

**Feature test (not needed to ship):** `@supports (selector(:has(*)))`

```html
<!-- Real radios underneath — arrow-key navigable, announced as a group. -->
<fieldset class="segmented">
  <legend class="visually-hidden">Choose a view</legend>
  <div class="track" style="--n: 3">
    <span class="indicator" aria-hidden="true"></span>
    <label><input type="radio" name="view" class="visually-hidden" /> List</label>
    <label><input type="radio" name="view" class="visually-hidden" /> Grid</label>
    <label><input type="radio" name="view" class="visually-hidden" /> Board</label>
  </div>
</fieldset>
```

```css
.track {
  position: relative;
  display: grid;
  grid-template-columns: repeat(var(--n), minmax(0, 1fr));
}

/* One pill, one cell wide; it slides between options in whole steps, and only
   when the reader has not asked for reduced motion. Otherwise it jumps. */
.indicator {
  position: absolute;
  inset-block: 4px;
  inline-size: calc(100% / var(--n));
  transform: translateX(calc(var(--i, 0) * 100%));
}

@media (prefers-reduced-motion: no-preference) {
  .indicator {
    transition: transform 200ms;
  }
}

/* :has() maps the checked radio to the pill's slot. */
.track:has(label:nth-of-type(1) input:checked) { --i: 0; }
.track:has(label:nth-of-type(2) input:checked) { --i: 1; }
.track:has(label:nth-of-type(3) input:checked) { --i: 2; }
```

## text-wrap: balance / pretty

**Guard:** `@supports (text-wrap: balance)`

```css
/* Evens out line lengths across a short block — ideal for headings. */
.headline {
  text-wrap: balance;
}

/* Targets just the last line to prevent a single orphaned word — cheap
   enough for body copy, where balance would be too costly. */
.body-copy {
  text-wrap: pretty;
}
```

## Scroll snap

**Feature test (not needed to ship):** `@supports (scroll-snap-type: x mandatory)`

```html
<!-- A focusable region, so arrow keys / Page / Home-End scroll it too. The
     list stays a real <ul> inside, so "list, 5 items" is still announced
     (putting role="region" on the <ul> would strip that list semantics). -->
<div class="track" tabindex="0" role="region" aria-label="Featured cards">
  <ul role="list">
    <li class="card">…</li>
  </ul>
</div>
```

```css
.track {
  display: flex;
  gap: 1rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
}

.card {
  flex: 0 0 16rem;
  scroll-snap-align: center;
}
```

## Popover attribute

**Guard:** No guard needed.

```html
<!-- Wired by id alone. The platform gives Esc-to-close, click-outside
     dismiss, top-layer rendering, and aria-expanded on the button — no JS. -->
<button type="button" popovertarget="menu">Actions ▾</button>

<div id="menu" popover role="menu">
  <!-- Each item closes the menu declaratively. -->
  <button type="button" role="menuitem" popovertarget="menu" popovertargetaction="hide">
    Rename
  </button>
</div>
```

## :user-valid / :user-invalid

**Feature test (not needed to ship):** `@supports (selector(:user-valid))`

```css
/* :invalid fires immediately — an empty required field is
   "wrong" before the user has done anything at all. */
.field-eager:invalid {
  border-color: var(--color-error);
}

/* :user-invalid waits for real interaction: it only matches
   after the user has edited or left the field. Same native
   validation, judged at a humane moment. */
.field-patient:user-invalid {
  border-color: var(--color-error);
}

.field-patient:user-valid {
  border-color: var(--color-success);
}
```

## Native error squiggles

**Guard:** `@supports (text-decoration-line: spelling-error)`

```css
/* The platform already has a squiggle convention — let the browser
   draw it. Right shape, right colour, on every wrapped line, at any
   zoom, and it survives forced colours. */
.typo {
  /* Fallback for browsers without it: meaning preserved,
     convention lost. */
  text-decoration-line: underline;
  text-decoration-style: dotted;
  text-decoration-color: var(--color-error);
}

.grammar-slip {
  text-decoration-line: underline;
  text-decoration-style: dotted;
  text-decoration-color: var(--color-success);
}

@supports (text-decoration-line: spelling-error) {
  .typo {
    /* The browser ignores your decoration colour and style here
       and paints its native marking instead. */
    text-decoration-line: spelling-error;
  }

  .grammar-slip {
    text-decoration-line: grammar-error;
  }
}

/* The hack: paint the squiggle yourself. Backgrounds mark boxes,
   not text — the span goes inline-block so the gradient has a box
   to sit against, and the prose stops wrapping like prose. */
.typo-hack {
  display: inline-block;
  background-image: repeating-linear-gradient(
    -45deg,
    var(--color-error) 0 2px,
    transparent 2px 5px
  );
  background-repeat: no-repeat;
  background-position: bottom;
  background-size: 100% 4px;
  /* Why it drifts: on wrap the squiggle holds only the last line
     of the box; it cuts through descenders instead of skipping
     ink; and forced colours erase painted backgrounds outright. */
}
```

## Anchor positioning

**Guard:** `@supports (anchor-name: --a)`

```html
<!-- popover gives open/close, Esc + click-outside dismiss, and
     aria-expanded on the button — all from the platform, no JS. -->
<button type="button" class="trigger" popovertarget="panel">Toggle popover</button>
<div id="panel" popover class="panel">…</div>
```

```css
.trigger {
  anchor-name: --panel-anchor;
}

/* Tether the panel to its trigger and flip it across any edge it would
   overflow — no positioning JS. Stays position: fixed (the popover default)
   so it resolves against the viewport and the flips actually fire. */
@supports (anchor-name: --a) {
  .panel {
    position: fixed;
    position-anchor: --panel-anchor;
    position-area: block-end span-inline-end;
    inset: auto;
    position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
  }
}
```

## Anchor-positioned tooltip

**Guard:** `@supports (anchor-name: --a)`

```html
<!-- aria-describedby ties the hint to its trigger; role="tooltip" makes
     assistive tech announce it as one. The "?" glyph is decorative. -->
<span class="hint">
  <button type="button" aria-describedby="hint-name" aria-label="About display name">
    <span aria-hidden="true">?</span>
  </button>
  <span id="hint-name" role="tooltip" class="hint-bubble">
    Shown publicly beside your comments.
  </span>
</span>
```

```css
/* Hidden until hover or keyboard focus. */
.hint-bubble {
  display: none;
  opacity: 0;
  transition:
    opacity 150ms,
    display 150ms allow-discrete;
}

/* Reveal on hover, on keyboard focus, and while the bubble itself is hovered,
   so it stays put when you move onto it (the hoverable part of 1.4.13). */
.hint button:hover ~ .hint-bubble,
.hint button:focus-visible ~ .hint-bubble,
.hint-bubble:hover {
  display: block;
  opacity: 1;

  @starting-style {
    opacity: 0;
  }
}

/* No fade under reduced motion. Up to WebKit 26.5 a near-instant display
   exit never finished on an anchored element (fixed in 27.2), so a page with
   a 0.01ms reduced-motion reset would leave the hint open in Safari 26. */
@media (prefers-reduced-motion: reduce) {
  .hint-bubble {
    transition-property: none;
  }
}

/* The enhancement: anchor the bubble to its trigger and let it flip to stay
   on-screen. fixed means it follows on scroll and isn't clipped by overflow. */
@supports (anchor-name: --a) {
  .hint button {
    anchor-name: --hint;
  }

  .hint-bubble {
    position: fixed;
    position-anchor: --hint;
    position-area: block-start span-all;
    margin-block-end: 8px;
    position-try-fallbacks: flip-block;
  }

  /* A bridge over the 8px gap on both sides, so the pointer can cross to a
     flipped hint as well as to one above. */
  .hint-bubble::before {
    content: '';
    position: absolute;
    inset-block: -8px;
    inset-inline: 0;
    z-index: -1;
  }
}
```

## contrast-color()

**Guard:** `@supports (color: contrast-color(red))`

```css
.swatch {
  background: var(--bg);
  /* Fallback: a fixed light label with a shadow — readable on most colours
     but not guaranteed. That's the gap contrast-color() closes. */
  color: #fff;
  text-shadow: 0 1px 2px rgb(0 0 0 / 60%);
}

@supports (color: contrast-color(red)) {
  .swatch {
    /* The browser guarantees a contrasting label for ANY background. */
    color: contrast-color(var(--bg));
    text-shadow: none;
  }
}
```

## Contrast-safe theming

**Guard:** `@supports (color: oklch(from red l c h))`

```css
/* Two seeds per theme… */
.theme {
  --seed-surface: oklch(99% 0.004 255deg);
  --seed-accent: oklch(52% 0.16 260deg);
}

/* …and the whole accessible palette is DERIVED from them — no hand-picked
   greys. color-mix() blends each token toward the text colour. */
.theme {
  --color-surface: color-mix(in oklab, var(--seed-surface), var(--color-text) 5%);
  --color-border: color-mix(in oklab, var(--seed-surface), var(--color-text) 44%);
}

/* contrast-color() picks a guaranteed-readable text colour for each seed. */
@supports (color: contrast-color(red)) {
  .theme {
    --color-text: contrast-color(var(--seed-surface));
    --color-primary-text: contrast-color(var(--seed-accent));
  }
}
```

## Contrast-clamped theme picker

**Guard:** `@supports (color: oklch(from red l c h))`

```css
/* A pure-CSS contrast clamp: snap a picked lightness OUT of the "muddy
   middle", where neither a black nor a white label can reach WCAG AA. No JS —
   just min()/max() and a step built from clamp(). */
.preview {
  --raw-l: calc(var(--pick-l) / 100);
  /* 1 when the accent sits on the dark side of the threshold, else 0. */
  --is-dark: clamp(0, (0.62 - var(--raw-l)) * 100000, 1);
  /* Dark accents cap low (white label safe); light accents floor high (black
     label safe); the unsafe middle is squeezed out. */
  --safe-l: calc(
    var(--is-dark) * min(var(--raw-l), 0.45) +
    (1 - var(--is-dark)) * max(var(--raw-l), 0.78)
  );
  --seed-accent: oklch(var(--safe-l) 0.15 var(--pick-h));
}
```

## Scroll-driven animations

**Guard:** `@supports (animation-timeline: scroll())`

```css
.progress {
  /* Frozen, this would read as "0% progress" — worse than nothing — so it's
     hidden unless the browser can actually drive it from scroll position. */
  display: none;
}

@supports (animation-timeline: scroll()) {
  .progress {
    display: block;
    transform-origin: 0 50%;
    animation: grow linear both;
    /* Driven by the nearest scroll container, not by time. Runs off the main
       thread. It tracks the user's gesture one to one, like the scrollbar
       thumb, so it is left running under reduced motion: a judgement call,
       not an exemption. */
    animation-timeline: scroll(nearest);
  }
}

@keyframes grow {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

/* A reveal is different: it moves content. A timeline ignores
   animation-duration, so a global "every duration to 0.01ms" reset never
   reaches it. The reveal therefore carries its own motion gate. Fill mode
   none keeps the resting state visible, and the range ends once the row is
   fully in view. */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .row {
      animation: reveal linear none;
      animation-timeline: view();
      animation-range: entry 0% entry 100%;
    }
  }
}

@keyframes reveal {
  from { opacity: 0; translate: 0 1.5rem; }
}
```

## Container style queries · non-color cues

**Guard:** No guard needed.

```css
/* The toggle's state lives on the ancestor, set purely in CSS via :has(). */
.panel {
  --cues: off;
}
.panel:has(input:checked) {
  --cues: on;
}

/* A style query reads the inherited value and adds a non-colour SHAPE to each
   status — so meaning never rests on colour alone. The status elements never
   reference --cues; the query reaches them from their ancestor.

   Gotcha: keep pseudo-element rules OUT of the query — Safari doesn't apply
   them there. Let the query set a variable, and the pseudo render it. */
.status::before {
  content: var(--cue, none);
}

@container style(--cues: on) {
  .status--ok { --cue: '✓'; }
  .status--down { --cue: '✕'; }
}
```

## shape() responsive clipping

**Guard:** `@supports (clip-path: shape(from 0 0, line to 100% 0))`

```css
/* shape() draws in %/units, so the curve tracks the element at any width —
   the whole point of the demo. */
@supports (clip-path: shape(from 0 0, line to 100% 0)) {
  .banner-fluid {
    clip-path: shape(
      from 0 0,
      line to 100% 0,
      line to 100% 60%,
      curve to 0% 60% with 50% 100%,
      close
    );
  }
}

/* path()'s coordinates are absolute pixels, frozen at one size. The same
   curve only meets the edges at ~360px wide; resize and it detaches. This
   is what shape() fixes. */
.banner-frozen {
  clip-path: path('M0 0 L360 0 L360 58 C240 96 120 96 0 58 Z');
}

/* shape()'s two superpowers path() lacks: it reads custom properties, and it
   animates. Register the depth so it interpolates, feed it into the curve,
   and a class toggle morphs the whole silhouette on a transition. */
@property --wave {
  syntax: '<percentage>';
  inherits: true;
  initial-value: 78%;
}

.banner-morph {
  clip-path: shape(
    from 0 0,
    line to 100% 0,
    line to 100% 55%,
    curve to 50% 55% with 75% var(--wave),
    curve to 0% 55% with 25% calc(110% - var(--wave)),
    close
  );
}

.banner-morph.is-deep {
  --wave: 100%;
}

@media (prefers-reduced-motion: no-preference) {
  .banner-morph {
    transition: --wave 400ms;
  }
}
```

## content-visibility

**Guard:** `@supports (content-visibility: auto)`

```html
<article class="long-read">
  <section>
    <h2>Chapter one</h2>
    <p>Painted only when it approaches the viewport…</p>
  </section>
  <!-- …dozens more sections. Skipped, not hidden: still searchable
       with find-in-page, still in the tab order, still in the
       accessibility tree. display:none would take away all three. -->
</article>
```

```css
/* Skip the rendering work for off-screen sections — a long page
   paints fast with no virtual-scrolling JS, and nothing is taken
   from anyone: skipped content stays findable and announceable. */
@supports (content-visibility: auto) {
  .long-read section {
    content-visibility: auto;

    /* Estimated size for skipped sections, so the scrollbar
       doesn't lie while they're unrendered. */
    contain-intrinsic-block-size: auto 12rem;
  }
}
```

## Rounded polygon()

**Guard:** `@supports (clip-path: polygon(round 8px, 0 0, 100% 0, 50% 100%))`

```html
<!-- The ring lives on the <a>; the clip lives on the child. -->
<a class="spec-link" href="/spec">
  <span class="spec-tag">Read the spec</span>
</a>
```

```css
/* round softens every corner with one radius —
   per-corner control is shape()'s job. */
.spec-tag {
  display: inline-block;
  padding: 4px 26px 4px 12px;
  background: #dbe7ff;
  clip-path: polygon(
    round 8px,
    0 0,
    calc(100% - 14px) 0,
    100% 50%,
    calc(100% - 14px) 100%,
    0 100%
  );
}

/* Clip the child, never the focusable element: clip-path cuts away
   everything the element paints — a focus outline included. */
.spec-link:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 2px;
}

/* Better: a ring that follows the shape. Drop-shadows on the UNclipped
   parent trace the clipped child's silhouette — outline never can, and
   on a clipped link the clip runs after filters and cuts the ring away. */
@supports (clip-path: polygon(round 1px, 0 0, 1% 0, 0 1%)) {
  .spec-link:focus-visible {
    outline: none;
    filter: drop-shadow(2px 0 0 currentColor) drop-shadow(-2px 0 0 currentColor)
      drop-shadow(0 2px 0 currentColor) drop-shadow(0 -2px 0 currentColor);
  }
}

/* No support? A plain rounded chip, not a broken shape. */
@supports not (clip-path: polygon(round 1px, 0 0, 1% 0, 0 1%)) {
  .spec-tag {
    border-radius: 8px;
    padding-inline-end: 12px;
  }
}
```

## corner-shape

**Guard:** `@supports (corner-shape: squircle)`

```css
/* corner-shape reshapes the corner your border-radius already reaches:
   the radius sets how far in the corner starts, corner-shape sets the
   path it takes — squircle, scoop, notch, bevel. */
.corner-button {
  border: 1px solid #c9c9c9;
  border-radius: 16px;
}

/* Chrome/Edge 139+ for now. Guard it: everyone else simply keeps the
   plain rounded corner — a fallback, not a failure. */
@supports (corner-shape: squircle) {
  .corner-button.squircle { corner-shape: squircle; }
  .corner-button.scoop    { corner-shape: scoop; }
  .corner-button.notch    { corner-shape: notch; }
}

/* Per-corner longhands mix treatments on one box. Two full-height
   bevels on the same side meet in a point: the pointer-tag silhouette,
   cut with borders intact — no clip anywhere near it. */
.tag-button {
  border-radius: 0 1.5rem 1.5rem 0 / 0 50% 50% 0;
}

@supports (corner-shape: squircle) {
  .tag-button {
    corner-top-right-shape: bevel;
    corner-bottom-right-shape: bevel;
  }
}

/* The keywords are superellipse() curves underneath, so corner-shape
   interpolates: this hover morph sweeps squircle → bevel → scoop as one
   eased transition. Portable code carries its own motion guard — with
   reduced motion the shape still changes, it just stops animating. */
.morph-button {
  corner-shape: squircle;
  transition: corner-shape 0.4s ease;
}

.morph-button:hover,
.morph-button:focus-visible {
  corner-shape: scoop;
}

@media (prefers-reduced-motion: reduce) {
  .morph-button {
    transition: none;
  }
}

/* Real border geometry, so everything drawn along the box follows it —
   background, border, box-shadow, and the focus ring. Nothing bespoke
   here: any outline, the browser default included, hugs the new corner. */
.corner-button:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 2px;
}

/* The anti-pattern: faking the notch by clipping. clip-path applies
   AFTER the element paints — outline included — so the ring is thrown
   away wherever it falls outside the polygon. Clipped exactly to the
   box like this, an offset ring vanishes altogether; the live demo
   hangs its polygon 8px past each edge so you can watch the slice. */
.clipped-button {
  clip-path: polygon(
    0 16px, 16px 16px, 16px 0,
    calc(100% - 16px) 0, calc(100% - 16px) 16px, 100% 16px,
    100% calc(100% - 16px), calc(100% - 16px) calc(100% - 16px),
    calc(100% - 16px) 100%,
    16px 100%, 16px calc(100% - 16px), 0 calc(100% - 16px)
  );
}
```

## @starting-style

**Guard:** `@supports (transition-behavior: allow-discrete)`

```css
.card {
  /* Closed: out of layout and the accessibility tree. */
  display: none;
  opacity: 0;
  /* allow-discrete lets `display` participate, so the EXIT animates too. */
  transition:
    opacity 250ms,
    display 250ms allow-discrete;
}

.card.is-open {
  display: block;
  opacity: 1;

  /* The from-state for the ENTRY transition (display: none → shown). */
  @starting-style {
    opacity: 0;
  }
}

/* The same trick inside a breakpoint: crossing it FADES the panel
   in instead of popping it. */
.panel {
  display: none;
  opacity: 0;
  transition:
    opacity 250ms,
    display 250ms allow-discrete;
}

@container (inline-size >= 26rem) {
  .panel {
    display: grid;
    opacity: 1;

    @starting-style {
      opacity: 0;
    }
  }
}

/* Resize and device rotation fire this transition, so gate the
   MOTION — never the visibility. The panel must still appear for
   reduced-motion users; it just stops animating. */
@media (prefers-reduced-motion: reduce) {
  .panel {
    transition: none;
  }
}
```

## Typed attr()

**Guard:** `@supports (width: calc(attr(data-value type(<number>), 0) * 1%))`

```html
<!-- The value lives in the attribute — real text, so it stays accessible. -->
<span class="bar" data-value="92"></span>
```

```css
.bar {
  /* Fallback: a custom property mirrors the value until typed attr() ships. */
  inline-size: calc(var(--v, 0) * 1%);
}

/* The feature: read the attribute as a typed number, straight into the
   property — no inline style, no JS. */
@supports (width: calc(attr(data-value type(<number>), 0) * 1%)) {
  .bar {
    inline-size: calc(attr(data-value type(<number>), 0) * 1%);
  }
}
```

## field-sizing: content

**Guard:** `@supports (field-sizing: content)`

```html
<textarea class="note" rows="2"></textarea>
```

```css
.note {
  /* Fallback: a fixed box you can drag to resize (rows sets the start). */
  resize: vertical;
}

@supports (field-sizing: content) {
  .note {
    /* Grow and shrink with the content — no scrollHeight measuring, no resize
       listeners, no JS — bounded so it can't run away. */
    field-sizing: content;
    min-block-size: 2lh;
    max-block-size: 8lh;
    resize: none;
  }
}
```

## CSS zoom

**Guard:** `@supports (zoom: 2)`

```css
/* Magnify a component AND reflow the layout around it — the accessible
   kind of zoom (WCAG 1.4.10), unlike transform: scale(). */
@supports (zoom: 2) {
  .magnify {
    zoom: 2;
  }
}
```

## Customizable <select>

**Guard:** `@supports (appearance: base-select)`

```html
<!-- A real native <select>: keyboard, type-ahead, the screen-reader
     combobox and form submission all keep working — no JS. -->
<select name="status" class="status-select">
  <button type="button">
    <selectedcontent></selectedcontent>
  </button>

  <optgroup label="Open">
    <option value="backlog">Backlog</option>
    <option value="in-progress" selected>In progress</option>
  </optgroup>
  <optgroup label="Closed">
    <option value="done">Done</option>
    <option value="archived" disabled>Archived</option>
  </optgroup>
</select>
```

```css
/* Opt the control and its dropdown into full styling. Where unsupported,
   the markup stays an ordinary, fully-usable native <select>. */
@supports (appearance: base-select) {
  .status-select,
  .status-select::picker(select) {
    appearance: base-select;
  }

  /* The popup enters with a small rise — no JS, just transitions. */
  .status-select::picker(select) {
    padding: 4px;
    border: 1px solid #d4d4d8;
    border-radius: 8px;
    background: #fff;
    opacity: 0;
    translate: 0 -4px;
    transition: opacity 0.15s, translate 0.15s, display 0.15s allow-discrete;
  }

  .status-select:open::picker(select) {
    opacity: 1;
    translate: 0 0;
  }

  @starting-style {
    .status-select:open::picker(select) {
      opacity: 0;
      translate: 0 -4px;
    }

    /* The built-in ✓ pops in when the choice changes. */
    .status-select option:checked::checkmark {
      opacity: 0;
      scale: 0.4;
    }
  }

  .status-select option {
    padding: 8px 12px;
  }

  /* The ✓ marks the current value too, so this isn't colour alone. */
  .status-select option:checked {
    background: color-mix(in oklch, royalblue 14%, transparent);
  }

  .status-select option::checkmark {
    color: royalblue;
    transition: opacity 0.15s, scale 0.15s;
  }

  /* WebKit keeps the chosen value on one line and clips it;
     let a long value wrap, as Chromium does. */
  .status-select selectedcontent {
    white-space: normal;
  }

  /* The closed control reflects the selection — reinforcement only;
     <selectedcontent> mirrors the option's own text. */
  .status-select:has(option[value='done']:checked) button {
    border-color: seagreen;
  }
}
```

## Scroll-state queries

**Guard:** `@supports (container-type: scroll-state)`

```css
/* The card becomes a scroll-state query container… */
.card {
  container-type: scroll-state;
  scroll-snap-align: center;
}

/* …so its contents restyle when the card is the snapped one — no scroll
   listeners, no JS. scroll-state(stuck: top) works the same for a pinned
   sticky header. */
@supports (container-type: scroll-state) {
  @container scroll-state(snapped: inline) {
    .card-inner {
      border-color: royalblue;
      scale: 1.03;
    }
  }
}

/* The scroller is a container too; two sticky markers inside it carry
   the edge shades, and the query switches each one on only while there
   is more to scroll that way: a hint that never lies at the end. */
.strip {
  display: flex;
  overflow-x: auto;
  container-type: scroll-state;
}

.hint {
  position: sticky;
  flex: 0 0 0;
  opacity: 0;
}

.hint-end {
  inset-inline-end: 0;
}

@supports (container-type: scroll-state) {
  @container scroll-state(scrollable: inline-end) {
    .hint-end { opacity: 1; }
  }
}

/* scroll-state(scrolled: …), Chrome 144+, can slide a bar away while the
   reader scrolls down. The page scroller is the container here. The bar's
   links stay in the tab order, so give it a way back: without the last
   rule, keyboard focus lands on a link that is off screen. */
html {
  container-type: scroll-state;
}

@supports (container-type: scroll-state) {
  @container scroll-state(scrolled: bottom) {
    .bar { translate: 0 100%; }
  }

  .bar:focus-within { translate: 0 0; }
}
```

## Pure-CSS carousel

**Guard:** `@supports (selector(::scroll-marker))`

```css
/* The baseline for EVERY browser: a focusable snap scroller.
   Buttons and dots below are enhancement on top of this. */
.carousel {
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-marker-group: after;
}

/* Generated, focusable, auto-disabling buttons — the text after
   the slash is the accessible name. Never ship them nameless. */
.carousel::scroll-button(inline-end) {
  content: '›' / 'Next slides';
}

.carousel::scroll-button(inline-start) {
  content: '‹' / 'Previous slides';
}

/* One dot per slide; the group is keyboard-navigable by itself. */
.slide::scroll-marker {
  content: ' ' / attr(data-marker-label);
}

.slide::scroll-marker:target-current {
  background: var(--color-primary);
}
```

## Custom Highlight API

**Guard:** Feature-detect in JS — no CSS condition expresses it.

```css
/* Style the named highlight — no wrapper <span>s injected, the accessibility
   tree stays untouched. ::highlight() is a global pseudo-element, so it isn't
   tied to any one element. */
::highlight(search) {
  background: royalblue;
  color: #fff;
}
```

```js
// Build Range objects over the matches, then register them under a name.
// The ::highlight(search) rule paints them — no DOM mutation.
const ranges = findMatchRanges(container, query) // your match-finding
CSS.highlights.set('search', new Highlight(...ranges))

// Presentation only: because it never touches the DOM or a11y tree, it must
// never be the sole way meaning is conveyed.
```

## Dialog & popover dismissal

**Guard:** `@supports (selector(:open))`

```html
<!-- Toggletip: showing, dismissing, and top-layer rendering all come free. -->
<button type="button" popovertarget="tip" aria-label="What does closedby do?">?</button>
<div id="tip" popover="hint" role="status">
  closedby sets how a dialog can be dismissed.
</div>

<!-- Takeover: Esc and outside-clicks can't close it. Use sparingly —
     trapping people is normally a trap to avoid. -->
<dialog closedby="none">…</dialog>
```

```css
/* Style the dialog only while open, via the new :open pseudo-class. */
dialog:open {
  opacity: 1;
  scale: 1;
  transition:
    opacity 250ms,
    scale 250ms;

  /* Entry from display: none. Motion tokens would zero this under
     reduced motion; here it's a literal duration for portability. */
  @starting-style {
    opacity: 0;
    scale: 0.96;
  }
}
```

## Invoker commands

**Guard:** Feature-detect in JS — no CSS condition expresses it.

```html
<!-- The button names its target (commandfor) and the action (command).
     No JS anywhere: focus moves in, Esc closes, focus returns to the trigger. -->
<button type="button" commandfor="dlg" command="show-modal">Open the dialog</button>

<dialog id="dlg">
  <p>Top layer, backdrop, and focus handling — all platform-provided.</p>
  <!-- Closing is declarative too. -->
  <button type="button" commandfor="dlg" command="close">Close</button>
</dialog>

<!-- The same attribute pair drives popovers. -->
<button type="button" commandfor="card" command="toggle-popover">Toggle the popover</button>
<div id="card" popover>Esc and light dismiss included.</div>

<!-- Escape hatch: command="request-close" fires a cancelable close request
     first, for the rare dialog that needs to confirm before closing. -->
```

## Interest invokers (interestfor)

**Guard:** `@supports (interest-delay: 0s)`

```html
<!-- One attribute wires hover / keyboard focus / touch long-press
     to the popover — delays, Esc dismissal, and a11y mapping included. -->
<a href="/spec" interestfor="preview">Read the spec</a>

<div id="preview" popover="hint">
  <strong>The spec</strong>
  A short preview of where this link goes.
</div>
```

```css
/* Politeness dial: wait a beat before showing, linger before hiding. */
[interestfor] {
  interest-delay: 0.4s 0.2s;
}

/* Once one hint in a toolbar is open, its neighbours open instantly. */
.toolbar:has(:interest-source) [interestfor] {
  interest-delay-start: 0s;
}

/* Style either side of the relationship while interest is shown. */
a:interest-source {
  background-color: color-mix(in oklab, currentcolor 12%, transparent);
}
```

## View Transitions

**Guard:** Feature-detect in JS — no CSS condition expresses it.

```css
/* Each shared element gets a unique, stable name, so the browser can match it
   across the DOM change and tween its position + size. */
.item-tokens { view-transition-name: vt-tokens; }
.item-layers { view-transition-name: vt-layers; }
```

```js
// Wrap the DOM change; the browser captures before/after and tweens the morph
// — no FLIP math, no animation library. Guard it so the swap stays instant
// under reduced motion and where the API isn't supported.
function setView(next) {
  const apply = () => render(next)
  if (document.startViewTransition && !prefersReducedMotion()) {
    document.startViewTransition(apply)
  } else {
    apply()
  }
}
```

## reading-flow: flex-visual

**Guard:** `@supports (reading-flow: flex-visual)`

```html
<ul class="gallery">
  <li><button>Update 1</button></li>
  <li><button>Update 2</button></li>
  <li><button>Update 3</button></li>
  <li class="featured"><button>Update 4</button></li>
</ul>
```

```css
.gallery {
  display: flex;
  flex-wrap: wrap;
}

/* Visual promotion — the DOM stays chronological. */
.featured {
  order: -1;
}

/* Without this, Tab follows the DOM: 1 → 2 → 3 → jump back to 4.
   With it, sequential focus (and assistive-tech reading order)
   follows the visual arrangement: 4 → 1 → 2 → 3. */
@supports (reading-flow: flex-visual) {
  .gallery {
    reading-flow: flex-visual;
  }
}
```

## ::details-content

**Guard:** `@supports (selector(::details-content))`

```html
<details>
  <summary>Does an accordion need JavaScript?</summary>
  <p>Not anymore — this is a native details element.</p>
</details>
```

```css
/* Style the browser's own content region; allow-discrete keeps the
   content rendered until the closing fade finishes. */
details::details-content {
  opacity: 0;
  block-size: 0;
  overflow-y: clip;
  transition:
    content-visibility 0.25s allow-discrete,
    opacity 0.25s,
    block-size 0.25s;
}

details[open]::details-content {
  opacity: 1;
  block-size: auto;
}

/* Where supported, block-size animates to the real auto height;
   elsewhere the content still fades. */
details {
  interpolate-size: allow-keywords;
}

@media (prefers-reduced-motion: reduce) {
  details::details-content {
    transition: none;
  }
}
```
