UI & styling
The virgin baseline
The kit ships with zero styling of its own. Its components render as clean,
accessible, semantic HTML — no Tailwind, no globals.css, no CSS-in-JS, no
fonts, no design tokens. A page renders as raw semantic markup until you add a
styling layer.
This is deliberate: styling is the one thing every project wants to own, so the
kit doesn't impose one. Instead, components expose styling hooks as
data-* attributes.
The component library
src/components/ui/ is the kit's own component library — Radix primitives for
behavior (focus management, accessibility, keyboard nav), stripped of all
styling. Each component emits attributes instead of classes:
<button data-slot="button" data-variant="default" data-size="default">
Feature code imports these per file — import { Button } from "@/components/ui/button" — and that path is the stable port your app binds
to. variant and size props surface as data-variant / data-size and emit
no classes.
Enforced boundaries
Two rules are enforced by ESLint (orthogonal to styling, which is free):
- Feature code imports components only from
@/components/ui— neverradix-uidirectly. - Icons come from
@/components/icons(a curated lucide re-export) — neverlucide-reactdirectly. Add new icons there.
Styling itself is not restricted: className, inline style, and *.css
imports are all yours to use in your feature code and your styling layer.
Three ways to style
The full recipes are in docs/design-system.md in the repo. In short:
- Path A — your own tokens. Write CSS that targets the
data-*contract ([data-slot="button"][data-variant="default"] { … }). The most native path. - Path B — shadcn/ui. Fill the port file-for-file with shadcn components; feature code doesn't change because the import path is stable.
- Path C — an external design system. Bind
@/components/uito a component library with thin adapters.
All three share one CSS entry point: the root layout,
src/app/[locale]/layout.tsx (there is no src/app/layout.tsx). Nothing is
imported by default.
The one exception
Sonner (toasts, in ui/sonner.tsx) self-injects its own scoped stylesheet —
a toast has no unstyled semantic-HTML equivalent. It's scoped to
[data-sonner-toaster] and overridable. The docs system
adds two more scoped, toggleable exceptions (shiki + pagefind). Everything else
the kit authors is genuinely CSS-free.
Accessibility
Because the kit is unstyled, semantic markup does the accessibility work: real
<button>/<nav>/<main> elements, labels tied to inputs, error messages
linked via aria-describedby, and dialogs that trap and restore focus (use the
ui/ Dialog). Your :focus-visible styles then have real elements to target.