New and emerging CSS, crawled from specs, browser blogs, and expert writeupsAlmost entirely built by Claude CodeLast updated Aug 21, 2026

CSS Edge

GitHub (opens in a new tab)

Filters

New examples

Category
Browser support

Showing 63 of 63 examples

NEWComponents & InteractivityProduction ready

Making the background inert while a modal is open

inert

Setting inert on an element — via the plain HTML attribute or the matching el.inert JS property — removes it and everything inside it from click handling, tab order, text selection, and the accessibility tree, all in one step. Opening the modal below marks the background section inert instead of juggling tabindex="-1" on every focusable descendant by hand; closing it restores normal interaction.

View code

HTML

<div class="demo-area">
  <div id="background" class="panel">
    <p style="margin:0">Background content</p>
    <input type="text" placeholder="Try typing or tabbing here">
    <button id="bg-btn">Click me</button>
  </div>
  <button id="open-btn" class="open-btn">Open modal</button>

  <div id="modal" class="modal">
    <p style="margin:0">I'm a modal. The background is now inert — try tabbing, clicking, or typing behind me.</p>
    <button id="close-btn">Close</button>
  </div>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
.demo-area { position: relative; min-height: 220px; max-width: 260px; }
.panel {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 1rem;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  transition: opacity 0.2s;
}
.panel[inert] { opacity: 0.45; }
.panel input, .panel button {
  font: inherit;
  padding: 0.4rem 0.6rem;
  border-radius: 6px;
  border: 1px solid #d1d5db;
}
.open-btn {
  margin-top: 0.75rem;
  padding: 0.5rem 1rem;
  border: none;
  border-radius: 6px;
  background: #2563eb;
  color: #fff;
  font: inherit;
  cursor: pointer;
}
.modal {
  position: absolute;
  inset: 0;
  display: none;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 0.75rem;
  padding: 1.25rem;
  background: #fff;
  border: 2px solid #2563eb;
  border-radius: 10px;
  box-shadow: 0 10px 25px rgb(0 0 0 / 0.15);
}
.modal.is-open { display: flex; }
.modal button {
  padding: 0.5rem 1rem;
  border: none;
  border-radius: 6px;
  background: #2563eb;
  color: #fff;
  font: inherit;
  cursor: pointer;
}
@media (prefers-reduced-motion: reduce) {
  .panel { transition: none; }
}

JS

const background = document.getElementById('background');
const modal = document.getElementById('modal');
const openBtn = document.getElementById('open-btn');
const closeBtn = document.getElementById('close-btn');

openBtn.addEventListener('click', () => {
  background.inert = true;
  openBtn.inert = true;
  modal.classList.add('is-open');
  closeBtn.focus();
});

closeBtn.addEventListener('click', () => {
  background.inert = false;
  openBtn.inert = false;
  modal.classList.remove('is-open');
  openBtn.focus();
});
Browser support:Chrome 102+Firefox 112+Safari 15.5+Edge 102+
Animation & TransitionsProduction ready

Staggering a list reveal with an inline custom-property index

custom properties, calc(), animation-delay

Each item carries its own position as an inline custom property (style="--i: 2"), and a single shared animation-delay: calc(var(--i) * 90ms) rule turns that index into a staggered entrance — one keyframe animation and one CSS rule cover every item, with no per-item rules or JavaScript needed.

View code

HTML

<ul class="stagger">
  <li class="item" style="--i: 0">Fade in</li>
  <li class="item" style="--i: 1">one item</li>
  <li class="item" style="--i: 2">at a time</li>
  <li class="item" style="--i: 3">in a neat</li>
  <li class="item" style="--i: 4">staggered</li>
  <li class="item" style="--i: 5">sequence</li>
</ul>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
.stagger {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  list-style: none;
  margin: 0;
  padding: 0;
  max-width: 220px;
}
.item {
  padding: 0.65rem 1rem;
  border-radius: 8px;
  background: #eff6ff;
  border: 1px solid #bfdbfe;
  font-weight: 600;
  color: #1d4ed8;
  opacity: 0;
  translate: 0 12px;
  animation: stagger-in 0.5s ease both;
  animation-delay: calc(var(--i) * 90ms);
}
@keyframes stagger-in {
  to { opacity: 1; translate: 0 0; }
}
@media (prefers-reduced-motion: reduce) {
  .item { animation: none; opacity: 1; translate: 0 0; }
}
Browser support:Chrome 49+Firefox 31+Safari 10+Edge 16+
Components & InteractivityGrowing support

A back-to-top button that hides itself with @container scroll-state()

@container scroll-state(scrollable)

container-type: scroll-state on the root element exposes whether the page has more content to scroll toward a given edge, so @container scroll-state(scrollable: top) can slide a back-to-top link into view only once the user has actually scrolled down — no scroll event listener, no JavaScript.

View code

HTML

<div class="page-content">
  <p>Scroll down to reveal the back-to-top button ↓</p>
  <p>Paragraph two of filler content to make this page scrollable.</p>
  <p>Paragraph three of filler content.</p>
  <p>Paragraph four of filler content.</p>
  <p>Paragraph five of filler content.</p>
  <p>Paragraph six of filler content.</p>
  <p>Paragraph seven of filler content.</p>
  <p>Paragraph eight of filler content.</p>
  <p>Paragraph nine of filler content.</p>
  <p>Paragraph ten of filler content.</p>
  <p>Paragraph eleven of filler content.</p>
  <p>Paragraph twelve of filler content.</p>
  <p>Paragraph thirteen of filler content.</p>
  <p>Paragraph fourteen of filler content.</p>
  <p>Paragraph fifteen — end of content.</p>
</div>
<a href="#top" class="back-to-top" aria-label="Back to top">↑</a>

CSS

html {
  container-type: scroll-state;
  container-name: page;
  scroll-behavior: smooth;
}
body { font-family: system-ui, sans-serif; margin: 0; padding: 2rem; background: #fff; color: #111827; }
.page-content p { margin: 0 0 2rem; }
.back-to-top {
  position: fixed;
  bottom: 1.5rem;
  right: 1.5rem;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: #2563eb;
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.25rem;
  text-decoration: none;
  translate: 0 100px;
  transition: translate 0.3s ease-in-out;
}
@container page scroll-state(scrollable: top) {
  .back-to-top {
    translate: 0 0;
  }
}
Browser support:Chrome 133+Firefox Safari Edge 133+
Animation & TransitionsProduction ready

A seamless CSS-only marquee

animation, transform: translateX()

A single repeating track, animated by one @keyframes rule that translates it by exactly -50% of its own width, creates an infinite horizontal marquee with no JavaScript. The content is duplicated enough times that the track is always wider than the container, so there is no visible gap when the loop restarts — and every duplicate past the first is aria-hidden, so screen readers hear it exactly once.

View code

HTML

<div class="marquee">
  <div class="track">
    <span class="item">Item one</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item">Item two</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item one</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item two</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item one</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item two</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item one</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item two</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item one</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item two</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item one</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item two</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item one</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item two</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item one</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item two</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item one</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item two</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item one</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item two</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item one</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item two</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item one</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item two</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item one</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item two</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item one</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item two</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item one</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item two</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item one</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item two</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item one</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item two</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item one</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item two</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item one</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item two</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item one</span>
    <span class="sep" aria-hidden="true">&bull;</span>
    <span class="item" aria-hidden="true">Item two</span>
    <span class="sep" aria-hidden="true">&bull;</span>
  </div>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 0; background: #fff; }
.marquee {
  overflow: hidden;
  white-space: nowrap;
  background: #2563eb;
  padding: 0.75rem 0;
}
.track {
  display: inline-flex;
  align-items: center;
  animation: marquee 28s linear infinite;
}
.item {
  padding: 0 1rem;
  color: #fff;
  font-size: 0.9rem;
  font-weight: 600;
}
.sep {
  color: rgba(255, 255, 255, 0.4);
}
@keyframes marquee {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(-50%);
  }
}
@media (prefers-reduced-motion: reduce) {
  .track {
    animation: none;
  }
}
Browser support:Chrome 4+Firefox 5+Safari 5.1+Edge 12+
Forms & UIProduction ready

A CSS-only star rating widget with :has()

:has(), counter-increment

Real radio inputs and labels form an accessible rating control, styled entirely with :has() and general sibling selectors — :has(~ .star:hover) reaches backwards to light up every star before the hovered one, the same trick fills stars up to whichever value is :checked, and a CSS counter turns that fill state directly into a live rating number, all without JavaScript.

View code

HTML

<fieldset class="widget">
  <legend>Rate this product</legend>
  <div class="stars">
    <input type="radio" id="star1" name="rating" value="1">
    <label for="star1" class="star">★</label>
    <input type="radio" id="star2" name="rating" value="2">
    <label for="star2" class="star">★</label>
    <input type="radio" id="star3" name="rating" value="3">
    <label for="star3" class="star">★</label>
    <input type="radio" id="star4" name="rating" value="4">
    <label for="star4" class="star">★</label>
    <input type="radio" id="star5" name="rating" value="5">
    <label for="star5" class="star">★</label>
  </div>
  <p class="rating-display">Rating: <span class="rating-num"></span>/5</p>
</fieldset>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
.widget { border: none; padding: 0; counter-reset: rating; }
.stars { display: flex; gap: 0.25rem; font-size: 2rem; }
.stars input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
}
.star {
  color: #d1d5db;
  cursor: pointer;
  transition: color 0.15s;
}
.stars input:focus-visible + .star {
  outline: 2px solid #2563eb;
  outline-offset: 2px;
  border-radius: 4px;
}
.star:has(~ input:checked),
.stars input:checked + .star,
.star:has(~ .star:hover),
.star:hover {
  color: #f59e0b;
  counter-increment: rating;
}
.rating-display { margin-top: 0.5rem; font-size: 0.95rem; }
.rating-num::after { content: counter(rating); }
Browser support:Chrome 105+Firefox 121+Safari 15.4+Edge 105+
Components & InteractivityGrowing support

CSS-only carousel dots with ::scroll-marker

::scroll-marker, scroll-marker-group, :target-current

scroll-marker-group: after generates a ::scroll-marker-group container for a scroll-snap carousel, and giving each slide a ::scroll-marker turns it into a clickable dot placed in that group automatically — the browser tracks which one is current via :target-current, so active-dot highlighting needs no JavaScript at all.

View code

HTML

<div class="widget">
  <ul class="carousel">
    <li class="slide">1</li>
    <li class="slide">2</li>
    <li class="slide">3</li>
    <li class="slide">4</li>
    <li class="slide">5</li>
    <li class="slide">6</li>
    <li class="slide">7</li>
    <li class="slide">8</li>
    <li class="slide">9</li>
    <li class="slide">10</li>
    <li class="slide">11</li>
    <li class="slide">12</li>
  </ul>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; }
