Design System Contracts

Measured

What works

The success side of the pair. 54 components from 6 third-party libraries across five styling architectures were run through one pipeline and scored against the original package rendering: 89.6% mean computed-style equality, exact string match, no tolerance, over 379,861 style cells. In the other direction a 599-variant Figma kit converted to code scores 92.70% over the 537 statically scorable variants. What that does not say: those 54 components are 6.0% of the 893 components in the libraries they came from, and they were picked because they were the tractable ones.

Read this page with its companion. This one is the shorter of the two, and it is the one written by the interested party. docs/23-known-limitations.md — Known Limitations is the complete inventory of what this tool does not do: coverage, the component classes captured nowhere, what a captured component fails to reproduce, the journey verbs that do not exist, and what each gate leaves out of its denominator. If you are deciding whether to adopt this, read that one too — start with its coverage section, which opens on the same 6.0% denominator this page does.

6.0%
of the 893 components in those
libraries are measured here — the denominator
89.6%
mean computed-style equality
over 54 captured components
92.70%
canvas→code visual fidelity
over 537 scored variants
15 / 15
components whose canvas→code→canvas
round trip closednot lossless
188 gates
executable claims green
(npm run eval)
265
generated files pinned byte-identical
(evals/golden.json)

Every figure above and below is parsed at build time out of docs/24-what-works.md, which is generated from the committed artifacts — no number on this page is typed in, and the coverage figure is printed at the same size as the flattering ones on purpose.

The denominator, first Generated

Every mean below is an average over captured components. This is which components, and what fraction of each library they are — printed before the results rather than after them.

librarycontracts committedmeasured (has a scorecard)library sizecoverage
Altitude (altitude-web-components@1.0.2)886711.9%
Astryx (@astryxdesign/core@0.1.6)1352222.3%
Carbon (@carbon/react@1.112.0)10102434.1%
MUI (@mui/material@9.2.0)141413510.4%
Polaris (@shopify/polaris@13.9.5)12121806.7%
Flowbite / Tailwind (flowbite-react@0.12.17)554610.9%
total62548936.0%

Read every percentage on this page as “on the easy 6.0%.” The 54 measured components were chosen because they were tractable, not at random — Button, Badge, Chip, Card, Checkbox, Tag, Avatar, Divider and their siblings. Data grid, tree, virtualized list, date picker, rich text and charts appear in zero committed contracts. A mean over this slice is a statement about this slice.

The size denominators are the one set of numbers here that is not machine-derived: they come from one-off extractor runs recorded in docs/22 §8.3, and they count helpers and utility directories, so they lean against us. The true percentages are a little higher; the order of magnitude is the finding. This page also uses the stricter column — components with a measured scorecard (54), not contracts committed (62) — because the fidelity numbers describe only the measured ones.

Three more corpora exist and are deliberately not counted above, because they are not third-party captures:

