Dev notes
Four layers built on top of each other — design tokens, Tailwind v4, a theme switcher, and accessible component primitives.
One tokens.css file with colors, spacing, and font sizes as CSS custom properties. The palette shades (50–950) are the same in both light and dark mode. The semantic aliases flip — background, foreground, surface, border, muted. Values are reusable everywhere and changing one token updates every consumer. The downside: lots of CSS variables, and once you name a token it's hard to rename.
A @theme block bridges the token CSS variables to Tailwind utility classes — bg-primary-600 and var(--color-primary-600) resolve to the same value. The CSS Module files are legacy and kept for backwards compatibility. A @custom-variant tells Tailwind to match [data-theme="dark"] instead of the media query, so dark:hover:bg-neutral-800 works with the toggle.
A ThemeProvider sets the data-theme attribute on the base <html> element. Instead of useState with useEffect (which triggers ESLint warnings about setState in effects), it uses useSyncExternalStore — one subscription for localStorage, one for matchMedia. The theme is derived, never stored in state. Defaults to OS theme but allows manual override. data-theme is used over a class because data attributes describe state while classes describe styling, and it avoids collision with Tailwind utility classes.
Reusable Button, Input, and Modal primitives built without Radix or Headless UI — for three components it's reasonable to build by hand for zero runtime deps, full DOM control, and hands-on focus management experience.
loading prop sets aria-busy, shows a spinner, and disables clicks. Focus rings use focus-visible so they only appear for keyboard users.useId(). Errors use role="alert" so screen readers announce them immediately. aria-describedby links the input to its error or helper text.createPortal to escape stacking contexts. Custom focus trap cycles Tab through focusable elements. On close, focus returns to whatever element opened it. No external dependencies.