.widget { border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; }
.carousel {
  display: flex;
  gap: 1rem;
  list-style: none;
  margin: 0;
  padding: 1.5rem 1rem 0.5rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  scroll-marker-group: after;
}
.slide {
  flex: 0 0 140px;
  height: 100px;
  scroll-snap-align: center;
  background: #2563eb;
  color: #fff;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.5rem;
  font-weight: 700;
}
.carousel::scroll-marker-group {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  padding-bottom: 0.75rem;
}
.slide::scroll-marker {
  content: "";
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: #d1d5db;
}
.slide::scroll-marker:target-current {
  background: #2563eb;
  scale: 1.2;
}
Browser support:Chrome 135+Firefox Safari Edge 135+
Functions & ValuesGrowing support

Doing math on keyword sizes with calc-size()

calc-size()

Plain calc() can't accept keyword values like auto, fit-content, or max-content as an operand. calc-size(fit-content, size + 40px) can — its first argument is the keyword to resolve, and size stands in for that resolved value inside the expression, so an element can size itself relative to its own intrinsic content plus a fixed offset.

View code

HTML

<div class="row">
  <div class="chip">Hi</div>
  <div class="chip">Hello there</div>
  <div class="chip">A much longer label</div>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
.row { display: flex; flex-direction: column; gap: 0.75rem; align-items: flex-start; }
.chip {
  padding: 0.6rem 1rem;
  border-radius: 8px;
  background: #eff6ff;
  border: 2px dashed #2563eb;
  font-weight: 600;
  width: fit-content; /* fallback if calc-size() is unsupported */
  width: calc-size(fit-content, size + 40px);
}
Browser support:Chrome 129+Firefox Safari Edge 129+
Animation & TransitionsProduction ready

Animating an element along a curve with offset-path

offset-path, offset-distance, offset-rotate

offset-path, offset-distance, and offset-rotate move an element along an arbitrary SVG path — a Bézier curve, a loop, any custom shape — with offset-rotate: auto keeping it tangent to the curve as it travels, so a single @keyframes animation on offset-distance replaces manual left/top math or a JS animation library.

View code

HTML

<div class="stage">
  <svg class="guide" viewBox="0 0 280 140"><path d="M 20 120 C 20 20 260 20 260 120" /></svg>
  <div class="mover"></div>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 0; background: #fff; display: flex; align-items: center; justify-content: center; height: 240px; }
.stage { position: relative; width: 280px; height: 140px; }
.guide { position: absolute; inset: 0; width: 100%; height: 100%; }
.guide path { fill: none; stroke: #e5e7eb; stroke-width: 2; }
.mover {
  position: absolute;
  width: 18px;
  height: 18px;
  background: #2563eb;
  clip-path: polygon(0% 0%, 100% 50%, 0% 100%);
  offset-path: path("M 20 120 C 20 20 260 20 260 120");
  offset-rotate: auto;
  animation: travel 3s ease-in-out infinite alternate;
}
@keyframes travel {
  0% { offset-distance: 0%; }
  100% { offset-distance: 100%; }
}
Browser support:Chrome 46+Firefox 72+Safari 16+Edge 79+
Responsive & Container QueriesGrowing support

Detecting which card is snapped with @container scroll-state()

@container scroll-state(snapped)

container-type: scroll-state turns a scroll-snapping element into a query container, so a descendant can react via @container scroll-state(snapped: inline) to whether that specific card is the one currently snapped into view — the active state for a carousel or slider, computed entirely by the browser with no IntersectionObserver or scroll event listener.

View code

HTML

<div class="widget">
  <ul class="track">
    <li class="card"><div class="card-inner"><span class="num">1</span><p>Slide one</p></div></li>
    <li class="card"><div class="card-inner"><span class="num">2</span><p>Slide two</p></div></li>
    <li class="card"><div class="card-inner"><span class="num">3</span><p>Slide three</p></div></li>
    <li class="card"><div class="card-inner"><span class="num">4</span><p>Slide four</p></div></li>
    <li class="card"><div class="card-inner"><span class="num">5</span><p>Slide five</p></div></li>
    <li class="card"><div class="card-inner"><span class="num">6</span><p>Slide six</p></div></li>
    <li class="card"><div class="card-inner"><span class="num">7</span><p>Slide seven</p></div></li>
    <li class="card"><div class="card-inner"><span class="num">8</span><p>Slide eight</p></div></li>
    <li class="card"><div class="card-inner"><span class="num">9</span><p>Slide nine</p></div></li>
    <li class="card"><div class="card-inner"><span class="num">10</span><p>Slide ten</p></div></li>
    <li class="card"><div class="card-inner"><span class="num">11</span><p>Slide eleven</p></div></li>
    <li class="card"><div class="card-inner"><span class="num">12</span><p>Slide twelve</p></div></li>
  </ul>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; }