corpuscontractswhat it issource
this repo's own library51hand-authored here; the code→design direction's fixturecontracts/*.contract.json
Untitled UI (Figma kit)30proposed FROM a canvas, not extracted from code — see §4examples/untitled-ui/storybook/contracts/
Eventz (Figma kit)17a designer's own file with real variable namesexamples/eventz-vars/contracts/

Fidelity — code → contract → rendering Generated

The measurement: an enriched contract is emitted to HTML by core/emit-html and compared against the original npm package rendering in the same pinned Chromium, per prop combination × interaction state. Computed-style equality is an exact string match over the styled channel set, with no tolerance and no whitelist — the browser's full longhand set is enumerated, and a channel the pipeline never opened still counts against it.

librarycomponentsmean %equalmedian≥90%≥80%cells comparedcell-weighted
Altitude891.794.05/88/86,22490.2
Astryx593.198.64/54/518,65695.9
Carbon1087.586.74/108/1054,29782.8
MUI1489.791.39/1412/1489,85185.4
Polaris1289.491.57/1211/12200,40590.5
Flowbite / Tailwind586.888.92/54/510,42894.2
all six5489.690.731/5447/54379,86188.6

Two means, both printed, because they answer different questions. The unweighted mean (89.6%) treats a 16-cell Spinner and an 83,520-cell Button as equals; the cell-weighted figure (88.6%) is what fraction of every style cell in the corpus actually matched. Neither is quoted alone. 31/54 components are at ≥90%, 47/54 at ≥80%.

Every measured component, worst first

No component is omitted, and the worst row in the corpus is at the top. A success page that showed only its best rows would be the exact failure this project exists to catch.

componentlibrary%equalcombos × statescells
AvatarPolaris69.810 × 41,920
CardFlowbite / Tailwind72.41 × 4116
CheckboxMUI72.56 × 41,656
SwitchAstryx77.34 × 41,536
AccordionCarbon77.616 × 45,472
SwitchMUI77.756 × 425,088
ButtonCarbon79.0112 × 420,608
TagCarbon80.552 × 412,896
CheckboxPolaris81.06 × 42,464
TagPolaris81.032 × 47,401
RadioButtonPolaris81.34 × 41,392
ButtonAltitude81.55 × 41,280
AvatarAltitude81.74 × 4568
TextFieldPolaris82.064 × 441,440
LinkAltitude82.44 × 4432
CheckboxCarbon84.36 × 41,776
ToggleCarbon84.34 × 41,376
AlertFlowbite / Tailwind84.54 × 4880
TableMUI85.22 × 46,176
ButtonMUI86.4126 × 427,216
ToggleSwitchFlowbite / Tailwind88.96 × 4864
TextInputCarbon89.010 × 43,560
SliderMUI89.412 × 47,392
ModalCarbon90.05 × 41,305
ChipMUI90.228 × 46,832
SliderAstryx90.46 × 42,944
BadgeFlowbite / Tailwind90.712 × 41,968
TooltipMUI90.72 × 4129
ButtonPolaris90.8240 × 483,520
AccordionMUI91.88 × 46,400
ProgressBarPolaris92.112 × 41,824
TabsCarbon93.21 × 41,240
BadgeAltitude93.810 × 41,280
TabsMUI93.86 × 44,320
PaginationMUI94.01 × 4796
MenuMUI94.21 × 4171
ChipAltitude94.312 × 41,968
AutocompleteMUI95.12 × 42,536
DialogMUI95.45 × 4435
BannerPolaris97.016 × 412,192
InlineNotificationCarbon97.112 × 44,944
BadgePolaris97.560 × 423,396
ButtonFlowbite / Tailwind97.650 × 46,600
CardAstryx98.613 × 41,144
ButtonAstryx99.024 × 411,520
DividerAltitude100.02 × 4144
HeadingAltitude100.014 × 4392
IconCloseAltitude100.08 × 4160
BadgeAstryx100.014 × 41,512
IconButtonCarbon100.040 × 41,120
CardMUI100.04 × 4704
SpinnerPolaris100.02 × 48
TextPolaris100.0379 × 424,256
ThumbnailPolaris100.04 × 4592

The synthetic fixture, held out of every average above

extract/computed/out/conformance/ holds 50 more scorecards at a mean of 96.1%. They are excluded from the table above entirely and must never be folded into a library mean: they are synthetic single-construct cases this repo wrote to probe one CSS or DOM feature each, not components from anyone's design system. Including them would raise the headline by averaging in a fixture we built to be measurable — the shape of every overclaim this repo has caught. The corpus is partitioned by directory before any mean is taken.

fixturecasesmean %equalcounted in §3?
synthetic CSS/DOM constructs5096.1no
real third-party components5489.6yes

Fidelity — canvas → code Generated

The reverse journey, measured on a real Figma community kit. Variants are proposed from the canvas into contracts, emitted as static HTML, rendered, and scored against the exported reference image of the same variant.

measurevalue
rows in the scored table599
statically scorable537
mean fidelity over those92.70%
component sets15
unscored62

The unscored rows are named rather than dropped, and they are not all one thing:

why a row is unscoredrows
interaction-state (CSS-rendered, not statically scorable)58
axis not carried: size=xxs4

An interaction-state rendering is produced by CSS at runtime, so a static export cannot be scored against it — that is an instrument limit. The rest is a carriage gap: an axis the pipeline did not carry. They are counted separately rather than folded into the same excuse.

setvariants scoredmean fidelity
toggle-base1698.00
avatar-add-button696.69
avatar16296.17
progress-bar5594.02
button-group-base3292.24
social-button10892.06
button-base2091.97
input-field-base1091.63
avatar-label-group1291.48
badge-base891.33
slider4091.18
dropdown-list-item1290.91
progress-circle1685.85
avatar-group1284.90
tooltip2881.21

Denominator for that table: these are the sets in one community kit that were imported at all. The kit's un-imported sets do not appear as low scores — they do not appear.

The round trip closes — and it is not lossless Generated

Canvas → code → canvas, on the same kit. The claim is not that the round trip preserves everything. It is that it closes: it runs to completion on every component, and every fact lands in exactly one of four buckets, so a loss is a row in a table rather than an absence.

bucketfactsshare of all facts
matched11,10447.4%
diverged1,8577.9%
loss4,08817.5%
invented6,36527.2%
components closed15 / 15

47.4% matched is the honest headline, and it is not high. The value of this instrument is the classification, not the ratio — and the largest single divergence class is an artifact of the comparison rather than a loss: auto-layout-inert accounts for 940 of the 960 layout.mode divergences. A frame with one child, or with children the designer positioned absolutely, has no observable auto-layout direction to read back; the engine writes a direction the original canvas did not record. It is tagged as its own class precisely so it cannot be counted as a fidelity loss. The remaining ones are real.

buckettagfacts
divergedauto-layout-inert940
diverged(untagged)885
divergedvector-glyph32
lossrestructured2,705
loss(untagged)665
losstext-style-identity338
lossvector-glyph189
lossinteraction-states137
lossurl-image54
inventedrestructured4,336
inventeddup-sibling-names1,710
inventedcartesian-fill80
inventedhug-vs-fixed72
inventedzero-stroke60
inventedzero-fill54
inventedmixed-stroke-weight32
inventedvector-glyph15
inventeddeclared-not-drawn6

The (untagged) rows are the honest hole in this instrument: those facts are classified into a bucket but carry no reason, so nothing here can say whether they are engine defects or comparison artifacts. They are printed rather than excluded from the denominator.

The honesty instruments, counted as features Generated

These are the numbers this project is least tempted to publish and most needs to. Each one counts something the engine could not do and said so. They belong in a capability report because a conversion tool without them is not more capable — it is just quieter.

Dropped-fact receipts

When the plugin engine compiles a contract and cannot carry a fact onto the canvas, it emits a receipt naming the fact. Across the committed corpora there are 70 such receipts, and the count is pinned exactly, in both directions — fewer receipts is not automatically progress, because it is either a real fix or a refusal path that quietly stopped firing, and both require a human to look.

corpusdropped-fact receiptscontracts carrying one
untitled-ui1515
mui1414
polaris1212
carbon1010
altitude88
astryx66
tailwind55
eventz-vars00
total70

Named refusals — the construct vocabularies

Two hand-authored manifests are the independent denominators for “what can the engine be asked to do”. Both are deliberately not derived from the code that decides carriage: an instrument whose denominator comes from the same filter that decides carriage scores 100% on a channel it never opened. A construct that is neither carried nor named-refused is a hard failure of the suite — “it silently did nothing” is not an allowed outcome.

manifestcasesbreakdown
canvas constructs91CARRIED 72 · LEDGERED 11 · REFUSED 8
CSS / DOM frontier53CARRIED 22 · UNSUPPORTED 18 · REFUSED 11 · LOWERED 2

Reproducibility — the part that is not a percentage Generated

A fidelity number you cannot reproduce is an anecdote. These are the pins that make everything above re-derivable, and each is enforced by a gate rather than asserted in prose.

pinvaluewhat it forbids
generated source, byte-identical265 files hasheda contract change altering generated code without review
capture double-sweep identity104/104 runsa capture whose second sweep disagrees with its first
browser captures behind the corpus6,317a floor quoted from a sample smaller than it claims
executable claims188 gatesa documented behaviour with no test
dropped-fact receipt count70 pinned exactlyhonesty being switched off unnoticed — see §6
doc numbers vs the repogateda doc quoting a number the repo no longer produces
this document vs its sources--checkthis page going stale while still reading as current

The undefined executable claims are not all “does it work”. They are classified by what they claim, and the largest classes after extraction are detection and refusal — gates that fail if the engine stops saying no. An engine that carries everything is not a better engine; it is one that has stopped telling you what it could not do.

claim classgateswhat the class asserts
C1-determinism32same input, same bytes out
C2-refusal29the engine refuses BY NAME rather than guessing
C3-detection46a defect or drift is caught, not silently absorbed
C4-convergence4a round trip settles instead of oscillating
C5-extraction58a fact is carried out of a real library correctly
C6-theming1a mode/brand switch resolves to the right values
C7-cli5the command-line surface behaves as documented
C8-journey13an end-to-end adopter path completes

What it costs

Not restated here, deliberately — a summary of the limitations written by the success document is a summary written by the interested party. The complete inventory is docs/23-known-limitations.md — Known Limitations, and it is the longer of the two. It answers the three questions this page cannot: how long a library takes to onboard, how much of the work is expert-configured rather than automatic, and whether any of this holds past the 6.0% in the denominator table.

This page is a rendering of docs/24-what-works.md, generated by npm run capability:report from ten committed artifacts and refused by npm run capability:fresh if the committed bytes differ from a rebuild — one of the undefined evals runs that refusal. Every table above is parsed out of that file at build time; a section or a row that stops existing fails this build by name rather than rendering a blank. The report also cross-checks its own corpus twice from independent files — 54 scorecards, 62 committed contracts, and the capture double-sweep receipts (104/104 runs) — against a document written months earlier for a different purpose, and prints any disagreement instead of resolving it toward the more flattering value.