Skip to main content
Localizia

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):

  1. Feature code imports components only from @/components/ui — never radix-ui directly.
  2. Icons come from @/components/icons (a curated lucide re-export) — never lucide-react directly. 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/ui to 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.