Skip to content

[types] Validate CSSProperties against the CSS grammar, Baseline, and Flow - #1883

Draft
henryqdineen wants to merge 1 commit into
hqd-stylex-css-types-dtsfrom
hqd-stylex-css-types-validate
Draft

henryqdineen wants to merge 1 commit into
hqd-stylex-css-types-dtsfrom
hqd-stylex-css-types-validate

Conversation

@henryqdineen

@henryqdineen henryqdineen commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

What changed / motivation ?

Stacked on #1882, which made StyleXCSSTypes.d.ts a hand-maintained duplicate of the Flow types.

CSSProperties is 522 properties and roughly 2,500 keywords, all written by hand, and nothing checks it against CSS. At that size review cannot catch a keyword that is misspelled, one that was never in any spec, or one that is missing — and after #1882 there are two copies that can drift apart.

CSS is machine-readable where it matters: the value definition grammar states what each property accepts, and Baseline states what is safe to use. These tests ask those sources rather than asking a reviewer to know CSS, so the types become checkable instead of trusted.

Deliberately validating, not generating. The types stay hand-written and hand-editable; the tests only report where they disagree with CSS. Where they intentionally differ — browsers and specs disagree too — it goes in an allowlist with a reason. Today a missing keyword and a deliberate omission look identical; afterwards they do not. That turns out to be 38 decisions and 66 gaps, all currently indistinguishable from each other and from correct code.

The six tests

  1. The TypeScript definitions match the Flow types — nothing else stops them diverging.
  2. Every property accepts the CSS-wide keywords (initial, inherit, unset, revert, revert-layer) — valid everywhere, in no grammar, so nothing else can check them.
  3. Every keyword is real, per the grammar — typos and invented values.
  4. Closed unions list every keyword the grammar has — the reverse of 3: values a user cannot write at all.
  5. Properties whose grammar needs arbitrary strings accept them. Not "are the literals right?" but "should this be a union of literals at all?" Would have caught url value for cursor property #1463.
  6. Every Baseline-available property is typed.

Built so an agent can close the loop

Each allowlist entry is work still to do, and each failure names the property, prints its grammar or Baseline status, and states the two valid fixes. Remove an entry, run the test, read the failure, make the change, re-run — no CSS knowledge required beyond the output.

That is deliberate, because what remains is bulk cleanup rather than design. I had a small model do three of them unattended from the test output alone and it got them right. #1886 is five worked examples; what makes them trustworthy is not care in writing them but that the tests check each against the grammar and confirm both files still agree.

Linked PR/Issues

Stacked on #1882. Test 5 surfaces #1463, fixed in #1885.

Additional Context

The allowlists are two different things, and it is worth separating them. Some entries are decisions that should stay, because specs and browsers disagree and StyleX sides with browsers. The rest are a to-do list.

Deliberate deviations (38) — expected to stay:

KEPT_KEYWORDS 19 Keywords the spec dropped that browsers still ship. marginTrim is the clearest: the spec was rewritten to none | in-flow | all, but Safari ships the original block/inline
NOT_TYPED 17 SVG presentation attributes. StyleX types fill, stroke and strokeWidth, so excluding the rest is arbitrary — but x, y, d and r read oddly as style keys
NOT_TYPED 1 all, which resets every property at once and cannot work with styleq's key-based override model
NO_GRAMMAR 1 WebkitBoxOrient, which css-tree simply lacks but the -webkit-line-clamp idiom needs

Known gaps (66) — expected to reach zero:

INCOMPLETE_UNIONS 32 Closed unions missing keywords the grammar allows — display cannot take flow or table-caption, overflow cannot take overlay
NO_GRAMMAR 15 Properties in no current spec, to be deleted: abandoned drafts (motion-* became offset-*), @font-face descriptors that were never properties (src, unicode-range)
CLOSED_DESPITE_GRAMMAR 13 Types too narrow for their grammar. colorScheme and fontStyle accept neither strings nor numbers; the rest accept a bare number, so only calc(), var() and units are out of reach
NOT_TYPED 6 Baseline-available properties that should be typed — whiteSpaceCollapse is literally commented out in StyleXCSSTypes.js awaiting this

