Skip to content

feat: Support container units - #926

Merged
shuding merged 10 commits into
mainfrom
shu/container-units
Oct 15, 2026
Merged

shuding merged 10 commits into
mainfrom
shu/container-units

Conversation

@shuding

@shuding shuding commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Container units like cqw were unknown units, so declarations with them were dropped with a style error, like border-radius: 1.5cqw on the course cards of nextjs.org.

Now they're resolved in stylesheets, like in browsers:

  • cqw and cqi are of the width of the nearest query container, cqh and cqb of the height of the nearest size container, and cqmin and cqmax the smaller and larger of them. A pseudo-element can use its element as its container.
  • Without such a container, they're of the viewport.
  • Container sizes are known after the layout, so trees that use these units are laid out twice, like with container queries, see feat: Support container queries in stylesheets #914.
  • Declarations with them are validated as lengths, so invalid ones are still dropped and reported, with their original value.
  • Without stylesheets, there are no query containers, so they're of the viewport. In values of custom properties, they're also of the viewport, as they're resolved where the property is used.

cascade() and rebuild() now take a context object instead of more and more parameters.

New tests cover the units with inline-size and size containers, the fallback to the viewport, pseudo-elements and inline styles. They fail without this change.

The cards' radius is now 18.48px like in Chrome, and nextjs-learn-courses has no style errors. Its content mismatch stays at 2.19%, as the corners only change a few pixels.

Linear-time scanning

A security review found that the first version's regular expression for container units backtracked cubically on long numbers without a container unit: a quoted font name of 4,000 digits took about 17 seconds to validate, blocking the render. Values are now scanned once by a small tokenizer, which also skips strings and identifiers, and only when a quick check finds a container unit. A regression test with 4,000-digit values took 16.5 seconds before and takes about 30ms now.

Custom properties registered with `@property` have their initial value,
and aren't inherited if `inherits` is false, like in browsers. Tailwind
v4 registers its variables this way, e.g. `--tw-translate-x` with the
initial value `0`, so utilities like `-translate-y-1/2` were dropped.

Custom properties are inherited in the layout, so the initial values are
declared on the root, and on elements whose parent declares another
value of a property that isn't inherited.
`ch` was half of the font size, but it's the width of `0` in the font,
which is often wider: 0.67em in Geist. A `max-width: 42ch` paragraph of
nextjs.org was 378px instead of 508px, so it wrapped into more lines.

The width is measured once per font, and stored as a ratio to the font
size in the style. Like `em`, an element's own lengths use the ratio of
the font it inherits.
Like in browsers, text inputs show their value, or their placeholder in
the color of `::placeholder`, on a line that's vertically centered and
clipped. By default, the line is `size` average characters of the font
wide. Form controls are `inline-block`, like in Chrome.

Percentages of heights of atomic inlines are now of the height of the
container if it's a length, e.g. an input with `height: 100%`.
Container units like `cqw` in stylesheets are resolved with the size of
the nearest query container whose type allows their axis, and like in
browsers, of the viewport without one. Trees that use them are laid out
twice, like with container queries. Without stylesheets, there are no
query containers, so they're of the viewport.

Declarations with them are validated as lengths, so they're not dropped
like unknown units anymore.
@shuding
shuding added this pull request to stack #924 October 15, 2026 07:56
@vercel

vercel Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
satori-playground Ready Ready Preview, v0 Oct 15, 2026 9:41am UTC

@shuding
shuding marked this pull request as ready for review October 15, 2026 07:57
@vercel-security-reviewer

Copy link
Copy Markdown

Security review details

Base automatically changed from shu/text-inputs to main October 15, 2026 08:04
The regular expression that found container units backtracked
cubically on long numbers without a container unit, e.g. a quoted font
name of 4,000 digits took about 17 seconds to validate. Values are now
scanned once, like by a CSS tokenizer, which also skips strings and
identifiers, and only when they have container units.
shuding added a commit that referenced this pull request Oct 15, 2026
While fixing the security finding on #926, I checked other regular
expressions for the same problem. 15 of them, used for lengths, angles,
percentages, image sizes and grid tracks, matched numbers with
`\d+\.?\d*` or `\d*\.?\d+`. These can split a run of digits in many
ways, so on a long number followed by something they don't expect, they
backtrack through all of them. They're anchored, so this is quadratic
rather than cubic, but a value of 32,000 digits followed by `!`, in a
stylesheet or an inline style, took about 7 seconds to render.

The numbers are now matched with:

- `\d+(?:\.\d*)?` instead of `\d+\.?\d*`
- `(?:\d+(?:\.\d+)?|\.\d+)` instead of `\d*\.?\d+`

They match exactly the same values, with the same captures, but the
digits before the dot can only be matched one way, so failing matches
backtrack in linear time.

- I compared each old and new expression on 30,000 random inputs each,
including the captures, with no differences, and checked that the new
ones take under 50ms on 50,000-digit failing inputs.
- A new test renders such values in a stylesheet and inline styles. It
took 6.8 seconds before, and takes about 30ms now.
@shuding
shuding merged commit b75a07b into main Oct 15, 2026
10 checks passed
@shuding
shuding deleted the shu/container-units branch October 15, 2026 12:23
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 0.60.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

This branch was successfully deployed

1 active deployment
Preview — 01b8998a Deployed Oct 15, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant