packages/components — measured against 0.2.321.
This is the single thing blocking us from adopting the library, and it is bigger than the two bugs I filed in #1973 and #1974. Opening it separately because it is a design decision rather than a defect, and the fix is a direction rather than a patch.
The numbers
components total 102
hard-code a Tailwind palette colour 56
use dark: variants rather than token swaps 59Button is the clearest case:
const variantClasses = {
primary: 'bg-blue-500 hover:bg-blue-600 text-white ... dark:bg-blue-600 dark:hover:bg-blue-700',
danger: 'bg-red-500 hover:bg-red-600 text-white ... dark:bg-red-600',
secondary: 'bg-surface-sunken hover:bg-neutral-300 text-neutral-900 dark:text-white',
}Why that is unusable for us
Our six apps theme through crosswind semantic tokens backed by CSS custom properties — --accent, --panel, --border, --text. Those generate real utilities (bg-accent, text-accent-ink, bg-panel, border-line, text-subtle), and dark mode works by swapping the custom properties under [data-theme] / prefers-color-scheme, so there is no dark: on anything. That was a deliberate migration away from a 13-line :root block copy-pasted into nine views.
Concretely, in bughq:
--accent: #e11d48 (rose) <Button variant="primary"> ships bg-blue-500
our buttons: rounded 999px <Button> ships rounded-md
our buttons: px-4 py-1.5 <Button size="md"> ships a fixed h-10
our dark mode: token swap <Button> ships dark: variantsAdopting <Button> across our 294 call sites would turn a rose-accented product Tailwind blue, square off every pill, and reintroduce the dark: variants the design system exists to avoid.
className is not an escape hatch
buttonClasses appends className last in the template string:
`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${widthClasses} ${className}`but class attribute order does not decide CSS. bg-blue-500 and bg-accent are both single-class selectors of equal specificity, so the winner is whichever comes later in the generated stylesheet — not something a call site can control. Even where it happens to win, the caller is passing a className whose entire job is to cancel the variant, at which point the component contributes nothing but a wrapper.
What would make this adoptable
Any of these works for us; the first is cleanest:
- Drive variants from CSS custom properties with sensible fallbacks, e.g.
background: var(--stx-button-primary-bg, theme(colors.blue.500)). Apps that define nothing keep today's look exactly; apps with a token system point the variables at their own and everything follows, dark mode included, with nodark:needed. - A theme/preset layer — one place to remap the palette per app, rather than per component per call site.
- Unstyled/headless variants of the common primitives, so the behaviour can be adopted without the paint.
The 46 that are already fine
Worth saying this is not a wholesale complaint — 46 components carry no hard-coded palette colour and we can adopt them today. Usefully, that includes the whole Table family, Popover, Dropdown, Portal, Teleport, Transition, VirtualList, Sidebar* and the Stepper/TabBar families.
So the split is almost exactly structural-vs-painted: the components that only provide behaviour are adoptable, and the ones that paint are not. That is also why this is worth fixing centrally rather than app-side — the painted half is where most of the 294 buttons, 54 selects and 11 dialogs would have gone.
Happy to prototype the custom-property approach on Button as a PR if that direction is agreeable.