marginTrim appears in both halves, which is honest: it keeps values the spec dropped and is missing values the spec added.

Scoping test 4 is what makes it useful: restricted to closed unions it finds 125 real omissions across 32 properties, where unrestricted it demands over ten thousand additions, because shorthands like background transitively pull in every named colour.

Two new devDependencies, both test-only. css-tree supplies the grammar and its parser — a patched fork of mdn-data, and the same parser csstype uses. web-features supplies Baseline; mdn-data's status field looks like it would do instead, but it describes the spec rather than browsers, calling font-stretch obsolete though every browser has supported it since 2020 while marking its replacement font-width experimental. It is pinned exactly so that a version bump lands in package.json rather than as a lockfile line inside Dependabot's weekly grouped PR — worth knowing, because when it moves, test 6 can legitimately fail: a property reaching Baseline is the news it exists to deliver.

Why not codegen, or csstype. Generating these types from the grammar was the first thing I tried; it worked, but produced a ~4,000-line diff, replaced a file people read with generated output, and put css-tree in the build path of a public API — with no existing codegen in the repo to follow. csstype is spec-current in ways that clash: it would break roughly 24 of the deviations above, and wrapping it for StyleX's semantics (null to unset, bare numbers meaning pixels, theme, comma-list arrays) is unpleasant in Flow. The dual Flow/TypeScript requirement is what makes most off-the-shelf options awkward.

The omissions are not deliberate — I checked whether StyleX prefixes or normalises values in a way that made them redundant, and it does not.

Follow-ups. No TSDoc, unlike csstype, though most of the data is available here. And packages/@stylexjs/eslint-plugin/src/reference/cssProperties.js is a second, independent encoding of this knowledge that could use the same treatment — worth knowing before leaning on it that its conversion misassigned values, so stylex/valid-styles currently accepts zIndex: 'ideographic' and transform: 'border-box'.

Each failure was verified by injecting it. The suite runs in ~60ms.

Pre-flight checklist

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown

workflow: benchmarks/perf

Comparison of performance test results, measured in operations per second. Larger is better.
yarn workspace v1.22.22
yarn run v1.22.22
$ node ./compare.js /tmp/tmp.po4ejDp09G /tmp/tmp.csvCIFnYjT

Results Base Patch Ratio
babel-plugin: stylex.create
· basic create 571 556 0.97 -
· complex create 63 63 1.00
babel-plugin: stylex.createTheme
· basic themes 450 446 0.99 -
· complex themes 31 31 1.00
Done in 0.08s.
Done in 0.34s.

@henryqdineen
henryqdineen added this pull request to stack #1887 September 11, 2026 03:13
@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown

workflow: benchmarks/size

Comparison of minified (terser) and compressed (brotli) size results, measured in bytes. Smaller is better.
yarn workspace v1.22.22
yarn run v1.22.22
$ node ./compare.js /tmp/tmp.oJpRmIs4aB /tmp/tmp.9atf0ROKpO

Results Base Patch Ratio
@stylexjs/stylex/lib/cjs/stylex.js
· compressed 1,535 1,535 1.00
· minified 5,166 5,166 1.00
@stylexjs/stylex/lib/cjs/inject.js
· compressed 1,793 1,793 1.00
· minified 4,915 4,915 1.00
benchmarks/size/.build/bundle.js
· compressed 496,650 496,650 1.00
· minified 4,847,840 4,847,840 1.00
benchmarks/size/.build/stylex.css
· compressed 99,735 99,735 1.00
· minified 740,755 740,755 1.00
Done in 0.08s.
Done in 0.33s.

Five things about `CSSProperties` are hand-maintained and can drift silently.

