The Lab

The platform, up close.

Each experiment isolates one modern feature — running live, with its source and an honest note on where it works today. Every demo degrades gracefully.

01

Scroll-driven animations

Tie an animation's progress to scroll position (scroll()) or an element's visibility (view()) — running on the compositor, so it stays smooth even when the main thread is busy. Scroll the box.

animation-timelineNewly available

↓ keep scrolling

Compositor-threaded
No scroll listeners
No layout thrash
Declarative ranges
Respects reduced-motion
Falls back gracefully

fin.

scroll() drives the top bar · view() reveals each row

Support: Chromium 115+ and Safari 26 ship scroll() and view() timelines; Firefox is rolling them out. Because they're declared in @supports and CSS, unsupported browsers simply show the static end-state — no JavaScript fallback needed.

View source
lab/scroll-driven-animations.astro
---
export const meta = {
  title: "Scroll-driven animations",
  feature: "animation-timeline",
  baseline: "Newly available",
  support: "Chromium 115+ and Safari 26 ship scroll() and view() timelines; Firefox is rolling them out. Because they're declared in @supports and CSS, unsupported browsers simply show the static end-state — no JavaScript fallback needed.",
  description:
    "Tie an animation's progress to scroll position (scroll()) or an element's visibility (view()) — running on the compositor, so it stays smooth even when the main thread is busy. Scroll the box.",
  order: 1,
};

const rows = ["Compositor-threaded", "No scroll listeners", "No layout thrash", "Declarative ranges", "Respects reduced-motion", "Falls back gracefully"];
---
<div class="sda">
  <div class="sda__scroller">
    <div class="sda__progress" aria-hidden="true"></div>
    <div class="sda__inner">
      <p class="sda__hint">↓ keep scrolling</p>
      {rows.map((r, i) => (
        <div class="sda__row" style={`--i:${i}`}><span class="sda__dot"></span>{r}</div>
      ))}
      <p class="sda__end">fin.</p>
    </div>
  </div>
  <p class="sda__label mono">scroll() drives the top bar · view() reveals each row</p>
</div>