.widget { border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; }
.track {
  display: flex;
  gap: 1rem;
  list-style: none;
  margin: 0;
  padding: 2rem 1rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
}
.card {
  flex: 0 0 160px;
  scroll-snap-align: center;
  container-type: scroll-state;
  container-name: slide;
}
.card-inner {
  height: 160px;
  border-radius: 12px;
  background: #f8fafc;
  border: 2px solid transparent;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  transition: background 0.2s, border-color 0.2s, transform 0.2s, opacity 0.2s;
  transform: scale(0.92);
  opacity: 0.75;
}
.num {
  font-size: 1.5rem;
  font-weight: 700;
  color: #9ca3af;
  transition: color 0.2s;
}
@container slide scroll-state(snapped: inline) {
  .card-inner {
    background: #eff6ff;
    border-color: #2563eb;
    transform: scale(1);
    opacity: 1;
  }
  .num { color: #2563eb; }
}
Browser support:Chrome 133+Firefox Safari Edge 133+
Animation & TransitionsProduction ready

Animating a rotating gradient border with @property

@property, conic-gradient()

Registering --angle with @property makes it an animatable <angle>, so a conic-gradient() painted onto the border-box (with a plain fill clipped to the padding-box) can spin smoothly via a keyframe animation — no rotating pseudo-element or JavaScript required.

View code

HTML

<div class="card">
  <p>Animated gradient border</p>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 0; background: #fff; display: flex; justify-content: center; align-items: center; height: 240px; }

@property --angle {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}

.card {
  --angle: 0deg;
  width: 220px;
  padding: 1.5rem;
  border: 4px solid transparent;
  border-radius: 14px;
  background:
    linear-gradient(#fff, #fff) padding-box,
    conic-gradient(from var(--angle), #2563eb, #db2777, #f59e0b, #2563eb) border-box;
  color: #111827;
  font-weight: 600;
  text-align: center;
  animation: spin-border 4s linear infinite;
}

@keyframes spin-border {
  to { --angle: 360deg; }
}
Browser support:Chrome 85+Firefox 128+Safari 16.4+Edge 85+
Scroll & View TransitionsProduction ready

Revealing boxes as they scroll into view

animation-timeline: view()

animation-timeline: view() ties a keyframe animation to how far an element has scrolled through its nearest scrollable ancestor, so each box can fade and slide in exactly as it enters the viewport — a common scroll-reveal effect that used to need an IntersectionObserver, now built entirely in CSS.

View code

HTML

<div class="scroller">
  <p class="hint">Scroll down ↓</p>
  <div class="spacer-top"></div>
  <div class="box">1</div>
  <div class="box">2</div>
  <div class="box">3</div>
  <div class="box">4</div>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 0; background: #fff; }
.scroller {
  height: 240px;
  overflow-y: auto;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  padding: 0 1rem;
  box-sizing: border-box;
}
.hint { margin: 0.75rem 0; color: #6b7280; font-size: 0.85rem; text-align: center; }
.spacer-top { height: 220px; }
.box {
  height: 70px;
  margin-bottom: 1rem;
  border-radius: 10px;
  background: #2563eb;
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 1.1rem;
  animation-name: reveal;
  animation-duration: 1ms; /* the scroll timeline drives progress, not this duration — some engines need a nonzero value to run the animation at all */
  animation-timing-function: linear;
  animation-fill-mode: both;
  animation-timeline: view();
  animation-range: entry 0% entry 100%;
}
@keyframes reveal {
  from { opacity: 0; }
  to { opacity: 1; }
}
Browser support:Chrome 115+Firefox 157+Safari 26.0+Edge 115+
Components & InteractivityProduction ready

A dropdown menu with anchor positioning and automatic flipping

anchor-name, position-try-fallbacks

Pairing the popover attribute with anchor-name, position-anchor, and position-try-fallbacks builds a dropdown menu that opens directly below its trigger button and automatically flips above it when there's no room below — all without a single line of positioning JavaScript.

View code

HTML

<div class="wrap">
  <button popovertarget="menu" id="menu-btn" class="menu-btn">Options ▾</button>
  <div id="menu" popover class="menu">
    <button class="menu-item">Edit</button>
    <button class="menu-item">Duplicate</button>
    <button class="menu-item">Archive</button>
    <button class="menu-item">Delete</button>
  </div>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 0; background: #fff; color: #111827; height: 240px; display: flex; align-items: flex-end; justify-content: center; padding-bottom: 2rem; }
.menu-btn {
  anchor-name: --menu-btn;
  padding: 0.6rem 1.2rem;
  border: none;
  border-radius: 8px;
  background: #2563eb;
  color: #fff;
  font: inherit;
  cursor: pointer;
}
.menu {
  position: fixed;
  position-anchor: --menu-btn;
  inset: auto;
  top: anchor(bottom);
  left: anchor(left);
  position-try-fallbacks: flip-block;
  margin: 0.4rem 0 0;
  padding: 0.4rem;
  min-width: 160px;
  border: none;
  border-radius: 8px;
  background: #fff;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
  flex-direction: column;
  gap: 2px;
}
.menu:popover-open {
  display: flex;
}
.menu-item {
  padding: 0.5rem 0.75rem;
  border: none;
  background: none;
  text-align: left;
  font: inherit;
  border-radius: 6px;
  cursor: pointer;
}
.menu-item:hover { background: #f1f5f9; }
Browser support:Chrome 125+Firefox 147+Safari 26+Edge 125+
Animation & TransitionsGrowing support

Animating height: auto with interpolate-size

interpolate-size

interpolate-size: allow-keywords lets a transition genuinely animate toward auto, so an expanding panel no longer needs a guessed max-height or a JavaScript-measured pixel value to animate smoothly to its real content height.

View code

HTML

<div class="expander">
  <input type="checkbox" id="exp1" class="toggle">
  <label for="exp1">Toggle details</label>
  <div class="content">
    <p>This panel animates to its natural height using interpolate-size, without a guessed max-height or JavaScript measuring the content.</p>
  </div>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
:root { interpolate-size: allow-keywords; }
.toggle { position: absolute; opacity: 0; pointer-events: none; }
label {
  display: inline-block;
  cursor: pointer;
  padding: 0.5rem 1rem;
  background: #2563eb;
  color: #fff;
  border-radius: 6px;
  font-weight: 600;
  margin-bottom: 0.75rem;
}
.content {
  height: 0;
  overflow: hidden;
  transition: height 0.35s ease;
  border-radius: 8px;
  background: #f1f5f9;
  padding: 0 1rem;
}
.toggle:checked ~ .content { height: auto; }
.toggle:checked ~ .content p { padding: 1rem 0; margin: 0; }
Browser support:Chrome 129+Firefox Safari Edge 129+
LayoutGrowing support

Drawing lines in grid gaps with row-rule and column-rule

row-rule, column-rule

The CSS Gap Decorations module extends column-rule to grid and flexbox and adds a matching row-rule, so dividers between grid cells can be styled directly, without extra spacer elements or background-image tricks.

View code

HTML

<div class="grid">
  <div class="cell">1</div>
  <div class="cell">2</div>
  <div class="cell">3</div>
  <div class="cell">4</div>
  <div class="cell">5</div>
  <div class="cell">6</div>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; }
.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 20px;
  column-rule: 3px solid #2563eb;
  row-rule: 3px dashed #f97316;
}
.cell {
  background: #f1f5f9;
  border-radius: 6px;
  padding: 1.5rem;
  text-align: center;
  font-weight: 600;
  color: #111827;
}
Browser support:Chrome 149+Firefox Safari Edge 149+
Responsive & Container QueriesProduction ready

Switching styles by a container's custom property with @container style()

@container style()

@container style() queries a container's own computed custom-property value, not just its size, so a component can react to a --theme flag set anywhere in its ancestor chain without that flag being repeated as a class name or a media query.

View code

HTML

<div class="panel" style="--theme: light">
  <div class="card">Light panel</div>
</div>
<div class="panel" style="--theme: dark">
  <div class="card">Dark panel</div>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; display: flex; gap: 1rem; }
.panel {
  container-type: inline-size;
  container-name: panel;
  flex: 1;
  padding: 1.25rem;
  border-radius: 10px;
  background: #f1f5f9;
}
.card {
  padding: 1rem;
  border-radius: 8px;
  background: #fff;
  color: #111827;
  font-weight: 600;
  text-align: center;
}
@container panel style(--theme: dark) {
  .card { background: #111827; color: #fff; }
}
Browser support:Chrome 106+Firefox 110+Safari 16+Edge 106+
Color & BackgroundsProduction ready

Automatic accessible text color with contrast-color()

contrast-color()

contrast-color() picks black or white for you, whichever contrasts better against a given color, so a badge painted from a single dynamic background variable keeps readable text automatically instead of a text color hand-picked for every theme.

View code

HTML

<div class="swatches">
  <div class="swatch" style="--bg: #1d4ed8">Aa</div>
  <div class="swatch" style="--bg: #fde047">Aa</div>
  <div class="swatch" style="--bg: #111827">Aa</div>
  <div class="swatch" style="--bg: #f472b6">Aa</div>
  <div class="swatch" style="--bg: #10b981">Aa</div>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; }
.swatches { display: flex; gap: 0.75rem; flex-wrap: wrap; }
.swatch {
  width: 72px;
  height: 72px;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 1.1rem;
  background: var(--bg);
  color: contrast-color(var(--bg));
}
Browser support:Chrome 147+Firefox 146+Safari 26+Edge 147+
Functions & ValuesGrowing support

Scattering confetti with the CSS random() function

random()

random() generates a random number natively in CSS, so positions, rotations, and hues for things like confetti or particle effects can vary on every page load straight from a stylesheet, without JavaScript or a hand-rolled custom property per element. Browsers without support fall back to a static pattern via @supports.

View code

HTML

<div class="confetti">
  <span class="piece"></span><span class="piece"></span><span class="piece"></span>
  <span class="piece"></span><span class="piece"></span><span class="piece"></span>
  <span class="piece"></span><span class="piece"></span><span class="piece"></span>
  <span class="piece"></span><span class="piece"></span><span class="piece"></span>
  <span class="piece"></span><span class="piece"></span><span class="piece"></span>
  <span class="piece"></span><span class="piece"></span><span class="piece"></span>
  <span class="piece"></span><span class="piece"></span><span class="piece"></span>
  <span class="piece"></span><span class="piece"></span><span class="piece"></span>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; }
.confetti {
  display: flex;
  flex-wrap: wrap;
  align-content: flex-start;
  gap: 10px;
  height: 200px;
  padding: 16px;
  overflow: hidden;
  background: #f8fafc;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
}
.piece {
  width: 20px;
  height: 20px;
  border-radius: 4px;
  background: #93c5fd; /* static fallback pattern if random() is unsupported */
}
.piece:nth-child(3n+1) { background: #86efac; }
.piece:nth-child(3n+2) { background: #f9a8d4; }

@supports (top: random(0%, 100%)) {
  .confetti { display: block; position: relative; padding: 0; }
  .piece {
    position: absolute;
    top: random(-10%, 100%);
    left: random(0%, 100%);
    width: 10px;
    height: 14px;
    background: hsl(random(0, 360) 80% 60%);
    rotate: random(0deg, 360deg);
    border-radius: 2px;
  }
}
Browser support:Chrome Firefox Safari 26.2+Edge
Functions & ValuesGrowing support

Staggered bar chart from sibling-index() and sibling-count()

sibling-index(), sibling-count()

sibling-index() and sibling-count() expose an element's position and total sibling count directly as CSS numbers, so per-item height, hue, and animation delay can be derived with plain calc() math instead of inline custom properties, nth-child selectors, or a scripting loop.

View code

HTML

<ul class="bars">
  <li class="bar"></li>
  <li class="bar"></li>
  <li class="bar"></li>
  <li class="bar"></li>
  <li class="bar"></li>
  <li class="bar"></li>
  <li class="bar"></li>
  <li class="bar"></li>
</ul>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; }
.bars {
  display: flex;
  align-items: flex-end;
  gap: 8px;
  height: 140px;
  list-style: none;
  margin: 0;
  padding: 0;
}
.bar {
  width: 22px;
  border-radius: 4px 4px 0 0;
  background: hsl(calc(360deg / sibling-count() * sibling-index()) 70% 55%);
  height: calc(100% / sibling-count() * sibling-index());
  transform-origin: bottom;
  animation: rise 0.6s ease both;
  animation-delay: calc(sibling-index() * 80ms);
}
@keyframes rise {
  from { transform: scaleY(0); }
  to { transform: scaleY(1); }
}
Browser support:Chrome 138+Firefox Safari 26.2+Edge 138+
Functions & ValuesProduction ready

Positioning elements around a circle with sin() and cos()

sin(), cos()

CSS trigonometric functions (sin(), cos(), and friends) compute coordinates directly in CSS, so dots on a dial or items in a radial menu can be placed around a circle from one angle custom property instead of hand-typed positions or a JavaScript loop.

View code

HTML

<div class="dial">
  <span class="tick" style="--i: 0"></span>
  <span class="tick" style="--i: 1"></span>
  <span class="tick" style="--i: 2"></span>
  <span class="tick" style="--i: 3"></span>
  <span class="tick" style="--i: 4"></span>
  <span class="tick" style="--i: 5"></span>
  <span class="tick" style="--i: 6"></span>
  <span class="tick" style="--i: 7"></span>
  <span class="tick" style="--i: 8"></span>
  <span class="tick" style="--i: 9"></span>
  <span class="tick" style="--i: 10"></span>
  <span class="tick" style="--i: 11"></span>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
.dial {
  position: relative;
  width: 200px;
  height: 200px;
  border-radius: 50%;
  border: 2px solid #e5e7eb;
}
.tick {
  --angle: calc(360deg / 12 * var(--i));
  position: absolute;
  top: 50%;
  left: 50%;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: #2563eb;
  transform:
    translate(-50%, -50%)
    translate(calc(sin(var(--angle)) * 88px), calc(cos(var(--angle)) * -88px));
}
Browser support:Chrome 79+Firefox 75+Safari 13.1+Edge 79+
Animation & TransitionsProduction ready

Organic Blob Shapes

border-radius: 40% 60% / 35% 65%

Creates organic blob shapes from a single div using the multi-value border-radius shorthand — the slash separates horizontal radii from vertical radii, and animating between value sets makes the blob feel alive.

View code

HTML

<div class="blob"></div>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; display: flex; justify-content: center; align-items: center; height: 200px; }

/* Horizontal radii / Vertical radii              */
/* top-left  top-right  bottom-right  bottom-left */
.blob {
  width: 180px;
  height: 180px;
  background: #2563eb;
  border-radius: 40% 60% 65% 35% / 40% 35% 65% 60%;
  animation: blob-morph 6s ease-in-out infinite;
}

/* Morph between shapes with a keyframe animation */
@keyframes blob-morph {
  0%, 100% { border-radius: 40% 60% 65% 35% / 40% 35% 65% 60%; }
  33%      { border-radius: 60% 40% 30% 70% / 60% 70% 30% 40%; }
  66%      { border-radius: 30% 70% 60% 40% / 50% 40% 60% 50%; }
}
Browser support:Chrome 4+Firefox 3+Safari 3.1+Edge 12+
Forms & UIGrowing support

Customizing the checkbox tick with ::checkmark

::checkmark

::checkmark exposes the checkmark/tick glyph inside a native checkbox or radio button as a styleable pseudo-element, so its shape and color can be customized without discarding native accessibility.

View code

HTML

<label class="check-row"><input type="checkbox" checked class="custom-check"> Check me</label>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
.check-row { display: flex; align-items: center; gap: 0.5rem; font-size: 1rem; }
.custom-check { width: 22px; height: 22px; accent-color: #2563eb; }
.custom-check::checkmark {
  color: #fff;
}
Browser support:Chrome 133+Firefox Safari 27+Edge 133+
Functions & ValuesProduction ready

Snapping values to a grid with round()

round()

round() snaps a value to the nearest multiple of a step — with nearest, up, and down strategies — useful for aligning computed sizes to a pixel or spacing grid without a build-time calculation.

View code

HTML

<div class="row">
  <div class="bar" style="width: round(up, 68%, 25%)">round(up, 68%, 25%)</div>
  <div class="bar" style="width: round(nearest, 68%, 25%)">round(nearest, 68%, 25%)</div>
  <div class="bar" style="width: round(down, 68%, 25%)">round(down, 68%, 25%)</div>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
.row { display: flex; flex-direction: column; gap: 0.5rem; max-width: 320px; }
.bar { background: #2563eb; color: #fff; font-size: 0.75rem; padding: 0.5rem; border-radius: 6px; white-space: nowrap; }
Browser support:Chrome 79+Firefox 75+Safari 13.1+Edge 79+
Responsive & Container QueriesProduction ready

Writing range media queries with comparison operators

@media (width >= 600px) and (width <= 1200px)

@media (400px <= width <= 700px) reads like an actual inequality instead of two separate min-width/max-width declarations — a small but genuinely more readable syntax for range-based breakpoints.

View code

HTML

<div class="box">Resize the preview panel — I only turn blue between 400px and 700px wide.</div>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
.box { padding: 1.5rem; border-radius: 8px; background: #f3f4f6; font-size: 0.9rem; }

@media (400px <= width <= 700px) {
  .box { background: #2563eb; color: #fff; }
}
Browser support:Chrome 104+Firefox 63+Safari 16.4+Edge 104+
Color & BackgroundsProduction ready

Deriving transparent variants with relative color syntax

oklch(from color l c h / α)

oklch(from color l c h / alpha) reads a color's own channels back out and lets you recompute just one of them — here, deriving four opacity levels of the same hue without four separate rgba() variables.

View code

HTML

<div class="row">
  <div class="chip" style="background: var(--base)"></div>
  <div class="chip" style="background: oklch(from var(--base) l c h / 0.7)"></div>
  <div class="chip" style="background: oklch(from var(--base) l c h / 0.4)"></div>
  <div class="chip" style="background: oklch(from var(--base) l c h / 0.15)"></div>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; }
:root { --base: oklch(60% 0.19 280); }
.row { display: flex; gap: 0.5rem; }
.chip { width: 56px; height: 56px; border-radius: 8px; box-shadow: inset 0 0 0 1px rgba(0,0,0,0.08); }
Browser support:Chrome 131+Firefox 133+Safari 18+Edge 131+
LayoutGrowing support

Evenly distributing wrapped flex items

flex-wrap: balance

flex-wrap: balance spreads items across wrapped rows more evenly than the default greedy wrap, so the last row doesn't end up with a single lonely leftover item.

View code

HTML

<div class="flex-demo">
  <span class="tag">1</span>
  <span class="tag">2</span>
  <span class="tag">3</span>
  <span class="tag">4</span>
  <span class="tag">5</span>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; }
.flex-demo {
  display: flex;
  flex-wrap: wrap; /* fallback if balance is unsupported */
  flex-wrap: balance;
  gap: 0.5rem;
  width: 220px;
}
.tag { flex: 1 1 60px; background: #2563eb; color: #fff; border-radius: 8px; padding: 0.75rem; text-align: center; font-weight: 700; }
Browser support:Chrome 150+Firefox Safari Edge 150+
Functions & ValuesGrowing support

Feature-detecting at-rules before using them

@supports at-rule(@keyword)

@supports at-rule(@layer) tests whether the browser understands a given at-rule before you rely on it, extending progressive enhancement to newer at-rules like @layer, @property, and @scope.

View code

HTML

<ul class="checks">
  <li>@keyframes <span class="badge" aria-hidden="true"></span><span class="status unsupported">not supported</span><span class="status supported">supported</span></li>
  <li>@layer <span class="badge" aria-hidden="true"></span><span class="status unsupported">not supported</span><span class="status supported">supported</span></li>
  <li>@scope <span class="badge" aria-hidden="true"></span><span class="status unsupported">not supported</span><span class="status supported">supported</span></li>
</ul>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
.checks { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0.5rem; font-family: ui-monospace, monospace; font-size: 0.9rem; }
.badge::after { content: "?"; color: #9ca3af; }
.status { font-family: system-ui, sans-serif; font-size: 0.8rem; }
.status.supported { display: none; color: #15803d; }
.status.unsupported { display: inline; color: #9ca3af; }

@supports at-rule(@keyframes) {
  .checks li:nth-child(1) .badge::after { content: "✓"; color: #15803d; }
  .checks li:nth-child(1) .status.supported { display: inline; }
  .checks li:nth-child(1) .status.unsupported { display: none; }
}
@supports at-rule(@layer) {
  .checks li:nth-child(2) .badge::after { content: "✓"; color: #15803d; }
  .checks li:nth-child(2) .status.supported { display: inline; }
  .checks li:nth-child(2) .status.unsupported { display: none; }
}
@supports at-rule(@scope) {
  .checks li:nth-child(3) .badge::after { content: "✓"; color: #15803d; }
  .checks li:nth-child(3) .status.supported { display: inline; }
  .checks li:nth-child(3) .status.unsupported { display: none; }
}
Browser support:Chrome 148+Firefox Safari Edge 148+
Color & BackgroundsProduction ready

Theme-aware images with light-dark()

light-dark()

light-dark() now accepts image values — gradients, url() references — not just colors, so a single declaration can swap an entire background image between a light and dark variant automatically.

View code

HTML

<button id="theme-toggle" type="button">Toggle theme</button>
<div class="swatch" id="swatch"></div>
<p class="note">If the box above doesn't visibly change, your browser doesn't yet support <code>light-dark()</code> with image values — this is one of the newest, least-supported capabilities in the gallery.</p>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
button { margin-bottom: 1rem; padding: 0.5rem 1rem; border: none; border-radius: 6px; background: #2563eb; color: #fff; font: inherit; cursor: pointer; }
.swatch {
  width: 200px;
  height: 120px;
  border-radius: 12px;
  border: 1px solid #e5e7eb;
  background: #dbeafe; /* fallback if light-dark() with image values is unsupported */
  background: light-dark(
    linear-gradient(135deg, #dbeafe, #bfdbfe),
    linear-gradient(135deg, #1e3a8a, #1e293b)
  );
}
:root.force-dark { color-scheme: dark; }
.note { margin-top: 1rem; font-size: 0.8rem; color: #6b7280; max-width: 300px; }
code { background: #f3f4f6; padding: 0 0.25rem; border-radius: 4px; }

JS

document.getElementById('theme-toggle').addEventListener('click', () => {
  document.documentElement.classList.toggle('force-dark');
});
Browser support:Chrome 123+Firefox 120+Safari 17.4+Edge 123+
LayoutProduction ready

Sticky positioning across independent scroll axes

position: sticky

Lets a sticky element track a different scroll container per axis, so more flexible split-pane and dashboard layouts can each keep their own header pinned independently.

View code

HTML

<div class="grid-2">
  <div class="col">
    <div class="sticky-item">Sticky in this column</div>
    <div class="filler"></div>
  </div>
  <div class="col">
    <div class="filler tall"></div>
  </div>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
.grid-2 { display: flex; gap: 1rem; height: 180px; }
.col { flex: 1; overflow-y: auto; border: 1px solid #e5e7eb; border-radius: 8px; padding: 0.5rem; }
.sticky-item { position: sticky; top: 0; background: #2563eb; color: #fff; padding: 0.5rem; border-radius: 6px; font-size: 0.85rem; }
.filler { height: 80px; }
.filler.tall { height: 400px; background: repeating-linear-gradient(0deg, #f3f4f6, #f3f4f6 20px, #fff 20px, #fff 40px); }
Browser support:Chrome 91+Firefox 59+Safari 7.1+Edge 91+
Scroll & View TransitionsProduction ready

A scroll-linked reading progress bar

animation-timeline: scroll(root)

A fixed bar's scaleX is driven directly by a named scroll-timeline, so it grows in perfect sync with how far the user has scrolled — a page-level reading indicator with no scroll event listener at all.

View code

HTML

<div class="page">
  <div class="bar"><div class="fill"></div></div>
  <div class="content">
    <p>Scroll down to fill the bar ↓</p>
    <p>The bar grows in sync with your scroll position, using a named scroll timeline — no JavaScript.</p>
    <p>The timeline is declared on this scrollable area and shared with the bar outside it via <code>timeline-scope</code>.</p>
    <p>Scroll back up to watch it shrink.</p>
    <p>Keep scrolling…</p>
    <p>Almost there…</p>
    <p>End of content.</p>
  </div>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
.page {
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  overflow: hidden;
  /* Makes the named timeline declared on .content visible to .fill, a sibling of .content */
  timeline-scope: --page-scroll;
}
.bar { height: 5px; background: #e5e7eb; }
.fill {
  height: 100%;
  background: #2563eb;
  transform-origin: left;
  animation: fill-bar linear both;
  animation-timeline: --page-scroll;
}
@keyframes fill-bar { from { transform: scaleX(0); } to { transform: scaleX(1); } }
.content {
  height: 200px;
  overflow-y: auto;
  padding: 1rem;
  scroll-timeline-name: --page-scroll;
  scroll-timeline-axis: block;
}
.content p { font-size: 0.9rem; color: #4b5563; margin: 0 0 2rem 0; }
code { background: #f3f4f6; padding: 0 0.25rem; border-radius: 4px; }
Browser support:Chrome 115+Firefox 157+Safari 26.0+Edge 115+
Color & BackgroundsGrowing support

Organic borders and cut-outs with shape()

border-shape: shape(...)

The shape() function draws an arbitrary border or background outline from move/line/curve commands — blob shapes, notches, and cut-outs that used to require an SVG or clip-path hack.

View code

HTML

<div class="shapes">
  <div class="shape blob"></div>
  <div class="shape outline"></div>
  <div class="shape cutout"></div>
</div>
<p class="note">Same organic path, three ways: a filled blob, a stroked outline, and a square-outside/organic-inside cut-out. Requires browser support for <code>border-shape</code> — shows as plain boxes otherwise.</p>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #1f1f1f; display: flex; flex-direction: column; align-items: center; gap: 1rem; }
.shapes { display: flex; gap: 1rem; align-items: center; }
.shape { width: 90px; height: 90px; }

/* 1. Solid blob — background follows the shape */
.blob {
  background: rebeccapurple;
  border-shape: shape(from 50% 0%, curve to 100% 40% with 100% 0%, curve to 60% 100% with 100% 80%, curve to 0% 60% with 30% 100%, curve to 50% 0% with 0% 30%, close);
}

/* 2. Organic outline — border hugs the shape */
.outline {
  border: 8px solid rebeccapurple;
  border-shape: shape(from 50% 0%, curve to 100% 40% with 100% 0%, curve to 60% 100% with 100% 80%, curve to 0% 60% with 30% 100%, curve to 50% 0% with 0% 30%, close);
}

/* 3. Inner cut-out — square outside, organic inside */
.cutout {
  border: 12px solid rebeccapurple;
  border-shape: inset(0) shape(from 50% 0%, curve to 100% 40% with 100% 0%, curve to 60% 100% with 100% 80%, curve to 0% 60% with 30% 100%, curve to 50% 0% with 0% 30%, close);
}

.note { max-width: 320px; text-align: center; font-size: 0.8rem; color: #6b6b6b; }
code { background: #eeeeee; padding: 0 0.25rem; border-radius: 4px; }
Browser support:Chrome 147+Firefox Safari Edge 147+
LayoutGrowing support

Native masonry layout for grid items

grid-template-rows: masonry

A masonry-style grid track lets items of different heights pack tightly into the next available slot, matching the classic Pinterest-style layout without a JavaScript layout library.

View code

HTML

<div class="masonry">
  <div class="tile" style="height:60px">1</div>
  <div class="tile" style="height:100px">2</div>
  <div class="tile" style="height:40px">3</div>
  <div class="tile" style="height:80px">4</div>
  <div class="tile" style="height:50px">5</div>
  <div class="tile" style="height:70px">6</div>
</div>
<p class="note">Uses <code>column-count</code> as a universal fallback so it always looks right, then upgrades to real masonry where <code>grid-template-rows: masonry</code> or <code>display: grid-lanes</code> is supported.</p>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
.masonry {
  column-count: 3;
  column-gap: 0.75rem;
}
.tile {
  break-inside: avoid;
  margin-bottom: 0.75rem;
  background: #2563eb;
  color: #fff;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
}
@supports (grid-template-rows: masonry) {
  .masonry { display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: masonry; gap: 0.75rem; column-count: initial; }
  .tile { margin-bottom: 0; }
}
@supports (display: grid-lanes) {
  .masonry { display: grid-lanes; grid-template-columns: repeat(3, 1fr); gap: 0.75rem; column-count: initial; }
  .tile { margin-bottom: 0; }
}
.note { margin-top: 1rem; font-size: 0.8rem; color: #6b7280; max-width: 320px; }
code { background: #f3f4f6; padding: 0 0.25rem; border-radius: 4px; }
Browser support:Chrome Firefox Safari 26.4+Edge
Scroll & View TransitionsProduction ready

A stacking-cards scroll effect with pure CSS

position: sticky + animation-timeline: view()

position: sticky with staggered offsets piles cards on top of each other as the user scrolls, while a view() animation-timeline scales each card down as the next one slides over it.

View code

HTML

<div class="stack-scroll">
  <div class="card card-1">Card 1</div>
  <div class="card card-2">Card 2</div>
  <div class="card card-3">Card 3</div>
  <div class="spacer"></div>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; }
.stack-scroll { height: 220px; overflow-y: auto; border: 1px solid #e5e7eb; border-radius: 8px; padding: 1rem; }
.card {
  position: sticky;
  height: 120px;
  margin-bottom: 1rem;
  border-radius: 12px;
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 1.1rem;
  animation: stack-recede linear both;
  animation-timeline: view();
  animation-range: exit -60px exit 0px;
}
.card-1 { top: 0; background: #2563eb; }
.card-2 { top: 12px; background: #7c3aed; }
.card-3 { top: 24px; background: #db2777; }
@keyframes stack-recede { to { transform: scale(0.92); filter: brightness(0.85); } }
.spacer { height: 220px; }
Browser support:Chrome 115+Firefox 157+Safari 26.0+Edge 115+
Scroll & View TransitionsProduction ready

A scroll-spy table of contents with view-timeline

view-timeline-name, animation-timeline

Each section declares a named view-timeline; the matching table-of-contents link consumes that timeline to animate its own active state as the section scrolls through the viewport — no IntersectionObserver required.

View code

HTML

<div class="layout">
  <nav class="toc">
    <a href="#s1" class="toc-link">① Intro</a>
    <a href="#s2" class="toc-link">② Methods</a>
    <a href="#s3" class="toc-link">③ Results</a>
  </nav>
  <div class="sections">
    <section id="s1" class="section">
      <h3>Introduction</h3>
      <p>Scroll down — the active link updates with no JavaScript at all.</p>
    </section>
    <section id="s2" class="section">
      <h3>Methods</h3>
      <p>Each section has a named <code>view-timeline-name</code>. The wrapper shares it with <code>timeline-scope</code>.</p>
    </section>
    <section id="s3" class="section">
      <h3>Results</h3>
      <p>The matching nav link consumes the timeline via <code>animation-timeline</code>.</p>
    </section>
  </div>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
.layout {
  display: flex;
  gap: 1rem;
  /* Common ancestor of both the nav links (consumers) and sections (sources) */
  timeline-scope: --s1-visible, --s2-visible, --s3-visible;
}
.toc { display: flex; flex-direction: column; gap: 0.35rem; min-width: 90px; flex-shrink: 0; }
.toc-link {
  padding: 0.3rem 0.5rem;
  border-radius: 4px;
  border-left: 2px solid transparent;
  color: #9ca3af;
  text-decoration: none;
  font-size: 0.85rem;
  animation: mark-active linear both;
  animation-range: entry 0% exit 100%;
}
.toc-link:nth-child(1) { animation-timeline: --s1-visible; }
.toc-link:nth-child(2) { animation-timeline: --s2-visible; }
.toc-link:nth-child(3) { animation-timeline: --s3-visible; }
@keyframes mark-active {
  0%, 100% { color: #9ca3af; font-weight: 400; border-left-color: transparent; }
  20%, 80% { color: #2563eb; font-weight: 700; border-left-color: #2563eb; }
}
.sections { flex: 1; height: 200px; overflow-y: auto; border-left: 1px solid #e5e7eb; padding-left: 1rem; }
.section { display: block; min-height: 180px; padding-block: 0.5rem; border-bottom: 1px dashed #e5e7eb; }
.section:nth-child(1) { view-timeline-name: --s1-visible; }
.section:nth-child(2) { view-timeline-name: --s2-visible; }
.section:nth-child(3) { view-timeline-name: --s3-visible; }
.section h3 { display: block; margin: 0 0 0.35rem; font-size: 1em; font-weight: 700; }
.section p { margin: 0; font-size: 0.85rem; color: #4b5563; }
code { background: #f3f4f6; padding: 0 0.25rem; border-radius: 4px; }
Browser support:Chrome 115+Firefox 157+Safari 26.0+Edge 115+
Scroll & View TransitionsProduction ready

Shrinking a sticky header as the page scrolls

animation-timeline: scroll(root) + position: sticky

A scroll-driven animation tied to a named scroll-timeline shrinks a position: sticky header's padding over the first stretch of scroll — a common product-site effect that used to need a scroll event listener.

View code

HTML

<div class="scroll-area">
  <header class="site-header">
    <span class="logo">BRAND</span>
    <span class="hint">Scroll down ↓</span>
  </header>
  <div class="content">
    <p>The header above is <code>position: sticky</code>.</p>
    <p>Its padding shrinks as you scroll, driven by <code>scroll(nearest block)</code> — no JS.</p>
    <p>Scroll back up to watch it expand again.</p>
    <p>Works on any scrollable container, not just the page root.</p>
    <p>Keep scrolling to see the full effect play out smoothly.</p>
  </div>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
.scroll-area { height: 220px; overflow-y: auto; border: 1px solid #e5e7eb; border-radius: 8px; }
.site-header {
  position: sticky;
  top: 0;
  background: #2563eb;
  color: #fff;
  padding-block: 1.25rem;
  padding-inline: 1rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* scroll(nearest block) finds the nearest scrollable ancestor automatically — no named timeline needed */
  animation: shrink-nav linear both;
  animation-timeline: scroll(nearest block);
  animation-range: 0px 120px;
}
.logo { font-weight: 700; letter-spacing: 0.05em; }
.hint { font-size: 0.75rem; color: #dbeafe; }
@keyframes shrink-nav {
  to { padding-block: 0.5rem; }
}
.content { padding: 1rem; min-height: 420px; }
.content p { font-size: 0.9rem; color: #4b5563; margin: 0 0 1.25rem 0; }
code { background: #f3f4f6; padding: 0 0.25rem; border-radius: 4px; }
Browser support:Chrome 115+Firefox 157+Safari 26.0+Edge 115+
Selectors & CombinatorsGrowing support

Styling the browser's own Find-in-Page matches

::search-text, ::search-text:current

::search-text and ::search-text:current let a page style the highlight the browser draws around a user's Ctrl/Cmd+F search matches, distinguishing the currently-focused match from the rest.

View code

HTML

<p>The <span class="match">accent-color</span> property. Current match: <span class="match current">accent</span> styled with <code>:current</code>.</p>
<p class="note">These highlights are simulated here — <code>::search-text</code> only applies to real matches from the browser's Ctrl/Cmd+F Find in Page.</p>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; max-width: 360px; }
/* Real usage (applies to actual Find in Page matches):
::search-text { background: yellow; }
::search-text:current { background: orange; }
*/
.match { background: #fef08a; border-radius: 2px; padding: 0 2px; }
.match.current { background: #fb923c; color: #111827; }
.note { font-size: 0.8rem; color: #6b7280; }
code { background: #f3f4f6; padding: 0 0.25rem; border-radius: 4px; }
Browser support:Chrome 144+Firefox Safari Edge 144+
Animation & TransitionsProduction ready

An animated accordion with zero JavaScript

details[name] + grid-template-rows: 0fr → 1fr

Tailwind: grid-rows-[0fr] + peer-open:grid-rows-[1fr] on the sibling element

Native <details>/<summary> plus a transition from grid-template-rows: 0fr to 1fr collapses and reveals content smoothly — CSS can interpolate fr track sizes even though it still can't transition height: auto. Giving each <details> the same name attribute also turns them into an exclusive group, so opening one closes the others with zero JavaScript.

View code

HTML

<div class="accordion">
  <div class="accordion-item">
    <details name="accordion-demo">
      <summary aria-controls="accordion-content-1">
        What is the 0fr &rarr; 1fr trick?
        <svg class="chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
        <polyline points="6 9 12 15 18 9"></polyline>
      </svg>
      </summary>
    </details>
    <div class="content" id="accordion-content-1">
      <div class="body">
        Wrapping the content in a grid row and transitioning <code>grid-template-rows</code> from <code>0fr</code> to <code>1fr</code> collapses and reveals it smoothly — CSS can interpolate <code>fr</code> track sizes even though it still can't transition <code>height: auto</code>.
      </div>
    </div>
  </div>
  <div class="accordion-item">
    <details name="accordion-demo">
      <summary aria-controls="accordion-content-2">
        Why put the content outside &lt;details&gt;?
        <svg class="chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
        <polyline points="6 9 12 15 18 9"></polyline>
      </svg>
      </summary>
    </details>
    <div class="content" id="accordion-content-2">
      <div class="body">
        Placing the animated div as a sibling of <code>&lt;details&gt;</code> sidesteps Chrome's <code>::details-content</code> / <code>content-visibility</code> mechanism, which would otherwise block a custom height transition from running.
      </div>
    </div>
  </div>
  <div class="accordion-item">
    <details name="accordion-demo">
      <summary aria-controls="accordion-content-3">
        How do exclusive groups work?
        <svg class="chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
        <polyline points="6 9 12 15 18 9"></polyline>
      </svg>
      </summary>
    </details>
    <div class="content" id="accordion-content-3">
      <div class="body">
        Give each <code>&lt;details&gt;</code> element the same <code>name</code> attribute and the browser enforces that only one can be open at a time — try opening this one, the others will close automatically, no JavaScript required.
      </div>
    </div>
  </div>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #1f1f1f; }
.accordion { max-width: 320px; border: 1px solid #e0e0e0; border-radius: 8px; overflow: hidden; }
.accordion-item + .accordion-item { border-top: 1px solid #e0e0e0; }

summary {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  list-style: none;
  cursor: pointer;
  user-select: none;
  padding: 0.625rem 1rem;
  font-weight: 500;
  transition: background-color 0.15s ease, color 0.15s ease;
}
summary::-webkit-details-marker { display: none; }
summary:hover,
details[open] summary {
  background: #2563eb;
  color: #fff;
}
summary:focus-visible {
  outline: 2px solid #2563eb;
  outline-offset: -2px;
}

.chevron {
  margin-left: auto;
  flex-shrink: 0;
  transition: rotate 0.3s ease;
}
details[open] .chevron {
  rotate: 180deg;
}

/* Animated div is a sibling of <details>, not inside it — this sidesteps */
/* Chrome's ::details-content / content-visibility mechanism, which would */
/* otherwise block a custom height transition. */
.content {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.3s ease;
}
details[open] + .content {
  grid-template-rows: 1fr;
  border-top: 1px solid #e0e0e0;
}
.body { overflow: hidden; padding: 0 1rem; font-size: 0.9rem; color: #4b4b4b; }
details[open] + .content .body { padding-block: 0.75rem; }

code { background: #eeeeee; padding: 0 0.25rem; border-radius: 4px; }
Browser support:Chrome 120+Firefox 130+Safari 17.2+Edge 120+
Functions & ValuesProduction ready

Deriving per-item values with mod() and calc()

mod(), rem()

mod() and rem() join calc() as real CSS math functions, so patterns like alternating row shades or repeating layouts can be computed from an index directly in CSS instead of a build-time loop.

View code

HTML

<div class="grid">
  <div class="cell" style="--i:0">0</div>
  <div class="cell" style="--i:1">1</div>
  <div class="cell" style="--i:2">2</div>
  <div class="cell" style="--i:3">3</div>
  <div class="cell" style="--i:4">4</div>
  <div class="cell" style="--i:5">5</div>
  <div class="cell" style="--i:6">6</div>
  <div class="cell" style="--i:7">7</div>
  <div class="cell" style="--i:8">8</div>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
.grid { display: grid; grid-template-columns: repeat(3, 48px); gap: 0.5rem; }
.cell {
  height: 48px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 6px;
  color: #fff;
  font-weight: 700;
  background: hsl(220 80% calc(35% + mod(var(--i), 3) * 15%));
}
Browser support:Chrome 79+Firefox 75+Safari 13.1+Edge 79+
Forms & UIProduction ready

Recoloring native form controls in one line

accent-color

Tailwind: v3.2+ accent-* utilities, e.g. accent-blue-500

accent-color restyles the checked state of checkboxes, radio buttons, range sliders, and progress bars to match a brand color, without abandoning the browser's native, accessible control rendering.

View code

HTML

<label><input type="checkbox" checked class="branded"> Branded checkbox</label>
<label><input type="radio" name="r" checked class="branded"> Option A</label>
<label><input type="radio" name="r" class="branded"> Option B</label>
<label>Volume <input type="range" class="branded" value="60"></label>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; display: flex; flex-direction: column; gap: 0.75rem; align-items: flex-start; }
.branded { accent-color: #db2777; }
label { display: flex; align-items: center; gap: 0.5rem; }
Browser support:Chrome 93+Firefox 92+Safari 26.2+Edge 93+
Selectors & CombinatorsProduction ready

Scoping styles to a subtree with @scope

@scope

@scope (.root) limits a block of rules to matches inside that root — and can even carve out an inner boundary — giving plain CSS a form of the encapsulation Shadow DOM or naming conventions used to require.

View code

HTML

<div class="scope-a">
  <p>Blue text here (inside .scope-a)</p>
</div>
<div class="scope-b">
  <p>Pink text here (inside .scope-b)</p>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; }
@scope (.scope-a) {
  p { color: #2563eb; font-weight: 600; }
}
@scope (.scope-b) {
  p { color: #db2777; font-weight: 600; }
}
Browser support:Chrome 118+Firefox 146+Safari 17.4+Edge 118+
Forms & UIProduction ready

Validating a field only after the user interacts

:user-valid, :user-invalid

Tailwind: v3.4+ user-valid: and user-invalid: variants

:user-valid and :user-invalid match like :valid/:invalid, but only once the user has actually interacted with the field — so a required input doesn't show an alarming red error before anyone's had a chance to type.

View code

HTML

<form>
  <label>Phone number (10 digits)
    <input type="tel" pattern="[0-9]{10}" placeholder="5551234567" required aria-describedby="phone-invalid-msg phone-valid-msg">
  </label>
  <p class="msg invalid-msg" id="phone-invalid-msg">✗ Enter exactly 10 digits</p>
  <p class="msg valid-msg" id="phone-valid-msg">✓ Looks good!</p>
</form>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
form { display: flex; flex-direction: column; gap: 0.5rem; max-width: 280px; }
label { display: flex; flex-direction: column; gap: 0.35rem; font-size: 0.9rem; }
input { font: inherit; padding: 0.5rem 0.75rem; border: 1px solid #d1d5db; border-radius: 6px; }
.msg { margin: 0; font-size: 0.85rem; display: none; }
input:user-invalid ~ .invalid-msg { display: block; color: #b91c1c; }
input:user-valid ~ .valid-msg { display: block; color: #15803d; }
Browser support:Chrome 119+Firefox 88+Safari 16.5+Edge 119+
Components & InteractivityProduction ready

Native popovers with the popover attribute

popover, popovertarget

popover and popovertarget give any element top-layer rendering, light-dismiss on outside click, Escape-to-close, and automatic focus handling — all the fiddly behavior a hand-rolled dropdown used to need JS for.

View code

HTML

<button popovertarget="pop">Toggle popover</button>
<div id="pop" popover>
  <p>I'm a native popover — top-layer, dismisses on outside click or Esc, no JS.</p>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
button { padding: 0.6rem 1.2rem; border: none; border-radius: 8px; background: #2563eb; color: #fff; font: inherit; cursor: pointer; }
[popover] { border: none; border-radius: 12px; padding: 1.25rem; max-width: 260px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); }
[popover]::backdrop { background: rgba(17,24,39,0.15); }
Browser support:Chrome 114+Firefox 125+Safari 17.0+Edge 114+
Components & InteractivityProduction ready

Controlling dialogs declaratively with commandfor

commandfor, command

The commandfor and command attributes let a plain <button> show, close, or toggle a <dialog> or popover directly from HTML — no click listener or imperative .showModal() call needed.

View code

HTML

<button commandfor="dlg" command="show-modal">Show dialog (no JS!)</button>
<dialog id="dlg" aria-label="Example dialog">
  <p>Opened declaratively via <code>commandfor</code> + <code>command</code>.</p>
  <button commandfor="dlg" command="close">Close</button>
</dialog>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
button { padding: 0.6rem 1.2rem; border: none; border-radius: 8px; background: #2563eb; color: #fff; font: inherit; cursor: pointer; }
dialog { border: none; border-radius: 12px; padding: 1.5rem; box-shadow: 0 10px 30px rgba(0,0,0,0.15); }
dialog::backdrop { background: rgba(17,24,39,0.4); }
code { background: #f3f4f6; padding: 0 0.25rem; border-radius: 4px; }
Browser support:Chrome 135+Firefox 144+Safari 26.2+Edge 135+
Forms & UIGrowing support

Fully restyling <select> with appearance: base-select

appearance: base-select

Opts a native <select> into a styleable rendering mode, including its dropdown picker and options, so teams can finally style a real <select> instead of reaching for a custom-built listbox component.

View code

HTML

<label for="base-select-demo" style="display:block;margin-bottom:0.5rem;">Choose a plan</label>
<select id="base-select-demo" class="base-select">
  <option>Customized ✓</option>
  <option>Option two</option>
  <option>Option three</option>
</select>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
select.base-select {
  appearance: base-select;
  padding: 0.6rem 1rem;
  border-radius: 8px;
  border: 1px solid #d1d5db;
  font: inherit;
  background: #fff;
}
select.base-select::picker(select) {
  border-radius: 10px;
  border: 1px solid #d1d5db;
  padding: 0.25rem;
}
select.base-select option {
  padding: 0.5rem 0.75rem;
  border-radius: 6px;
}
select.base-select option:checked {
  background: #eff6ff;
  color: #1d4ed8;
}
Browser support:Chrome 135+Firefox Safari 27+Edge 135+
Color & BackgroundsProduction ready

Perceptually uniform color with oklch()

oklch(), oklab()

Tailwind: v4's entire default color palette is defined in oklch for consistent perceived brightness

oklch(lightness chroma hue) keeps perceived brightness consistent as hue changes, unlike hsl() — which makes it much easier to build accessible color scales that don't have surprise-dark or surprise-bright entries.

View code

HTML

<div class="row">
  <div class="swatch" style="background:oklch(70% 0.2 20)"></div>
  <div class="swatch" style="background:oklch(70% 0.2 90)"></div>
  <div class="swatch" style="background:oklch(70% 0.2 160)"></div>
  <div class="swatch" style="background:oklch(70% 0.2 230)"></div>
  <div class="swatch" style="background:oklch(70% 0.2 300)"></div>
</div>
<p class="note">Same lightness (70%) and chroma (0.2) at five different hues — they all look equally bright.</p>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
.row { display: flex; gap: 0.5rem; }
.swatch { width: 56px; height: 56px; border-radius: 8px; }
.note { margin-top: 1rem; font-size: 0.85rem; color: #4b5563; max-width: 340px; }
Browser support:Chrome 111+Firefox 113+Safari 15.4+Edge 111+
Color & BackgroundsProduction ready

Mixing two colors natively in any color space

color-mix()

color-mix() blends two colors by a percentage in a chosen color space (like oklch or srgb), replacing the Sass/JS color-math functions teams used to reach for to generate tints and shades.

View code

HTML

<div class="row">
  <div class="swatch a"></div>
  <div class="swatch b"></div>
  <div class="swatch c"></div>
  <div class="swatch d"></div>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; }
.row { display: flex; gap: 0.75rem; }
.swatch { width: 64px; height: 64px; border-radius: 8px; }
.a { background: royalblue; }
.b { background: color-mix(in oklch, royalblue 75%, white); }
.c { background: color-mix(in oklch, royalblue 50%, white); }
.d { background: color-mix(in oklch, royalblue 25%, white); }
Browser support:Chrome 111+Firefox 113+Safari 16.2+Edge 111+
Typography & TextProduction ready

Trimming invisible leading space above and below text

text-box: cap alphabetic

text-box (a shorthand for text-box-trim and text-box-edge) removes the half-leading space baked into every line box, so icons, badges, and single-line labels can finally align on their actual glyph edges.

View code

HTML

<div class="row">
  <span class="pill default">SEND</span>
  <span class="pill trimmed">SEND</span>
</div>
<p class="note">Same padding on both — the second uses <code>text-box: cap alphabetic</code> to remove the invisible space above/below the capital letters.</p>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
.row { display: flex; gap: 1rem; align-items: center; }
.pill { display: inline-block; padding: 0.4rem 1rem; border-radius: 999px; background: #2563eb; color: #fff; font-weight: 700; outline: 1px dashed #93c5fd; outline-offset: -1px; }
.trimmed { text-box: cap alphabetic; }
.note { margin-top: 1rem; font-size: 0.85rem; color: #4b5563; max-width: 320px; }
code { background: #f3f4f6; padding: 0 0.25rem; border-radius: 4px; }
Browser support:Chrome 133+Firefox 154+Safari 18.2+Edge 132+
Typography & TextProduction ready

Keeping fallback fonts visually consistent

font-size-adjust

Sets a target x-height ratio so a fallback font renders at a visually matched size to the intended web font, reducing the layout jump that happens while a custom font is still loading.

View code

HTML

<p class="no-adjust">Fallback font (no font-size-adjust)</p>
<p class="adjusted">Fallback font (font-size-adjust: 0.5)</p>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
p { font-size: 1.5rem; font-family: "Some Custom Font", Georgia, serif; margin: 0 0 1rem 0; }
.adjusted { font-size-adjust: 0.5; }
Browser support:Chrome 127+Firefox 118+Safari 17+Edge 127+
LayoutProduction ready

Fixing the mobile 100vh bug with dynamic viewport units

dvh, svh, lvh, dvw

Tailwind: v3.4+ h-dvh, h-svh, h-lvh (and matching min-h-*/max-h-* variants)

svh, lvh and dvh give three explicit answers to 'how tall is the viewport': smallest, largest, and dynamically-updating-with-the-browser-UI — ending the classic issue where 100vh overshoots the visible area on mobile.

View code

HTML

<div class="row">
  <div class="bar vh">100vh</div>
  <div class="bar svh">100svh</div>
  <div class="bar dvh">100dvh</div>
</div>
<p class="note">On a real mobile browser, the address bar showing/hiding changes <code>dvh</code> live while <code>vh</code> stays locked to the largest size.</p>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
.row { display: flex; gap: 1rem; height: 160px; }
.bar { flex: 1; border-radius: 8px; display: flex; align-items: flex-end; justify-content: center; padding-bottom: 0.5rem; color: #fff; font-size: 0.8rem; font-weight: 600; }
.vh { background: #9ca3af; height: 100%; }
.svh { background: #60a5fa; height: 92%; }
.dvh { background: #2563eb; height: 100%; }
.note { font-size: 0.85rem; color: #4b5563; max-width: 360px; }
code { background: #f3f4f6; padding: 0 0.25rem; border-radius: 4px; }
Browser support:Chrome 108+Firefox 101+Safari 15.4+Edge 108+
Typography & TextProduction ready

Balancing headline line breaks automatically

text-wrap: balance | pretty

Tailwind: v3.4+ text-balance and text-pretty utilities

text-wrap: balance evens out the number of characters per line so multi-line headings don't end in a single lonely word — a one-line fix for a problem designers used to solve by hand with manual line breaks.

View code

HTML

<h2 class="balanced">Design systems make teams faster and products more consistent</h2>
<hr>
<h2 class="unbalanced">Design systems make teams faster and products more consistent</h2>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; max-width: 360px; }
h2 { font-size: 1.4rem; line-height: 1.3; }
.balanced { text-wrap: balance; }
.unbalanced { text-wrap: wrap; }
hr { border: none; border-top: 1px solid #e5e7eb; margin: 1.5rem 0; }
Browser support:Chrome 130+Firefox 121+Safari 17.5+Edge 130+
Animation & TransitionsProduction ready

Animating elements in with @starting-style

@starting-style

Tailwind: v4 starting: variant, e.g. starting:opacity-0

Defines the 'before' state for an element that's just been inserted into the DOM (or switched from display:none), so entry transitions finally work with plain CSS transitions instead of a JS-timed class toggle.

View code

HTML

<button id="add-btn">Add a card</button>
<div id="list" class="list"></div>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
.list { display: flex; gap: 0.75rem; margin-top: 1rem; flex-wrap: wrap; }
.toast {
  padding: 0.75rem 1rem;
  border-radius: 8px;
  background: #eff6ff;
  color: #1d4ed8;
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.4s, transform 0.4s;

  @starting-style {
    opacity: 0;
    transform: translateY(8px);
  }
}
button { padding: 0.5rem 1rem; border: none; border-radius: 6px; background: #2563eb; color: #fff; font: inherit; cursor: pointer; }

JS

let n = 0;
document.getElementById('add-btn').addEventListener('click', () => {
  n++;
  const div = document.createElement('div');
  div.className = 'toast';
  div.textContent = 'Card ' + n;
  document.getElementById('list').appendChild(div);
});
Browser support:Chrome 117+Firefox 129+Safari 17.5+Edge 117+
LayoutProduction ready

Anchoring a tooltip to any element with pure CSS

anchor-name, position-anchor

anchor-name and position-anchor tether a positioned element to any other element on the page via anchor() functions — the CSS replacement for JS-based tooltip and popover positioning libraries.

View code

HTML

<div class="wrap">
  <button id="anchor-btn" class="anchor-btn">Anchor element</button>
  <div id="tooltip" class="tooltip" role="tooltip">I'm anchored above the button</div>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; height: 160px; }
.wrap { display: flex; justify-content: center; margin-top: 3rem; }
.anchor-btn {
  anchor-name: --btn;
  padding: 0.6rem 1.2rem;
  border: none;
  border-radius: 8px;
  background: #2563eb;
  color: #fff;
  font: inherit;
  cursor: pointer;
}
.tooltip {
  position: fixed;
  position-anchor: --btn;
  bottom: anchor(top);
  left: anchor(center);
  translate: -50% -8px;
  background: #2563eb;
  color: #fff;
  padding: 0.4rem 0.75rem;
  border-radius: 6px;
  font-size: 0.8rem;
  white-space: nowrap;
}
Browser support:Chrome 125+Firefox 147+Safari 26+Edge 125+
Scroll & View TransitionsProduction ready

Morphing between UI states with the View Transitions API

view-transition-name

document.startViewTransition() plus a shared view-transition-name lets an element smoothly morph into its new position or appearance across a DOM update — the kind of transition that used to require a JS animation library.

View code

HTML

<div class="vt-demo">
  <div id="hero" class="hero pos-a">●</div>
  <button id="toggle">Toggle position</button>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; }
.vt-demo { display: flex; flex-direction: column; gap: 1rem; height: 140px; position: relative; }
.hero { width: 48px; height: 48px; border-radius: 50%; background: #2563eb; color: #fff; display: flex; align-items: center; justify-content: center; view-transition-name: hero; position: absolute; }
.pos-a { left: 0; top: 0; }
.pos-b { left: calc(100% - 48px); top: 60px; }
button { position: absolute; bottom: 0; right: 0; padding: 0.5rem 1rem; border: none; border-radius: 6px; background: #2563eb; color: #fff; font: inherit; cursor: pointer; }

JS

const hero = document.getElementById('hero');
const btn = document.getElementById('toggle');
btn.addEventListener('click', () => {
  const swap = () => hero.classList.toggle('pos-b');
  if (document.startViewTransition) {
    document.startViewTransition(swap);
  } else {
    swap();
  }
});
Browser support:Chrome 111+Firefox 144+Safari 18+Edge 111+
Scroll & View TransitionsProduction ready

Linking an animation to scroll position

animation-timeline: scroll()

A named scroll-timeline turns any scrollable element into an animation driver, so progress bars and reveal effects can track scroll position with zero JavaScript and zero scroll event listeners.

View code

HTML

<div class="demo-wrap">
  <div class="bar"><div class="fill"></div></div>
  <div class="scroller">
    <div class="track">
      <div class="chip">container queries</div>
      <div class="chip">:has()</div>
      <div class="chip">@layer</div>
      <div class="chip">view transitions</div>
      <div class="chip">anchor positioning</div>
      <div class="chip">→ scroll me →</div>
    </div>
  </div>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
.demo-wrap {
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  overflow: hidden;
  /* Makes the named timeline declared inside .scroller visible to .fill, a sibling of .scroller */
  timeline-scope: --track-scroll;
}
.bar { height: 6px; background: #e5e7eb; }
.fill {
  height: 100%;
  background: #2563eb;
  transform-origin: left;
  animation: grow-bar linear;
  animation-timeline: --track-scroll;
}
@keyframes grow-bar { from { transform: scaleX(0); } to { transform: scaleX(1); } }
.scroller {
  overflow-x: auto;
  scroll-timeline-name: --track-scroll;
  scroll-timeline-axis: x;
}
.track { display: flex; gap: 0.75rem; padding: 1rem; width: max-content; }
.chip { background: #eff6ff; color: #1d4ed8; padding: 0.5rem 1rem; border-radius: 999px; font-size: 0.85rem; white-space: nowrap; }
Browser support:Chrome 115+Firefox 157+Safari 26.0+Edge 115+
LayoutProduction ready

Writing-direction-agnostic spacing with logical properties

padding-inline, margin-block…

Tailwind: Used natively throughout — px/py map to padding-inline/padding-block, ps/pe to the logical start/end

padding-inline, margin-block, border-inline-start and friends follow the document's writing direction instead of physical left/right/top/bottom, so the same rules work correctly in RTL and vertical writing modes for free.

View code

HTML

<div class="demo">
  <div class="box ltr">padding-inline / margin-block</div>
  <div class="box rtl" dir="rtl">Same CSS, dir="rtl"</div>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
.demo { display: flex; gap: 1rem; flex-wrap: wrap; }
.box {
  padding-inline: 1.5rem;
  padding-block: 0.75rem;
  margin-block-end: 0.5rem;
  border-inline-start: 4px solid #2563eb;
  background: #f3f4f6;
  border-radius: 0 8px 8px 0;
  max-width: 220px;
}
.rtl { border-radius: 8px 0 0 8px; }
Browser support:Chrome 89+Firefox 66+Safari 15+Edge 89+
Cascade ControlProduction ready

Ordering the cascade explicitly with @layer

@layer

Groups styles into named layers compared by declaration order first — a later layer always beats an earlier one regardless of selector specificity, making it far more predictable to override utility or third-party CSS.

View code

HTML

<button class="btn">Layered button</button>

CSS

@layer base, theme;

@layer base {
  .btn { background: #6b7280; color: #fff; border: none; padding: 0.75rem 1.5rem; border-radius: 8px; font: inherit; font-weight: 600; cursor: pointer; }
}

@layer theme {
  /* Wins over base despite equal specificity, because "theme" is declared after "base" */
  .btn { background: #2563eb; }
}

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; display: flex; justify-content: center; align-items: center; height: 160px; }
Browser support:Chrome 99+Firefox 97+Safari 15.4+Edge 99+
Responsive & Container QueriesProduction ready

Defining and querying a container's own size

@container

Tailwind: v3.2+ container utility, plus @container responsive variants like @lg:flex

container-type turns an element into a query container, so descendants can respond with @container rules based on the container's own width — true component-driven responsiveness instead of viewport-only media queries.

View code

HTML

<div class="resizable">
  <div class="card">
    <div class="thumb"></div>
    <div class="body">
      <h3>Card title</h3>
      <p>Layout switches to a row once this container is wide enough.</p>
    </div>
  </div>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
.resizable { container-type: inline-size; resize: horizontal; overflow: auto; width: 220px; max-width: 100%; border: 2px dashed #9ca3af; padding: 1rem; }
.card { display: flex; flex-direction: column; gap: 0.75rem; }
.thumb { width: 100%; height: 60px; border-radius: 8px; background: #2563eb; }
.card h3 { margin: 0; }
.card p { margin: 0; color: #4b5563; font-size: 0.9rem; }
@container (min-width: 380px) {
  .card { flex-direction: row; align-items: center; }
  .thumb { width: 90px; height: 90px; flex-shrink: 0; }
}
Browser support:Chrome 106+Firefox 110+Safari 16+Edge 106+
LayoutProduction ready

Aligning cards of different lengths with subgrid

grid-template-rows: subgrid

Tailwind: v3.4+ col-subgrid and row-subgrid utilities

Lets a nested grid inherit its parent's row or column tracks, so cards with different amounts of content can still line up their headings, body text, and buttons on the same rows — no extra wrapper markup or JS measuring needed.

View code

HTML

<div class="cards">
  <article class="card">
    <h3>Starter</h3>
    <p>Good for trying things out.</p>
    <button>Choose plan</button>
  </article>
  <article class="card">
    <h3>Pro</h3>
    <p>More storage, more seats, and priority support for growing teams that need it.</p>
    <button>Choose plan</button>
  </article>
  <article class="card">
    <h3>Enterprise</h3>
    <p>Custom limits and a dedicated account manager.</p>
    <button>Choose plan</button>
  </article>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
.cards {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: repeat(3, auto);
  gap: 1.5rem;
}
.card {
  display: grid;
  grid-row: span 3;
  grid-template-rows: subgrid;
  gap: 0.75rem;
  border: 1px solid #d1d5db;
  border-radius: 12px;
  padding: 1.25rem;
}
.card h3 { margin: 0; }
.card p { margin: 0; color: #4b5563; }
.card button {
  align-self: end;
  padding: 0.6rem 1rem;
  border: none;
  border-radius: 6px;
  background: #2563eb;
  color: #fff;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}
.card button:hover { background: #1d4ed8; }
Browser support:Chrome 117+Firefox 71+Safari 16+Edge 117+
Typography & TextProduction ready

Font-size-proportional spacing with the lh unit

lh / rlh units

A length unit equal to the element's own computed line-height, so margins scale automatically when font-size changes instead of needing separate spacing values per heading level.

View code

HTML

<div class="rhythm">
  <h2>Small heading</h2>
  <p>This paragraph sits exactly one line-height below the heading above it, no matter that heading's font size.</p>
  <h2 class="big">Big heading</h2>
  <p>Same spacing rule, same visual rhythm — because the gap is defined in <code>lh</code>, not a fixed pixel value.</p>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
.rhythm h2 {
  margin: 0 0 1lh 0;
  line-height: 1.2;
}
.rhythm h2.big { font-size: 2.5rem; }
.rhythm p { margin: 0 0 2lh 0; }
code { background: #f3f4f6; padding: 0 0.25rem; border-radius: 4px; }
Browser support:Chrome 109+Firefox 120+Safari 16.4+Edge 109+
Selectors & CombinatorsProduction ready

Styling a form row based on its own invalid input

:has()

Tailwind: v3.4+ has-* variant, e.g. has-[input:checked]:bg-blue-500

A relational pseudo-class that matches an element based on what's inside or after it, enabling long-requested 'parent selector' and 'previous sibling' style rules in plain CSS.

View code

HTML

<form>
  <div class="row">
    <label for="email">Email</label>
    <input id="email" type="email" value="not-an-email" required>
  </div>
  <div class="row">
    <label for="name">Name</label>
    <input id="name" type="text" value="Ada Lovelace" required>
  </div>
</form>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
form { display: flex; flex-direction: column; gap: 1rem; max-width: 320px; }
.row {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  padding: 0.75rem;
  border-radius: 8px;
  border: 1px solid transparent;
}
.row:has(input:invalid) {
  background: #fef2f2;
  border-color: #fecaca;
}
.row:has(input:invalid) label { color: #b91c1c; }
input { font: inherit; padding: 0.5rem 0.75rem; border: 1px solid #d1d5db; border-radius: 6px; }
Browser support:Chrome 105+Firefox 121+Safari 15.4+Edge 105+
Selectors & CombinatorsProduction ready

Native CSS nesting with the & selector

CSS Nesting

Lets you nest one style rule inside another using the & nesting selector, parsed directly by the browser — no Sass/Less build step required.

View code

HTML

<button class="btn">Hover or click me</button>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; display: flex; justify-content: center; align-items: center; height: 160px; }
.btn {
  padding: 0.75rem 1.5rem;
  border: none;
  border-radius: 8px;
  background: #2563eb;
  color: #fff;
  font: inherit;
  font-weight: 600;
  cursor: pointer;

  &:hover {
    background: #1d4ed8;
  }

  &:active {
    transform: translateY(1px);
  }
}
Browser support:Chrome 120+Firefox 117+Safari 17.2+Edge 120+
Functions & ValuesProduction ready

Animatable custom properties with @property

@property

Registers a custom property with a type, inheritance behavior, and initial value — which, among other benefits, finally makes CSS custom properties animate smoothly like built-in properties instead of jumping instantly.

View code

HTML

<div class="swatch"></div>
<p>Hover the box — the color transition is smooth because <code>--highlight</code> is a registered, typed custom property.</p>

CSS

@property --highlight {
  syntax: '<color>';
  inherits: false;
  initial-value: #2563eb;
}

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
.swatch {
  width: 160px;
  height: 100px;
  border-radius: 12px;
  background: var(--highlight);
  transition: --highlight 0.6s ease;
}
.swatch:hover { --highlight: #dc2626; }
code { background: #f3f4f6; padding: 0 0.25rem; border-radius: 4px; }
Browser support:Chrome 85+Firefox 128+Safari 16.4+Edge 85+
Responsive & Container QueriesProduction ready

Fluid type sized to its container with cqi

cqi container query units

Units like cqi and cqw are sized relative to a query container's own dimensions rather than the viewport, so a component's type can scale fluidly based on the space it's actually given — a sidebar vs. a full-width card.

View code

HTML

<div class="resizable">
  <div class="card">
    <h3>Resize me</h3>
    <p>Drag the bottom-right corner of the outer box. The heading font-size below is set in <code>cqi</code>, so it scales with this container's width — not the viewport.</p>
  </div>
</div>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
.resizable {
  container-type: inline-size;
  resize: horizontal;
  overflow: auto;
  width: 320px;
  max-width: 100%;
  border: 2px dashed #9ca3af;
  padding: 1rem;
}
.card h3 {
  font-size: clamp(1rem, 8cqi, 3rem);
  margin: 0 0 0.5rem 0;
}
code { background: #f3f4f6; padding: 0 0.25rem; border-radius: 4px; }
Browser support:Chrome 105+Firefox 110+Safari 16+Edge 105+
Forms & UIProduction ready

Auto-growing form fields with field-sizing

field-sizing: content

Lets form controls like <input> and <textarea> grow or shrink to fit their content with a single CSS property, replacing a common JavaScript auto-resize hack.

View code

HTML

<form>
  <label>Default sizing
    <input type="text" value="Type more, width stays fixed">
  </label>
  <label>field-sizing: content
    <input class="auto-size" type="text" value="This one grows as you type">
  </label>
</form>

CSS

body { font-family: system-ui, sans-serif; margin: 2rem; background: #fff; color: #111827; }
form { display: flex; flex-direction: column; gap: 1rem; }
label { display: flex; flex-direction: column; gap: 0.35rem; font-size: 0.9rem; color: #374151; }
input {
  font: inherit;
  padding: 0.5rem 0.75rem;
  border: 1px solid #d1d5db;
  border-radius: 8px;
  width: 260px;
}
input.auto-size {
  field-sizing: content;
  min-width: 140px;
  max-width: 100%;
  width: auto;
}
Browser support:Chrome 123+Firefox 152+Safari 26.2+Edge 123+