@sub-design/ui
v0.26.0
Published
Portable React controls for GoodSync and NOKL. Twenty families today: Badge, Button, Card, Checkbox, Context Menu, Dialog, Input, Label, Menu, Notice, Radio Group, Select, Separator, Sheet, Skeleton, Status Mark, Switch, Tabs, Textarea, Tooltip.
Readme
@sub-design/ui
Portable React controls for GoodSync and NOKL. Twenty families today: Badge, Button, Card, Checkbox, Context Menu, Dialog, Input, Label, Menu, Notice, Radio Group, Select, Separator, Sheet, Skeleton, Status Mark, Switch, Tabs, Textarea, Tooltip.
What makes a component belong here
Core UI owns portable controls and accessible interaction patterns. File rows,
filter bars, sortable admin tables and marketing sections are not core
primitives — see docs/adr-0001-platform-foundation.md.
Two independent consumers is the bar, and it is necessary rather than
sufficient — Card's own history is the cautionary tale for how to apply it.
It was rejected once as unshareable because its two implementations shared a
name and nothing else; promoted later, 2026-08-11, once the same comparison
was redone property by property instead of as a totalled judgment.
Notice/Alert followed the same day: two real consumers whose colours already
converged, blocked instead by a composition-model difference — prop slots
vs. a compound component — that no token could carry, resolved by adopting
Enterprise's compound shape and adding the action slot it lacked. See
docs/roadmap.md's Delivery 3 section for both.
The complete promotion and API-evolution rules live in
docs/evolution-policy.md.
Installing
npm install @sub-design/ui @sub-design/tokens @sub-design/themes@base-ui/react, react and react-dom are peers.
The package ships source, not a build: .tsx files that a consumer's own
bundler compiles. That keeps one React instance and lets each product's Tailwind
generate only the classes it uses — but it means the classes are invisible to
Tailwind unless each adopted file is scanned explicitly:
@import "@sub-design/tokens";
@import "@sub-design/themes/goodsync";
@source "../node_modules/@sub-design/ui/src/button.tsx";Add a @source line per component adopted. Without it the component renders
unstyled, and nothing warns.
How it stays brand-neutral
Every class resolves through a token. No component contains a palette value, a
radius literal, or a product name — swapping data-brand is the only thing that
changes how it looks. Colours use the (color:--token) form and lengths
(length:--token), because Tailwind cannot otherwise tell what a bare custom
property holds.
Base UI is the primitive engine and stays behind this package; products import
from here, never from @base-ui/react, so the engine can be replaced without
touching product code.
Accessibility
Focus geometry, pointer target sizes and contrast are treated as guarantees
rather than style. --focus-ring-width and --switch-hit-area-min live in the
token contract for that reason, and a product that redeclares them silently
changes adopted components too.
Theme contrast is asserted in test/contrast.test.ts — eight pairs across both
brands and both colour schemes, read from the real CSS. Three GoodSync values
shipped below AA before it existed.
