← Back
[W] Writing

What you can ship now, November 2026

This is a short, recurring set of platform notes. Every few months, a handful of features cross the line from "nice in Chrome" to "works for nearly everyone", and they're easy to miss.

Each one here now ships in the stable versions of Chrome, Safari and Firefox. For each, you get the problem it removes, the CSS or HTML it takes, a demo, and one honest support line. Every demo checks for the feature first. If your browser doesn't have it, the demo says so and shows what it would do.

FeatureChromeSafariFirefox
Scroll-driven animations11526159
text-box13318.2154
sibling-index(), sibling-count()13826.2154
Container style queries11118151
shape()13518.4148
command, commandfor13526.2144
First version with support, from MDN's compatibility data as of November 2026. Edge follows Chrome's numbers. In every row, Firefox or Safari was the last to land, between December 2025 and October 2026.

Some neighbours are covered elsewhere: anchor positioning, view transitions, the customisable select and text wrapping.

Scroll-driven animations

Reading bars, parallax and reveal-on-scroll used to need a scroll listener. The script read the scroll position, did some maths, and wrote a style, on the main thread. If the page was busy, the effect fell behind your finger.

Now an ordinary keyframe animation can use scroll position as its clock instead of time.

.scroller {
  scroll-timeline: --read block;
}
.bar {
  animation: grow linear both;
  animation-timeline: --read;
}
Progress is scrollTop divided by the most you can scroll. That fraction becomes the animation's percentage.

The maths is the readout. At the top, the animation is at 0%. At the very bottom, it's at 100%. Because it's a real animation, the browser can run it on the compositor when you animate transform or opacity, so it doesn't wait for a free moment on the main thread.

Support: Firefox 159 turned it on by default on 27 October 2026, after Safari 26 and Chrome 115. It's also an Interop 2026 focus area, so expect the edge cases to get tighter through the year.

Trimming the space around text

A button with 8px of padding top and bottom rarely looks centred. The text sits a little low, or a little high, depending on the font.

That's because a line of text isn't as tall as its letters. The font reserves room above for accents and room below for descenders like g and y. Your padding is measured from the edge of that reserved space, not from the letters.

text-box cuts that space away. The values here mean: trim both ends, down to the top of the capitals and the baseline.

.button-label {
  text-box: trim-both cap alphabetic;
}
The tinted box is the text's own box. Trimmed, its top is the top of the H and its bottom is the baseline, so padding around it is equal to the eye.

The browser works it out from the font's own measurements. Above the capitals it removes half the extra line height, plus the font's ascent, minus its cap height. Below, it removes half the extra line height, plus the descent. The demo's readout runs the same sums from the font's metrics.

Support: Firefox 154 in August 2026 completed the set, after Safari 18.2 and Chrome 133. Without it, text just keeps its normal spacing, so it's safe to add today.

Knowing your place in a list

Staggered entrances and graded colours need each item to know its position. Until now, that meant writing style="--i: 3" on every item from a script or a template, and keeping it right whenever the list changed.

sibling-index() is the element's position among its siblings, starting at 1. sibling-count() is how many siblings there are, itself included. Both are plain numbers you can use in calc().

li {
  transition-delay: calc((sibling-index() - 1) * 40ms);
  background: oklch(82% 0.1 calc(360deg * sibling-index() / sibling-count()));
}
Add or remove an item and every colour and width updates, because sibling-count() changed. Replay the stagger to see the delays.

The numbers are live. Remove an item and the ones after it renumber themselves on the next style pass.

Support: Firefox 154 in August 2026, after Safari 26.2 and Chrome 138. In older browsers, the whole declaration is ignored, so put a sensible plain value before it.

Container style queries

A card that looks different when it's urgent usually gets a class on every part that changes: the border, the badge, the text. Or it gets a variant prop that's threaded through the component tree.

A style query lets the children ask the container for the value of a custom property. Set one variable on the outside, and everything inside restyles itself.

@container card style(--tone: urgent) {
  .card { border-color: var(--red); }
  .badge::before { content: "Failing"; }
}
Each button changes one custom property on the wrapper. Everything else is in the stylesheet.

Unlike size queries, you don't need container-type for this. Any element can be queried for its styles. Without a name, the query asks the nearest ancestor. The name lets you skip past wrappers to the one you mean.

Support: Firefox 151 in May 2026, after Safari 18 and Chrome 111. That covers checking a custom property for a value. Range comparisons like style(--level > 3) are newer, and Safari doesn't have them yet.

Responsive clip paths with shape()

path() takes SVG path data, which is in pixels only. A speech bubble drawn with it is right at one width and stretched or broken at every other.

shape() draws the same kinds of lines and curves using CSS syntax. That means percentages, calc() and fixed lengths, mixed in one shape.

.bubble {
  clip-path: shape(from 12px 0,
    hline to calc(100% - 12px),
    arc to 100% 12px of 12px cw,
    /* … */
    close);
}
The bubble's sides are percentages. Its corner radius and tail are fixed lengths, so they don't stretch as it resizes.

polygon() could already mix units, but only with straight lines. shape() adds arcs and curves, which is what makes the rounded corners possible. It works for offset-path too.

Support: Firefox 148 in February 2026, after Safari 18.4 and Chrome 135. It's an Interop 2026 focus area too.

Buttons that act on other elements

Opening a dialog needed a script: find the button, find the dialog, add a click listener, call showModal(). Every design system ships a version of that glue.

Now two attributes do it. commandfor names the target by its id, and command says what to do.

<button commandfor="confirm" command="show-modal">Delete</button>

<dialog id="confirm">
  <button commandfor="confirm" command="close">Cancel</button>
</dialog>
Open and close come from the attributes alone. The second button sends a custom command, which the page handles with one listener.

The built-in commands cover dialogs (show-modal, close) and popovers (toggle-popover, show-popover, hide-popover). Any value starting with two dashes is yours. The target gets a command event, with the command's name and the button that sent it.

Because the relationship is in the markup, the browser knows which button controls which element. It can tell assistive technology about it, which is the part hand-written glue most often forgets.

Support: Safari 26.2 in December 2025 completed the set, after Chrome 135 and Firefox 144.

Not yet

Several features are close, but not in all three browsers yet. Typed attr() is in Chrome and Firefox, and only in Safari Technology Preview. corner-shape is in Chrome only, as are if(), scroll-state queries and interestfor. Typed attr() is an Interop 2026 focus area, so it's a good bet for a future edition.

The useful habit is to check the slowest browser, not the newest one. A feature is ready when the last engine ships it, and these six are there.

Next