New and emerging CSS, crawled from specs, browser blogs, and expert writeups•Almost entirely built by Claude Code•Last updated Sep 29, 2026•New and emerging CSS, crawled from specs, browser blogs, and expert writeups•Almost entirely built by Claude Code•Last updated Sep 29, 2026•New and emerging CSS, crawled from specs, browser blogs, and expert writeups•Almost entirely built by Claude Code•Last updated Sep 29, 2026•New and emerging CSS, crawled from specs, browser blogs, and expert writeups•Almost entirely built by Claude Code•Last updated Sep 29, 2026•New and emerging CSS, crawled from specs, browser blogs, and expert writeups•Almost entirely built by Claude Code•Last updated Sep 29, 2026•New and emerging CSS, crawled from specs, browser blogs, and expert writeups•Almost entirely built by Claude Code•Last updated Sep 29, 2026•New and emerging CSS, crawled from specs, browser blogs, and expert writeups•Almost entirely built by Claude Code•Last updated Sep 29, 2026•New and emerging CSS, crawled from specs, browser blogs, and expert writeups•Almost entirely built by Claude Code•Last updated Sep 29, 2026•
Animating content into place as a CSS grid reflows
anchor-scope, inset: anchor(inside)
Each grid cell gets its own anchor-name, and anchor-scope keeps that name from colliding across the repeated cells — no manual numbering needed. Content is then absolutely positioned to its own cell via position-anchor and inset: anchor(inside), with a transition on inset, so as the grid reflows (here, by resizing its container) each piece of content glides to its new slot instead of snapping instantly. A distinct use of anchor positioning from this site's other examples: those anchor a tooltip, a dropdown, and a fallback-detecting arrow to a single element each, while this one anchors many repeated, self-scoped instances at once to animate a whole layout change. Firefox supports anchor-scope itself but doesn't yet do the style/layout interleaving anchor positioning needs to animate smoothly (tracked as bug 1924226), so the reflow there is correct but instant rather than eased.
A resize handle that follows writing-mode with resize: inline
resize: inline, resize: block
resize: inline (and its counterpart resize: block) resize along the flow-relative inline/block axis instead of a fixed horizontal/vertical direction — the same declaration drags left-right in a normal horizontal writing mode, but top-to-bottom once writing-mode flips to vertical. Requires overflow other than visible to take effect at all. Unlike resize: horizontal/vertical, which has been around for decades, these logical values are genuinely recent: full 4-engine support only landed once Chrome and Edge caught up at version 118, after Firefox (63) and Safari (16) had it for years.
Squircles and other corner curves with corner-shape
corner-shape, superellipse()
corner-shape reshapes whatever curve border-radius already carved out — squircle gives the continuous, Apple-style curve (equivalent to superellipse(2)), and higher superellipse(N) values flatten the sides further. A common misconception worth correcting: superellipse(infinity) is not an extreme squircle, it's a plain square — the exponent pushes the curve toward a hard corner, not a rounder one. Requires a real border-radius to have any visible effect at all.
@starting-style handles a dialog's entry, but closing it needs the reverse trick: transition-behavior: allow-discrete on display and the top-layer overlay property keeps the dialog (and its ::backdrop) rendered and in the top layer for the full transition instead of vanishing the instant .close() runs, only flipping display to none once the fade actually finishes. No JS delay or transitionend listener needed — calling dialog.close() just works, confirmed by sampling the computed opacity every frame during close.
View code
HTML
<button id="show" class="open-btn">Open dialog</button>
<dialog id="d">
<p>Hello! I fade in and out smoothly.</p>
<button id="close">Close</button>
</dialog>
view-transition-name: match-element assigns each matched element its own stable, unique transition name automatically, based on the browser's internal element identity — no hand-authored name (or attr()-based one) required for a whole list of items. The trade-off: since the auto-generated names aren't exposed to CSS, individual match-element groups can only be targeted with the universal ::view-transition-*(*) selectors, and it only works for same-document transitions, unlike the manually-named hero element in this site's other view-transitions example.
Detecting a stuck sticky header with @container scroll-state()
@container scroll-state(stuck)
container-type: scroll-state on a position: sticky element turns it into a query container for @container scroll-state(stuck: top) — matching only while that element is actually locked to the named edge, not merely scrolled near it. The container itself can't be restyled by its own query, so the color change targets a wrapper inside it — a common container-query gotcha. A different value from this site's other scroll-state examples (snapped, scrollable), completing the trio.
View code
HTML
<div class="scroll-area">
<header class="sticky-heading">
<div class="inner"><h3>Section heading</h3></div>
</header>
<div class="content">
<p>Scroll down — the heading above turns blue the instant it locks to the top, via @container scroll-state(stuck: top). No scroll event listener.</p>
<p>Paragraph two of filler content.</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 — end of content.</p>
</div>
</div>
Animating <details> to its real height with ::details-content
::details-content, interpolate-size
::details-content exposes the native content box a <details> element already shows/hides, so it can be styled and transitioned directly — and interpolate-size: allow-keywords lets height itself animate all the way to auto, no fixed max-height guess needed. Unlike a grid-template-rows trick, the content stays a real descendant of <details>, keeping the browser's native disclosure semantics (and features like find-in-page auto-expanding a match) intact. Where interpolate-size isn't supported, it still opens and closes correctly — just as an instant cut instead of a smooth animation.
View code
HTML
<div class="accordion">
<details open>
<summary>Item one (starts open)</summary>
<p>The content stays a real descendant of <details> this time — no sibling div, no aria-controls bridge needed.</p>
</details>
<details>
<summary>Item two</summary>
<p>::details-content plus interpolate-size: allow-keywords lets height animate all the way to auto.</p>
</details>
</div>
Sharing interaction state across unrelated elements with timeline-scope
timeline-scope
timeline-scope re-exposes a named timeline to every descendant of a shared ancestor, not just the timeline's own subtree — and a named timeline only needs to exist, not scroll, to work as a signal. Attaching view-timeline: --linked to an element only while it's :hover/:focus-visible, then reading animation-timeline: --linked on a completely unrelated element elsewhere in the scope, turns that presence/absence into a live cross-DOM toggle with no JavaScript, distinct from this site's other timeline-scope examples, which all share actual scroll position between siblings.
View code
HTML
<div class="scope">
<span class="term" tabindex="0">Hover this term</span>
<div class="highlight-target">I highlight when the unrelated term above is hovered/focused</div>
</div>
A hint popover that doesn't dismiss the menu behind it
popover="hint"
popover="hint" behaves like a lightweight, hover/focus-triggered tooltip that lives in its own top-layer stack, separate from popover="auto" — so showing a hint over an open auto popover (like a menu) doesn't close the menu the way opening a second auto popover would. Only one hint can be open at a time, but auto popovers are unaffected.
container-type: anchored turns an anchor-positioned element into a query container, so @container anchored(fallback: flip-block) can restyle its own descendants based on whether a position-try-fallbacks tactic actually kicked in — here, flipping a tooltip's arrow to match whichever side the tooltip ended up on, without guessing from JS or duplicating the tooltip for each case.
hypot() computes each tile's straight-line distance from the cursor from its own grid coordinates and two mouse-tracking custom properties, and the new if(style(...): ...; else: ...) function reads that distance directly to decide whether the tile should scale up — a conditional value baked into the declaration itself, no :hover selector or extra class needed for the threshold logic.
scroll-target-group: auto turns every fragment link inside a container into a scroll marker, and :target-current styles whichever one points at the section currently scrolled into view — a different, more direct mechanism than sharing a named view-timeline (as the site's other scroll-spy example does), built from plain anchor links with real fragment navigation.
View code
HTML
<div class="layout">
<nav class="toc">
<a href="#s1">① Intro</a>
<a href="#s2">② Methods</a>
<a href="#s3">③ Results</a>
</nav>
<div class="sections">
<section id="s1">
<h3>Introduction</h3>
<p>Scroll down — the active link updates as you scroll, no JS at all.</p>
</section>
<section id="s2">
<h3>Methods</h3>
<p>scroll-target-group: auto turns every fragment link inside this nav into a scroll marker.</p>
</section>
<section id="s3">
<h3>Results</h3>
<p>:target-current styles whichever marker points at the section currently in view.</p>
</section>
</div>
</div>
object-view-box defines a custom view window into a replaced element's natural content using inset() or rect(), and object-fit controls how that window fills the box — so the same image asset can show a tight corner crop in one place and a wide slice in another, without touching the file, exporting a second copy, or reaching for clip-path.
View code
HTML
<div class="row">
<figure>
<img class="full" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MDAiIGhlaWdodD0iMjAwIiB2aWV3Qm94PSIwIDAgNDAwIDIwMCI+CjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIiBmaWxsPSIjMWQ0ZWQ4Ii8+CjxyZWN0IHg9IjEwMCIgeT0iMCIgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiIGZpbGw9IiMyNTYzZWIiLz4KPHJlY3QgeD0iMjAwIiB5PSIwIiB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iIzNiODJmNiIvPgo8cmVjdCB4PSIzMDAiIHk9IjAiIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIiBmaWxsPSIjNjBhNWZhIi8+CjxyZWN0IHg9IjAiIHk9IjEwMCIgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiIGZpbGw9IiM5M2M1ZmQiLz4KPHJlY3QgeD0iMTAwIiB5PSIxMDAiIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIiBmaWxsPSIjYmZkYmZlIi8+CjxyZWN0IHg9IjIwMCIgeT0iMTAwIiB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iIzFlNDBhZiIvPgo8cmVjdCB4PSIzMDAiIHk9IjEwMCIgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiIGZpbGw9IiMxZTNhOGEiLz4KPHRleHQgeD0iNTAiIHk9IjU1IiBmb250LXNpemU9IjM2IiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZmlsbD0iI2ZmZiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9Im1pZGRsZSI+MTwvdGV4dD4KPHRleHQgeD0iMTUwIiB5PSI1NSIgZm9udC1zaXplPSIzNiIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZpbGw9IiNmZmYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJtaWRkbGUiPjI8L3RleHQ+Cjx0ZXh0IHg9IjI1MCIgeT0iNTUiIGZvbnQtc2l6ZT0iMzYiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmaWxsPSIjZmZmIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0ibWlkZGxlIj4zPC90ZXh0Pgo8dGV4dCB4PSIzNTAiIHk9IjU1IiBmb250LXNpemU9IjM2IiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZmlsbD0iI2ZmZiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9Im1pZGRsZSI+NDwvdGV4dD4KPHRleHQgeD0iNTAiIHk9IjE1NSIgZm9udC1zaXplPSIzNiIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZpbGw9IiMxZTNhOGEiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJtaWRkbGUiPjU8L3RleHQ+Cjx0ZXh0IHg9IjE1MCIgeT0iMTU1IiBmb250LXNpemU9IjM2IiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZmlsbD0iIzFlM2E4YSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9Im1pZGRsZSI+NjwvdGV4dD4KPHRleHQgeD0iMjUwIiB5PSIxNTUiIGZvbnQtc2l6ZT0iMzYiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmaWxsPSIjZmZmIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0ibWlkZGxlIj43PC90ZXh0Pgo8dGV4dCB4PSIzNTAiIHk9IjE1NSIgZm9udC1zaXplPSIzNiIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZpbGw9IiNmZmYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJtaWRkbGUiPjg8L3RleHQ+Cjwvc3ZnPg==" alt="Full reference image, an 8-cell numbered grid" width="200" height="100">
<figcaption>Original</figcaption>
</figure>
<figure>
<img class="crop-corner" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MDAiIGhlaWdodD0iMjAwIiB2aWV3Qm94PSIwIDAgNDAwIDIwMCI+CjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIiBmaWxsPSIjMWQ0ZWQ4Ii8+CjxyZWN0IHg9IjEwMCIgeT0iMCIgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiIGZpbGw9IiMyNTYzZWIiLz4KPHJlY3QgeD0iMjAwIiB5PSIwIiB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iIzNiODJmNiIvPgo8cmVjdCB4PSIzMDAiIHk9IjAiIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIiBmaWxsPSIjNjBhNWZhIi8+CjxyZWN0IHg9IjAiIHk9IjEwMCIgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiIGZpbGw9IiM5M2M1ZmQiLz4KPHJlY3QgeD0iMTAwIiB5PSIxMDAiIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIiBmaWxsPSIjYmZkYmZlIi8+CjxyZWN0IHg9IjIwMCIgeT0iMTAwIiB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iIzFlNDBhZiIvPgo8cmVjdCB4PSIzMDAiIHk9IjEwMCIgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiIGZpbGw9IiMxZTNhOGEiLz4KPHRleHQgeD0iNTAiIHk9IjU1IiBmb250LXNpemU9IjM2IiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZmlsbD0iI2ZmZiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9Im1pZGRsZSI+MTwvdGV4dD4KPHRleHQgeD0iMTUwIiB5PSI1NSIgZm9udC1zaXplPSIzNiIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZpbGw9IiNmZmYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJtaWRkbGUiPjI8L3RleHQ+Cjx0ZXh0IHg9IjI1MCIgeT0iNTUiIGZvbnQtc2l6ZT0iMzYiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmaWxsPSIjZmZmIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0ibWlkZGxlIj4zPC90ZXh0Pgo8dGV4dCB4PSIzNTAiIHk9IjU1IiBmb250LXNpemU9IjM2IiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZmlsbD0iI2ZmZiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9Im1pZGRsZSI+NDwvdGV4dD4KPHRleHQgeD0iNTAiIHk9IjE1NSIgZm9udC1zaXplPSIzNiIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZpbGw9IiMxZTNhOGEiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJtaWRkbGUiPjU8L3RleHQ+Cjx0ZXh0IHg9IjE1MCIgeT0iMTU1IiBmb250LXNpemU9IjM2IiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZmlsbD0iIzFlM2E4YSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9Im1pZGRsZSI+NjwvdGV4dD4KPHRleHQgeD0iMjUwIiB5PSIxNTUiIGZvbnQtc2l6ZT0iMzYiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmaWxsPSIjZmZmIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0ibWlkZGxlIj43PC90ZXh0Pgo8dGV4dCB4PSIzNTAiIHk9IjE1NSIgZm9udC1zaXplPSIzNiIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZpbGw9IiNmZmYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJtaWRkbGUiPjg8L3RleHQ+Cjwvc3ZnPg==" alt="Same image cropped tightly to just cell 5" width="90" height="90">
<figcaption>Corner crop</figcaption>
</figure>
<figure>
<img class="crop-wide" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MDAiIGhlaWdodD0iMjAwIiB2aWV3Qm94PSIwIDAgNDAwIDIwMCI+CjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIiBmaWxsPSIjMWQ0ZWQ4Ii8+CjxyZWN0IHg9IjEwMCIgeT0iMCIgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiIGZpbGw9IiMyNTYzZWIiLz4KPHJlY3QgeD0iMjAwIiB5PSIwIiB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iIzNiODJmNiIvPgo8cmVjdCB4PSIzMDAiIHk9IjAiIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIiBmaWxsPSIjNjBhNWZhIi8+CjxyZWN0IHg9IjAiIHk9IjEwMCIgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiIGZpbGw9IiM5M2M1ZmQiLz4KPHJlY3QgeD0iMTAwIiB5PSIxMDAiIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIiBmaWxsPSIjYmZkYmZlIi8+CjxyZWN0IHg9IjIwMCIgeT0iMTAwIiB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iIzFlNDBhZiIvPgo8cmVjdCB4PSIzMDAiIHk9IjEwMCIgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiIGZpbGw9IiMxZTNhOGEiLz4KPHRleHQgeD0iNTAiIHk9IjU1IiBmb250LXNpemU9IjM2IiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZmlsbD0iI2ZmZiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9Im1pZGRsZSI+MTwvdGV4dD4KPHRleHQgeD0iMTUwIiB5PSI1NSIgZm9udC1zaXplPSIzNiIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZpbGw9IiNmZmYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJtaWRkbGUiPjI8L3RleHQ+Cjx0ZXh0IHg9IjI1MCIgeT0iNTUiIGZvbnQtc2l6ZT0iMzYiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmaWxsPSIjZmZmIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0ibWlkZGxlIj4zPC90ZXh0Pgo8dGV4dCB4PSIzNTAiIHk9IjU1IiBmb250LXNpemU9IjM2IiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZmlsbD0iI2ZmZiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9Im1pZGRsZSI+NDwvdGV4dD4KPHRleHQgeD0iNTAiIHk9IjE1NSIgZm9udC1zaXplPSIzNiIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZpbGw9IiMxZTNhOGEiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJtaWRkbGUiPjU8L3RleHQ+Cjx0ZXh0IHg9IjE1MCIgeT0iMTU1IiBmb250LXNpemU9IjM2IiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZmlsbD0iIzFlM2E4YSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9Im1pZGRsZSI+NjwvdGV4dD4KPHRleHQgeD0iMjUwIiB5PSIxNTUiIGZvbnQtc2l6ZT0iMzYiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmaWxsPSIjZmZmIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkb21pbmFudC1iYXNlbGluZT0ibWlkZGxlIj43PC90ZXh0Pgo8dGV4dCB4PSIzNTAiIHk9IjE1NSIgZm9udC1zaXplPSIzNiIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZpbGw9IiNmZmYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJtaWRkbGUiPjg8L3RleHQ+Cjwvc3ZnPg==" alt="Same image cropped to a wide slice across cells 3 and 4" width="140" height="70">
<figcaption>Wide crop</figcaption>
</figure>
</div>
input[type="file"] styles the field's own border and padding, while ::file-selector-button targets just the native "choose file" button inside it — background, border-radius, hover states, anything a normal button accepts. No hidden-input-plus-styled-label workaround needed to get past the default OS-styled button.
View code
HTML
<label for="file-upload" class="field-label">Choose a file to upload</label>
<input type="file" id="file-upload">
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>
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.
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>
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.
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.
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.
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.
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.
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.
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.
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.
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.
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>
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.
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.
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.
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.
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.
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.
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.
::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.
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.
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>
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>
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.
@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.
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>
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>
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; }
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; }
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>
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.
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>
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>
::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>
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 → 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 <details>?
<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><details></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><details></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>
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.
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.
@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>
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.
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>
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>
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.
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>
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.
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>
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>
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>
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>
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>
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.
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.
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.
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.
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; }
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>
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>
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>
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.
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>
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>
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>