<style>
  .sda { padding: var(--space-l); display: grid; gap: 0.8rem; }
  .sda__scroller {
    position: relative; height: 300px; overflow-y: auto; overscroll-behavior: contain;
    border: 1px solid var(--border); border-radius: var(--radius-l); background: var(--surface);
    scroll-timeline: --box y;
  }
  .sda__progress {
    position: sticky; top: 0; left: 0; height: 4px; width: 100%; transform-origin: 0 50%; transform: scaleX(0); z-index: 2;
    background: linear-gradient(90deg, var(--spectrum-1, #8b5cf6), var(--spectrum-3, #45d3e8), var(--spectrum-5, #fbbf24));
  }
  .sda__inner { padding: 1.2rem 1.4rem 2rem; display: grid; gap: 1rem; }
  .sda__hint, .sda__end { color: var(--ink-faint); text-align: center; margin: 0.4rem 0; }
  .sda__end { padding-top: 8rem; }
  .sda__row {
    display: flex; align-items: center; gap: 0.8rem; font-size: var(--step-1); font-weight: 600;
    padding: 0.9rem 1.1rem; border-radius: var(--radius-m); background: var(--surface-2); border: 1px solid var(--border);
  }
  .sda__dot { width: 10px; height: 10px; border-radius: 50%; background: var(--spectrum-3, #45d3e8); flex: none; }

  @supports (animation-timeline: scroll()) {
    @media (prefers-reduced-motion: no-preference) {
      .sda__progress { animation: sda-grow linear both; animation-timeline: --box; }
      .sda__row { animation: sda-in linear both; animation-timeline: view(--box); animation-range: entry 5% cover 40%; }
    }
  }
  @keyframes sda-grow { to { transform: scaleX(1); } }
  @keyframes sda-in { from { opacity: 0; transform: translateX(-24px) scale(0.96); } to { opacity: 1; transform: none; } }

  .sda__label { font-size: var(--step--1); color: var(--ink-muted); text-align: center; }
</style>
02

View Transitions

Snapshot the page, mutate the DOM, and let the browser tween between the two states — including shared elements that morph across layouts. Shuffle the grid, or open a tile.

document.startViewTransition()Baseline
click a tile to expand

Support: Same-document view transitions are Baseline across Chromium, Safari and Firefox. The browser snapshots before/after states and animates the difference; where it's missing, startViewTransition() runs the callback synchronously with no animation, so nothing breaks.

View source
lab/view-transitions.astro
---
export const meta = {
  title: "View Transitions",
  feature: "document.startViewTransition()",
  baseline: "Baseline",
  support: "Same-document view transitions are Baseline across Chromium, Safari and Firefox. The browser snapshots before/after states and animates the difference; where it's missing, startViewTransition() runs the callback synchronously with no animation, so nothing breaks.",
  description:
    "Snapshot the page, mutate the DOM, and let the browser tween between the two states — including shared elements that morph across layouts. Shuffle the grid, or open a tile.",
  order: 2,
};

const tiles = [
  { id: "a", label: "Nebula", hue: "265" },
  { id: "b", label: "Lagoon", hue: "190" },
  { id: "c", label: "Meadow", hue: "150" },
  { id: "d", label: "Ember", hue: "35" },
  { id: "e", label: "Bloom", hue: "350" },
  { id: "f", label: "Cobalt", hue: "220" },
];
---
<div class="vt" data-vt>
  <div class="vt__bar">
    <button class="vt__btn" data-shuffle>⤨ Shuffle</button>
    <span class="vt__note mono">click a tile to expand</span>
  </div>
  <div class="vt__grid" data-grid>
    {tiles.map((t) => (
      <button class="vt__tile" data-tile={t.id} style={`--h:${t.hue}; view-transition-name: vt-${t.id};`}>
        <span>{t.label}</span>
      </button>
    ))}
  </div>

  <div class="vt__detail" data-detail hidden>
    <button class="vt__tile vt__tile--big" data-detail-tile></button>
    <button class="vt__close" data-close>← back to grid</button>
  </div>
</div>

<style>
  .vt { padding: var(--space-l); display: grid; gap: 1rem; }
  .vt__bar { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
  .vt__btn { padding: 0.5rem 1rem; border-radius: var(--radius-round); border: 1px solid var(--border-strong); background: var(--surface); color: var(--ink); font-weight: 600; }
  .vt__btn:hover { border-color: var(--accent); }
  .vt__note { font-size: var(--step--1); color: var(--ink-muted); }
  .vt__grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.8rem; }
  /* attribute selector out-specifies the display rules so [hidden] actually hides */
  .vt__grid[hidden], .vt__detail[hidden] { display: none; }
  .vt__tile {
    aspect-ratio: 4 / 3; border-radius: var(--radius-l); border: none; cursor: pointer;
    color: #fff; font-weight: 700; font-size: var(--step-1); display: grid; place-items: end start; padding: 0.9rem;
    background: linear-gradient(150deg, oklch(70% 0.18 var(--h)), oklch(52% 0.16 calc(var(--h) + 40)));
    box-shadow: var(--shadow-m);
  }
  .vt__tile span { text-shadow: 0 1px 8px rgba(0,0,0,0.4); }
  .vt__detail { display: grid; gap: 1rem; justify-items: start; }
  .vt__tile--big { width: 100%; aspect-ratio: 21 / 9; font-size: var(--step-4); place-items: center; }
  .vt__close { padding: 0.5rem 1rem; border-radius: var(--radius-round); border: 1px solid var(--border); background: var(--surface); color: var(--ink); font-weight: 600; }

  ::view-transition-group(*) { animation-duration: 0.4s; animation-timing-function: var(--ease-emph, cubic-bezier(0.2,0,0,1)); }
  @media (prefers-reduced-motion: reduce) { ::view-transition-group(*) { animation-duration: 0.001s; } }
</style>

<script>
  (function () {
    function bind() {
      document.querySelectorAll<HTMLElement>("[data-vt]").forEach((root) => {
        if (root.dataset.bound) return;
        root.dataset.bound = "1";
        const grid = root.querySelector<HTMLElement>("[data-grid]")!;
        const detail = root.querySelector<HTMLElement>("[data-detail]")!;
        const bigTile = root.querySelector<HTMLButtonElement>("[data-detail-tile]")!;
        const closeBtn = root.querySelector<HTMLButtonElement>("[data-close]")!;
        const run = (fn: () => void) =>
          (document as any).startViewTransition && !matchMedia("(prefers-reduced-motion: reduce)").matches
            ? (document as any).startViewTransition(fn)
            : fn();

        root.querySelectorAll<HTMLButtonElement>("[data-tile]").forEach((tile) => {
          tile.addEventListener("click", () => {
            const id = tile.dataset.tile!;
            run(() => {
              tile.style.viewTransitionName = "";
              bigTile.style.cssText = tile.style.cssText + `; view-transition-name: vt-${id};`;
              bigTile.textContent = tile.textContent;
              bigTile.dataset.from = id;
              grid.hidden = true; detail.hidden = false;
            });
            // move focus out of the now-hidden grid so keyboard users keep their place
            closeBtn.focus();
          });
        });

        closeBtn.addEventListener("click", () => {
          const id = bigTile.dataset.from!;
          const orig = grid.querySelector<HTMLElement>(`[data-tile="${id}"]`)!;
          run(() => {
            bigTile.style.viewTransitionName = "";
            orig.style.viewTransitionName = `vt-${id}`;
            detail.hidden = true; grid.hidden = false;
          });
          orig.focus();
        });

        root.querySelector<HTMLButtonElement>("[data-shuffle]")!.addEventListener("click", () => {
          const items = [...grid.children];
          for (let i = items.length - 1; i > 0; i--) {
            const j = Math.floor(Math.random() * (i + 1));
            [items[i], items[j]] = [items[j], items[i]];
          }
          run(() => items.forEach((el) => grid.appendChild(el)));
        });
      });
    }
    document.addEventListener("astro:page-load", bind);
    if (document.readyState !== "loading") bind();
  })();
</script>
03

CSS Anchor Positioning

Tether one element to another anywhere in the DOM and position it with anchor() — no wrapper, no JS measurement. The popover stays glued to its button and flips when it runs out of room.

anchor() / position-anchorLimited

Hover or focus a node — its label anchors to it purely in CSS.

Support: Shipping in Chromium 125+. Not yet in Safari or Firefox (in development). This demo uses @supports so browsers without it fall back to normal flow tooltips positioned the old way — still perfectly usable.

View source
lab/anchor-positioning.astro
---
export const meta = {
  title: "CSS Anchor Positioning",
  feature: "anchor() / position-anchor",
  baseline: "Limited",
  support: "Shipping in Chromium 125+. Not yet in Safari or Firefox (in development). This demo uses @supports so browsers without it fall back to normal flow tooltips positioned the old way — still perfectly usable.",
  description:
    "Tether one element to another anywhere in the DOM and position it with anchor() — no wrapper, no JS measurement. The popover stays glued to its button and flips when it runs out of room.",
  order: 3,
};

const spots = [
  { id: "n", label: "North", side: "top" },
  { id: "e", label: "East", side: "right" },
  { id: "s", label: "South", side: "bottom" },
  { id: "w", label: "West", side: "left" },
];
---
<div class="anc">
  <p class="anc__hint text-muted">Hover or focus a node — its label anchors to it purely in CSS.</p>
  <div class="anc__stage">
    {spots.map((s) => (
      <div class="anc__pair">
        <button class="anc__node" style={`anchor-name: --a-${s.id};`}>{s.label[0]}</button>
        <div class={`anc__tip anc__tip--${s.side}`} style={`position-anchor: --a-${s.id};`} role="tooltip">
          {s.label} · anchored {s.side}
        </div>
      </div>
    ))}
  </div>
</div>

<style>
  .anc { padding: var(--space-xl) var(--space-l); display: grid; gap: 1.5rem; justify-items: center; }
  .anc__hint { text-align: center; }
  .anc__stage { display: flex; flex-wrap: wrap; gap: 3rem; justify-content: center; padding: 2rem 0; }
  .anc__pair { position: relative; }
  .anc__node {
    width: 3.4rem; height: 3.4rem; border-radius: 50%; font-weight: 800; font-size: var(--step-1); color: #fff;
    background: radial-gradient(circle at 30% 30%, var(--spectrum-3, #45d3e8), var(--spectrum-1, #8b5cf6));
    border: none; box-shadow: var(--shadow-m);
  }
  .anc__tip {
    width: max-content; max-width: 14rem; font-size: var(--step--1); font-weight: 600;
    padding: 0.45rem 0.75rem; border-radius: var(--radius-m); background: var(--ink); color: var(--ink-invert);
    opacity: 0; visibility: hidden; transition: opacity var(--dur) var(--ease-out); pointer-events: none; z-index: 5;
  }
  .anc__pair:hover .anc__tip, .anc__pair:focus-within .anc__tip { opacity: 1; visibility: visible; }

  /* Fallback (no anchor positioning): absolutely position around the node's own pair */
  .anc__tip { position: absolute; left: 50%; top: 50%; translate: -50% -50%; }
  .anc__tip--top { top: -0.6rem; translate: -50% -100%; }
  .anc__tip--bottom { top: calc(100% + 0.6rem); translate: -50% 0; }
  .anc__tip--left { left: -0.6rem; top: 50%; translate: -100% -50%; }
  .anc__tip--right { left: calc(100% + 0.6rem); top: 50%; translate: 0 -50%; }

  @supports (anchor-name: --x) {
    .anc__tip { position: fixed; left: auto; top: auto; translate: 0 0; margin: 0.55rem; }
    .anc__tip--top    { position-area: top;    }
    .anc__tip--bottom { position-area: bottom; }
    .anc__tip--left   { position-area: left;   }
    .anc__tip--right  { position-area: right;  }
    .anc__tip { position-try-fallbacks: flip-block, flip-inline; }
  }
</style>
04

The :has() parent selector

Select an element by its descendants or siblings. Here, choosing a plan restyles the whole card, and the summary bar reacts to which options are checked — all in pure CSS.

:has()Baseline
Choose a plan
Add-ons
Your selections light up this bar — it turns green only when every add-on is checked.

Support: Baseline across Chromium, Safari and Firefox since late 2023. One of the most impactful CSS additions in years — style an element based on what it contains, or on the state of a sibling, with zero JavaScript.

View source
lab/has-selector.astro
---
export const meta = {
  title: "The :has() parent selector",
  feature: ":has()",
  baseline: "Baseline",
  support: "Baseline across Chromium, Safari and Firefox since late 2023. One of the most impactful CSS additions in years — style an element based on what it contains, or on the state of a sibling, with zero JavaScript.",
  description:
    "Select an element by its descendants or siblings. Here, choosing a plan restyles the whole card, and the summary bar reacts to which options are checked — all in pure CSS.",
  order: 4,
};

const plans = [
  { id: "free", name: "Hobby", price: "Free" },
  { id: "pro", name: "Pro", price: "$12/mo" },
  { id: "team", name: "Team", price: "$49/mo" },
];
const addons = ["Priority support", "Custom domain", "Analytics"];
---
<form class="has" data-has>
  <fieldset class="has__plans">
    <legend class="has__legend">Choose a plan</legend>
    {plans.map((p, i) => (
      <label class="has__plan">
        <input type="radio" name="plan" value={p.id} checked={i === 1} />
        <span class="has__plan-name">{p.name}</span>
        <span class="has__plan-price">{p.price}</span>
        <span class="has__check" aria-hidden="true">✓</span>
      </label>
    ))}
  </fieldset>

  <fieldset class="has__addons">
    <legend class="has__legend">Add-ons</legend>
    {addons.map((a, i) => (
      <label class="has__addon">
        <input type="checkbox" checked={i === 0} />
        <span>{a}</span>
      </label>
    ))}
  </fieldset>

  <output class="has__summary">Your selections light up this bar — it turns green only when every add-on is checked.</output>
</form>

<style>
  .has { padding: var(--space-l); display: grid; gap: 1.2rem; }
  .has__legend { font-weight: 700; margin-bottom: 0.6rem; color: var(--ink); }
  fieldset { border: none; padding: 0; margin: 0; }

  .has__plans { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.7rem; }
  .has__plan {
    position: relative; display: grid; gap: 0.2rem; padding: 1rem; cursor: pointer;
    border-radius: var(--radius-l); border: 1.5px solid var(--border); background: var(--surface);
    transition: border-color var(--dur) var(--ease-out), transform var(--dur) var(--ease-out), box-shadow var(--dur) var(--ease-out);
  }
  .has__plan input { position: absolute; opacity: 0; }
  .has__plan-name { font-weight: 700; }
  .has__plan-price { color: var(--ink-muted); font-size: var(--step--1); }
  .has__check { position: absolute; top: 0.7rem; right: 0.8rem; opacity: 0; color: var(--spectrum-4, #34d399); font-weight: 800; }
  /* the winning trick: style the label because IT HAS a checked input */
  .has__plan:has(input:checked) {
    border-color: var(--accent, #8b5cf6); transform: translateY(-2px); box-shadow: var(--glow);
    background: color-mix(in oklab, var(--accent, #8b5cf6) 10%, var(--surface));
  }
  .has__plan:has(input:checked) .has__check { opacity: 1; }
  .has__plan:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }

  .has__addons { display: flex; flex-wrap: wrap; gap: 0.6rem; }
  .has__addon { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.9rem; border-radius: var(--radius-round); border: 1px solid var(--border); background: var(--surface); cursor: pointer; }
  .has__addon:has(input:checked) { border-color: var(--spectrum-4, #34d399); color: var(--spectrum-4, #34d399); }

  .has__summary {
    padding: 0.9rem 1.1rem; border-radius: var(--radius-m); font-size: var(--step--1);
    border: 1px solid var(--border); background: var(--surface-2); color: var(--ink-muted);
    transition: background var(--dur) var(--ease-out), color var(--dur) var(--ease-out), border-color var(--dur) var(--ease-out);
  }
  /* form-level :has() — all three add-ons checked */
  .has:has(.has__addon:nth-of-type(1) input:checked):has(.has__addon:nth-of-type(2) input:checked):has(.has__addon:nth-of-type(3) input:checked) .has__summary {
    background: color-mix(in oklab, var(--spectrum-4, #34d399) 16%, transparent);
    border-color: var(--spectrum-4, #34d399); color: var(--ink);
  }
</style>
05

Container Queries

Drag the handle to resize the container. The card inside re-lays-out based on its own width — from stacked, to row, to feature layout — with no viewport media queries.

@containerBaseline

↔ Drag the right edge to resize the container

Field notes

Adaptive by container, not viewport

This card reads its own width and rearranges. Resize me and watch the layout snap between breakpoints.

Support: Baseline across all modern browsers since 2023. Components respond to the size of their container rather than the viewport — so the same card is correct in a sidebar, a grid, or full-width without media-query guesswork.

View source
lab/container-queries.astro
---
export const meta = {
  title: "Container Queries",
  feature: "@container",
  baseline: "Baseline",
  support: "Baseline across all modern browsers since 2023. Components respond to the size of their container rather than the viewport — so the same card is correct in a sidebar, a grid, or full-width without media-query guesswork.",
  description:
    "Drag the handle to resize the container. The card inside re-lays-out based on its own width — from stacked, to row, to feature layout — with no viewport media queries.",
  order: 5,
};
---
<div class="cq">
  <p class="cq__hint text-muted">↔ Drag the right edge to resize the container</p>
  <div class="cq__resizer">
    <article class="cq__card">
      <div class="cq__media" aria-hidden="true"></div>
      <div class="cq__body">
        <span class="cq__tag">Field notes</span>
        <h3>Adaptive by container, not viewport</h3>
        <p>This card reads its own width and rearranges. Resize me and watch the layout snap between breakpoints.</p>
        <button class="cq__btn">Read more →</button>
      </div>
    </article>
  </div>
</div>

<style>
  .cq { padding: var(--space-l); display: grid; gap: 0.9rem; }
  .cq__hint { text-align: center; }
  .cq__resizer {
    resize: horizontal; overflow: auto; min-width: 240px; max-width: 100%; width: 100%;
    padding: 0.5rem; border: 1px dashed var(--border-strong); border-radius: var(--radius-l); background: var(--bg-sunk);
    container-type: inline-size; container-name: card;
  }
  .cq__card { display: grid; gap: 1rem; padding: 1rem; border-radius: var(--radius-l); background: var(--surface); border: 1px solid var(--border); }
  .cq__media { border-radius: var(--radius-m); min-height: 8rem; background: linear-gradient(135deg, var(--spectrum-1, #8b5cf6), var(--spectrum-3, #45d3e8), var(--spectrum-5, #fbbf24)); }
  .cq__body { display: grid; gap: 0.5rem; align-content: start; }
  .cq__tag { font-size: var(--step--1); font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--accent, #8b5cf6); }
  .cq__card h3 { font-size: var(--step-2); margin: 0; }
  .cq__card p { color: var(--ink-muted); margin: 0; }
  .cq__btn { justify-self: start; padding: 0.5rem 1rem; border-radius: var(--radius-round); border: 1px solid var(--border-strong); background: var(--surface); color: var(--ink); font-weight: 600; }

  /* wider than 30rem: media beside body */
  @container card (min-width: 30rem) {
    .cq__card { grid-template-columns: 14rem 1fr; align-items: center; }
    .cq__media { min-height: 100%; height: 100%; }
  }
  /* wider than 46rem: bigger type + feature spacing */
  @container card (min-width: 46rem) {
    .cq__card { grid-template-columns: 20rem 1fr; padding: 1.6rem; gap: 1.6rem; }
    .cq__card h3 { font-size: var(--step-3); }
    .cq__media { min-height: 12rem; }
  }
</style>
06

Popover API & <dialog>

Declarative overlays that render in the top layer, above everything, with built-in accessibility. A menu via popover attributes, and a true modal via <dialog>, plus @starting-style entrance animations.

popover / <dialog>Baseline

Quick actions


Native modal dialog

Focus is trapped, the background is inert, and Esc closes it — all handled by the platform.

Support: The Popover API and <dialog> are Baseline across modern browsers. You get top-layer rendering, light-dismiss, focus management and Escape-to-close for free — no library, no focus-trap code.

View source
lab/popover-dialog.astro
---
export const meta = {
  title: "Popover API & <dialog>",
  feature: "popover / <dialog>",
  baseline: "Baseline",
  support: "The Popover API and <dialog> are Baseline across modern browsers. You get top-layer rendering, light-dismiss, focus management and Escape-to-close for free — no library, no focus-trap code.",
  description:
    "Declarative overlays that render in the top layer, above everything, with built-in accessibility. A menu via popover attributes, and a true modal via <dialog>, plus @starting-style entrance animations.",
  order: 6,
};
---
<div class="pop">
  <div class="pop__row">
    <button class="pop__btn" popovertarget="lab-menu" style="anchor-name: --menu">Open popover menu ▾</button>
    <button class="pop__btn pop__btn--solid" data-open-dialog>Open modal dialog</button>
  </div>

  <div id="lab-menu" popover class="pop__menu">
    <p class="pop__menu-title">Quick actions</p>
    <button class="pop__item">◱ Duplicate</button>
    <button class="pop__item">✎ Rename</button>
    <button class="pop__item">⇪ Share</button>
    <hr />
    <button class="pop__item pop__item--danger">⌫ Delete</button>
  </div>

  <dialog class="pop__dialog" data-dialog aria-labelledby="pop-dialog-title">
    <div class="pop__dialog-glow" aria-hidden="true"></div>
    <h3 id="pop-dialog-title">Native modal dialog</h3>
    <p>Focus is trapped, the background is inert, and <kbd>Esc</kbd> closes it — all handled by the platform.</p>
    <form method="dialog" class="pop__dialog-actions">
      <button class="pop__btn" value="cancel">Cancel</button>
      <button class="pop__btn pop__btn--solid" value="ok">Got it</button>
    </form>
  </dialog>
</div>

<style>
  .pop { padding: var(--space-xl) var(--space-l); display: grid; place-items: center; gap: 1rem; }
  .pop__row { display: flex; flex-wrap: wrap; gap: 0.8rem; justify-content: center; }
  .pop__btn { padding: 0.65rem 1.2rem; border-radius: var(--radius-round); border: 1px solid var(--border-strong); background: var(--surface); color: var(--ink); font-weight: 600; }
  .pop__btn:hover { border-color: var(--accent); }
  .pop__btn--solid { background: var(--accent, #8b5cf6); color: oklch(20% 0.03 265); border-color: transparent; }

  .pop__menu {
    margin: 0; inset: auto; padding: 0.5rem; border: 1px solid var(--border-strong); border-radius: var(--radius-l);
    background: var(--glass); backdrop-filter: blur(16px); box-shadow: var(--shadow-l); min-width: 12rem;
  }
  /* Where anchor positioning is supported, tether the menu under its trigger. */
  @supports (anchor-name: --x) {
    .pop__menu {
      position-anchor: --menu; position-area: bottom span-right; margin-top: 0.5rem;
      position-try-fallbacks: flip-block;
    }
  }
  .pop__menu-title { font-size: var(--step--1); color: var(--ink-muted); padding: 0.3rem 0.6rem; }
  .pop__item { display: block; width: 100%; text-align: left; padding: 0.5rem 0.6rem; border-radius: var(--radius-s); color: var(--ink); font-weight: 500; }
  .pop__item:hover { background: color-mix(in oklab, var(--ink) 8%, transparent); }
  .pop__item--danger { color: var(--spectrum-6, #f55c8f); }
  .pop__menu hr { margin: 0.3rem 0; border-color: var(--border); }

  .pop__dialog {
    margin: auto; border: 1px solid var(--border-strong); border-radius: var(--radius-xl); padding: 1.6rem;
    background: var(--surface); color: var(--ink); max-width: min(90vw, 28rem); overflow: hidden; position: relative;
  }
  .pop__dialog h3 { margin: 0 0 0.4rem; font-size: var(--step-2); }
  .pop__dialog p { color: var(--ink-muted); margin: 0 0 1.2rem; }
  .pop__dialog-glow { position: absolute; inset: -40% 40% auto -20%; height: 10rem; background: radial-gradient(closest-side, color-mix(in oklab, var(--spectrum-1, #8b5cf6) 40%, transparent), transparent); filter: blur(30px); }
  .pop__dialog-actions { display: flex; gap: 0.6rem; justify-content: flex-end; }
  kbd { font-family: var(--font-mono); background: var(--surface-2); padding: 0.05em 0.4em; border-radius: 4px; border: 1px solid var(--border); }

  /* top-layer entrance with @starting-style */
  .pop__menu, .pop__dialog { opacity: 0; translate: 0 8px; transition: opacity var(--dur) var(--ease-out), translate var(--dur) var(--ease-out), overlay var(--dur) allow-discrete, display var(--dur) allow-discrete; }
  .pop__menu:popover-open, .pop__dialog[open] { opacity: 1; translate: 0 0; }
  @starting-style { .pop__menu:popover-open, .pop__dialog[open] { opacity: 0; translate: 0 8px; } }
  .pop__dialog::backdrop { background: color-mix(in oklab, #05060c 55%, transparent); backdrop-filter: blur(3px); opacity: 0; transition: opacity var(--dur) var(--ease-out), overlay var(--dur) allow-discrete, display var(--dur) allow-discrete; }
  .pop__dialog[open]::backdrop { opacity: 1; }
  @starting-style { .pop__dialog[open]::backdrop { opacity: 0; } }
  @media (prefers-reduced-motion: reduce) { .pop__menu, .pop__dialog { transition-duration: 0.001s; } }
</style>

<script>
  (function () {
    function bind() {
      document.querySelectorAll<HTMLElement>(".pop").forEach((root) => {
        if (root.dataset.bound) return;
        root.dataset.bound = "1";
        const dialog = root.querySelector<HTMLDialogElement>("[data-dialog]");
        root.querySelector<HTMLButtonElement>("[data-open-dialog]")?.addEventListener("click", () => dialog?.showModal());
      });
    }
    document.addEventListener("astro:page-load", bind);
    if (document.readyState !== "loading") bind();
  })();
</script>
07

Modern CSS color

Drag the hue. Every swatch below is derived live from a single OKLCH base — a lightness ramp, a complementary via relative-color hue math, and tints/shades via color-mix(). No preprocessor, no JS color library.

oklch() · color-mix() · relative colorsBaseline

Lightness ramp — oklch(L 0.16 h)

Relative + mix — base, complement (from base … h+180), tint, shade

Support: oklch() and color-mix() are Baseline; relative color syntax (oklch(from …)) is available in all current engines. Perceptually-uniform lightness means ramps built by simply stepping L look evenly spaced — unlike HSL.

View source
lab/modern-color.astro
---
export const meta = {
  title: "Modern CSS color",
  feature: "oklch() · color-mix() · relative colors",
  baseline: "Baseline",
  support: "oklch() and color-mix() are Baseline; relative color syntax (oklch(from …)) is available in all current engines. Perceptually-uniform lightness means ramps built by simply stepping L look evenly spaced — unlike HSL.",
  description:
    "Drag the hue. Every swatch below is derived live from a single OKLCH base — a lightness ramp, a complementary via relative-color hue math, and tints/shades via color-mix(). No preprocessor, no JS color library.",
  order: 7,
};

const steps = [0.95, 0.85, 0.72, 0.6, 0.48, 0.36, 0.24];
---
<div class="col" data-color>
  <label class="col__control">
    <span class="mono">hue <output data-hue>265</output>°</span>
    <input type="range" min="0" max="360" value="265" data-hue-input aria-label="Base hue" />
  </label>

  <div class="col__group">
    <p class="col__label">Lightness ramp — <code class="mono">oklch(L 0.16 h)</code></p>
    <div class="col__ramp">
      {steps.map((l) => <span class="col__sw" style={`background: oklch(${l} 0.16 var(--h));`}></span>)}
    </div>
  </div>

  <div class="col__group">
    <p class="col__label">Relative + mix — base, complement <code class="mono">(from base … h+180)</code>, tint, shade</p>
    <div class="col__ramp">
      <span class="col__sw" style="background: var(--base);"></span>
      <span class="col__sw" style="background: oklch(from var(--base) l c calc(h + 180));"></span>
      <span class="col__sw" style="background: color-mix(in oklab, var(--base) 35%, white);"></span>
      <span class="col__sw" style="background: color-mix(in oklab, var(--base) 65%, black);"></span>
      <span class="col__sw" style="background: color-mix(in oklab, var(--base) 50%, oklch(from var(--base) l c calc(h + 60)));"></span>
    </div>
  </div>
</div>

<style>
  .col { --h: 265; --base: oklch(0.62 0.18 var(--h)); padding: var(--space-l); display: grid; gap: 1.3rem; }
  .col__control { display: grid; gap: 0.5rem; }
  .col__control span { font-size: var(--step--1); color: var(--ink-muted); }
  input[type="range"] { width: 100%; accent-color: var(--base); }
  .col__group { display: grid; gap: 0.5rem; }
  .col__label { font-size: var(--step--1); color: var(--ink-muted); }
  .col__ramp { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 0.4rem; height: 4.5rem; }
  .col__sw { border-radius: var(--radius-m); border: 1px solid color-mix(in oklab, var(--ink) 12%, transparent); box-shadow: var(--shadow-s); }
</style>

<script>
  (function () {
    function bind() {
      document.querySelectorAll<HTMLElement>("[data-color]").forEach((root) => {
        if (root.dataset.bound) return;
        root.dataset.bound = "1";
        const input = root.querySelector<HTMLInputElement>("[data-hue-input]")!;
        const out = root.querySelector<HTMLOutputElement>("[data-hue]")!;
        const update = () => {
          root.style.setProperty("--h", input.value);
          root.style.setProperty("--base", `oklch(0.62 0.18 ${input.value})`);
          out.textContent = input.value;
        };
        input.addEventListener("input", update);
        update();
      });
    }
    document.addEventListener("astro:page-load", bind);
    if (document.readyState !== "loading") bind();
  })();
</script>
08

CSS-only carousel

A swipeable, keyboard-scrollable carousel with magnetic snap points. Where supported, the navigation dots and arrows are generated entirely by CSS ::scroll-marker pseudo-elements — no JavaScript at all.

scroll-snap · scroll-markerNewly available
  • Snap
  • Swipe
  • Scroll
  • Keys
  • No JS

drag / swipe / arrow-keys · dots are pure CSS where supported

Support: scroll-snap is Baseline everywhere. The CSS carousel pseudo-elements (::scroll-marker, ::scroll-button) that render the dots and arrows with zero JS are newer — Chromium 135+ — and are progressively enhanced here; without them you still get a snapping, swipeable, keyboard-scrollable track.

View source
lab/scroll-snap.astro
---
export const meta = {
  title: "CSS-only carousel",
  feature: "scroll-snap · scroll-marker",
  baseline: "Newly available",
  support: "scroll-snap is Baseline everywhere. The CSS carousel pseudo-elements (::scroll-marker, ::scroll-button) that render the dots and arrows with zero JS are newer — Chromium 135+ — and are progressively enhanced here; without them you still get a snapping, swipeable, keyboard-scrollable track.",
  description:
    "A swipeable, keyboard-scrollable carousel with magnetic snap points. Where supported, the navigation dots and arrows are generated entirely by CSS ::scroll-marker pseudo-elements — no JavaScript at all.",
  order: 8,
};

const slides = [
  { t: "Snap", h: "265" },
  { t: "Swipe", h: "200" },
  { t: "Scroll", h: "160" },
  { t: "Keys", h: "120" },
  { t: "No JS", h: "40" },
];
---
<div class="snap">
  <ul class="snap__track" tabindex="0" aria-label="Carousel">
    {slides.map((s) => (
      <li class="snap__slide" style={`--h:${s.h}`}>
        <span class="snap__num mono">{s.t}</span>
      </li>
    ))}
  </ul>
  <p class="snap__hint text-muted mono">drag / swipe / arrow-keys · dots are pure CSS where supported</p>
</div>

<style>
  .snap { padding: var(--space-l); display: grid; gap: 0.9rem; }
  .snap__track {
    display: flex; gap: 1rem; list-style: none; margin: 0; padding: 0.25rem;
    overflow-x: auto; scroll-snap-type: x mandatory; scroll-behavior: smooth; scrollbar-width: none;
    anchor-name: --snaptrack;
  }
  .snap__track::-webkit-scrollbar { display: none; }
  .snap__slide {
    flex: 0 0 min(80%, 22rem); scroll-snap-align: center; height: 12rem;
    border-radius: var(--radius-l); display: grid; place-items: center;
    background: linear-gradient(150deg, oklch(72% 0.17 var(--h)), oklch(48% 0.15 calc(var(--h) + 40)));
    box-shadow: var(--shadow-m);
  }
  .snap__num { font-size: var(--step-4); font-weight: 800; color: #fff; text-shadow: 0 2px 12px rgba(0,0,0,0.35); }
  .snap__hint { text-align: center; font-size: var(--step--1); }

  /* Progressive: CSS-generated carousel controls (Chromium 135+) */
  @supports (scroll-marker-group: after) {
    .snap__track { scroll-marker-group: after; }
    .snap__track::scroll-marker-group { display: flex; gap: 0.5rem; justify-content: center; padding-top: 0.9rem; }
    .snap__slide::scroll-marker {
      content: ""; width: 10px; height: 10px; border-radius: 50%;
      border: 2px solid var(--border-strong); cursor: pointer; transition: background var(--dur), border-color var(--dur);
    }
    .snap__slide::scroll-marker:target-current { background: var(--accent, #8b5cf6); border-color: var(--accent, #8b5cf6); }
  }
  @media (prefers-reduced-motion: reduce) { .snap__track { scroll-behavior: auto; } }
</style>