`StyleXCSSTypes.d.ts` duplicates `StyleXCSSTypes.js` so that TypeScript can use
`(string & {})` where Flow only has `string`. Nothing stops the two files
diverging, so the first test reads both, reduces each property to the keywords,
numbers and strings it accepts, and asserts the two agree. `string & {}` reads
as an arbitrary string, which is exactly what it is.

`@babel/parser` reads both dialects -- the Flow types with the `flow` plugin,
the definitions with `typescript` in declaration mode -- so one reader covers
both files. The two ASTs use disjoint node names, so a single switch handles
either.

The CSS-wide keywords are valid on every property and appear in no grammar, so
the second test is the only thing that can check them. Properties compose them
from the `all` alias.

The property values are written by hand, so `css-tree`'s value definition
grammar checks them in both directions. The third test asserts every keyword in
the types is one the grammar accepts, catching typos and invented values. The
fourth asserts the reverse for closed unions -- that every keyword the grammar
allows is typed -- catching values a user simply cannot write. Both follow
`<'other-property'>` references, which is how logical properties and shorthands
are defined.

The reverse check is restricted to closed unions on purpose. Where a property
accepts an arbitrary string, every keyword is already accepted and completeness
means nothing; asserting it everywhere demands over ten thousand additions,
because shorthands like `background` transitively pull in every named colour.
Restricted, it finds 125 real omissions across 33 properties.

The set of properties is also hand-maintained, so the fifth test asserts every
Baseline-available property is typed. `web-features` is the data behind MDN's
"Widely available" / "Newly available" banners; css-tree carries no support
data at all, and mdn-data's `status` describes the spec rather than browsers --
it calls `font-stretch` obsolete though every browser has supported it since
2020, while marking its replacement `font-width` experimental. Using Baseline
rather than "css-tree knows about it" keeps the exclusion list to real
decisions: 25 entries rather than 82.

All five pass as-is. Each list entry carries a reason, and each failure says
what to do about it:

- 6 properties accepting none of the CSS-wide keywords, written
  `null | 'a' | 'b'` rather than composing `all`. They are also the only
  properties in the file with no named type alias -- inlining the union at the
  property is what hid the slip.
- 17 properties css-tree has no grammar for: abandoned drafts (`motion-*` became
  `offset-*`, the CSS Display Level 3 longhands were dropped), `@font-face`
  descriptors that were never properties (`src`, `unicode-range`), and names
  that never shipped unprefixed.
- 18 properties keeping a keyword the grammar has dropped, where real
  stylesheets still use it. `marginTrim` is the clearest: the spec was rewritten
  to `none | in-flow | all`, but Safari ships the original `block`/`inline`
  syntax.
- 33 properties whose closed union is missing keywords the grammar allows --
  `display` cannot take `flow` or `table-caption`, `overflow` cannot take
  `overlay`, `mixBlendMode` cannot take `plus-darker`. `marginTrim` appears in
  both keyword lists, which is honest: it keeps values the spec dropped and is
  missing values the spec added.
- 25 Baseline-available properties that are not typed: 7 that should be, 17 SVG
  presentation attributes, and `all`.

`web-features` is pinned exactly rather than by range, because its whole
purpose is to change over time: a floating version would let a routine install
flip a property to Baseline and fail the build with no code change. Pinned, the
test only fires on a deliberate bump -- which is when the news is wanted.

Verified by injecting each failure: a typo'd keyword trips the grammar and
parity tests and is reported by name; editing one type file without the other
trips only parity; removing an allowlist entry reports what to add, and
satisfying one reports the entry as stale.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@henryqdineen
henryqdineen force-pushed the hqd-stylex-css-types-validate branch from 8b7f11f to 95bf702 Compare September 11, 2026 03:15
@vercel

vercel Bot commented Sep 11, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated
stylex Ignored Ignored Sep 11, 2026 3:15am UTC

Request Review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant