Skip to content
NVX UI · v0.2

Design system

Living style guide for NVX Stack Builder: semantic design tokens (light + dark) and accessible, headless-based components. Everything below is rendered with the real components — switch theme or language in the header to see them adapt.

Source: src/app/tokens.css · src/components/ui/ · docs/UI-FRAMEWORK.md

Colour

Semantic colour tokens. Components never use raw palette colours, so dark mode is just a different set of variable values.

Surfaces & text

  • --nvx-bgPage background · bg-bg
  • --nvx-surfaceCards, inputs · bg-surface
  • --nvx-surface-2Hover, subtle fills · bg-surface-2
  • --nvx-surface-3Pressed, skeleton · bg-surface-3
  • --nvx-borderDefault border · border-border
  • --nvx-border-strongInputs, emphasis · border-border-strong
  • --nvx-fgPrimary text · text-fg
  • --nvx-mutedSecondary text · text-muted
  • --nvx-subtleHints, captions · text-subtle

Brand

  • --nvx-primaryPrimary actions · bg-primary
  • --nvx-primary-hoverPrimary hover · hover:bg-primary-hover
  • --nvx-primary-softSelected, soft fills · bg-primary-soft
  • --nvx-primary-soft-fgText on soft fill · text-primary-soft-fg
  • --nvx-accentSecondary brand · text-accent
  • --nvx-ringFocus ring · ring-ring

Status

  • --nvx-successSuccess · text-success
  • --nvx-success-softSuccess background · bg-success-soft
  • --nvx-warningWarning · text-warning
  • --nvx-warning-softWarning background · bg-warning-soft
  • --nvx-dangerDestructive / error · bg-danger
  • --nvx-danger-softError background · bg-danger-soft
  • --nvx-infoInfo · text-info
  • --nvx-info-softInfo background · bg-info-soft
  • --nvx-code-bgCode blocks (always dark) · bg-code-bg
Usage
tsx
<div className="bg-surface text-fg border border-border">…</div>
<p className="text-muted">Secondary text</p>
<button className="bg-primary text-primary-fg hover:bg-primary-hover">…</button>

Typography

Geist for Latin, IBM Plex Sans Thai for Thai (automatic per-glyph fallback in one font stack), Geist Mono for code. Thai needs generous line-height — body text uses Tailwind's default 1.5.

  • text-display · 52pxRunnable stacksDisplay — hero headline
  • text-4xl · 36pxRunnable stacksPage title (h1)
  • text-2xl · 24pxRunnable stacksSection title (h2)
  • text-xl · 20pxRunnable stacksStage / card group title
  • text-base · 16pxRunnable stacksBody, card titles
  • text-sm · 14pxRunnable stacksUI text, descriptions
  • text-xs · 12pxRunnable stacksCaptions, badges, hints

Sans · The quick brown fox · ภาษาไทยอ่านง่าย สบายตา ๑๒๓

Mono · npx create-next-app@latest --ts

Usage
tsx
<h1 className="text-4xl font-bold tracking-tight">…</h1>
<p className="text-sm text-muted">…</p>
<code className="font-mono">npm install</code>

Spacing & radius

A 4px spacing unit (p-1 = 4px). Prefer 2, 3, 4, 6, 8 for UI gaps. Radius scales with the size of the element: md for controls, xl for cards, full for pills.

1 · 4px
2 · 8px
3 · 12px
4 · 16px
6 · 24px
8 · 32px
12 · 48px
16 · 64px

sm

md

lg

xl

full

Elevation & motion

Three shadow levels: sm (resting cards), md (hover), lg (dialogs, toasts). Motion uses three durations and two easings; all animation is disabled under prefers-reduced-motion.

shadow-sm
shadow-md
shadow-lg
  • --nvx-duration-fast120msHover, press
  • --nvx-duration-base200msDialogs, fades
  • --nvx-duration-slow320msSheets, toasts
  • --nvx-ease-standardcubic-bezier(0.2, 0, 0, 1)Most transitions
  • --nvx-ease-emphasizedcubic-bezier(0.3, 0, 0, 1.2)Entrances with a little overshoot

Button

Six variants × four sizes. One primary button per view. `loading` shows a spinner and sets aria-busy; `asChild` styles a Link; icon-only buttons need an aria-label.

sm
md
lg
asChild → /builder
Usage
tsx
<Button>Save</Button>
<Button variant="secondary" size="sm">Cancel</Button>
<Button loading>Planning…</Button>
<Button variant="ghost" size="icon" aria-label="Remove">✕</Button>
<Button asChild><Link href="/builder">Open</Link></Button>

Input, Textarea, Select

Native form controls with token styling. Always pair with a visible label — `Field` wires up id, hint and error (aria-describedby, aria-invalid, role=alert).

a-z 0-9 - _ .

Usage
tsx
<Field label="Email" hint="We never share it" error={error}>
  {(p) => <Input {...p} invalid={!!error} type="email" />}
</Field>

Checkbox, Switch, Segmented

Checkbox and Switch are Radix primitives (keyboard + screen-reader ready). Segmented is a styled native radio group: arrow keys move the selection.

Lint + test on every push

not available for this template

JS package manager
Usage
tsx
<Checkbox label="Docker" checked={on} onCheckedChange={setOn} />
<Switch label="Dark mode" checked={dark} onCheckedChange={setDark} />
<Segmented legend="Package manager" value={pm} onChange={setPm}
  options={[{ value: "npm", label: "npm" }, { value: "pnpm", label: "pnpm" }]} />

Card

The main container. Compose with CardHeader/Title/Description/Content/Footer. `interactive` adds hover lift for clickable cards; pick the heading level with `as`.

Next.js Dashboard

Static card with header, content and footer.

12 steps · Node.js

interactive

Hover me — lifts with shadow-md.

Usage
tsx
<Card interactive>
  <CardHeader><CardTitle as="h3">Title</CardTitle><CardDescription>…</CardDescription></CardHeader>
  <CardContent>…</CardContent>
  <CardFooter><Button size="sm">Action</Button></CardFooter>
</Card>

Badge

Short status or metadata labels. Colour is never the only signal — the text carries the meaning.

neutralprimarysuccesswarningdangerinfooutlinesize sm
Usage
tsx
<Badge variant="success">AI plan</Badge>
<Badge size="sm">tag</Badge>

Tabs

Radix Tabs with roving focus: ←/→ switch tabs, Tab moves into the panel. Used for “Ask AI / Pick template” in the builder.

Describe your app and let the agent plan it.
Usage
tsx
<Tabs defaultValue="a">
  <TabsList aria-label="Start"><TabsTrigger value="a">A</TabsTrigger><TabsTrigger value="b">B</TabsTrigger></TabsList>
  <TabsContent value="a">…</TabsContent>
</Tabs>

Dialog & Sheet

Modal dialog with focus trap, Escape to close, scroll lock and focus return. `side="right"` turns it into a sheet — the mobile navigation uses it.

Usage
tsx
<Dialog>
  <DialogTrigger asChild><Button>Open</Button></DialogTrigger>
  <DialogContent title="Title" description="…">…</DialogContent>
</Dialog>

Toast

Non-blocking feedback for copy, download, add/remove. Announced politely (errors assertively), swipe or ✕ to dismiss, auto-hide after 3.5 s and paused on hover/focus.

Usage
tsx
const toast = useToast();
toast({ title: "Copied", description: "12 commands", variant: "success" });

Tooltip & Kbd

Tooltips supplement — never replace — an accessible name. They open on hover and keyboard focus. Kbd renders keyboard shortcuts.

Command palette: Ctrl K / ⌘ K
Usage
tsx
<Tooltip content="Move up"><Button size="icon" aria-label="Move up">↑</Button></Tooltip>
<Kbd>Ctrl</Kbd> <Kbd>K</Kbd>

CodeBlock

Always-dark code surface with a labelled copy button, optional title and shell prompt (prompt glyphs are not copied). Fires a toast when inside ToastProvider.

bash
nvm install --ltsnpm install rechartsnpm run dev
src/app/page.tsx
export default function Page() {
  return <h1>Hello NVX</h1>;
}
Usage
tsx
<CodeBlock code={"npm install\nnpm run dev"} prompt title="bash" toastTitle="Copied" />

Stepper

Shows progress through a multi-step flow. The current step has aria-current="step"; completed steps show ✓. Clickable when onStepChange is set. Vertical on mobile.

Usage
tsx
<Stepper current={step} onStepChange={setStep}
  items={[{ id: "start", label: "Start" }, { id: "configure", label: "Configure" }]} />

EmptyState

Explains why there's nothing to show and offers the next action. The danger tone is used for recoverable errors (role=alert).

No templates match your search.

Usage
tsx
<EmptyState icon="🔍" title="No templates match" action={<Button>Clear filters</Button>} />
<EmptyState tone="danger" title="Couldn't plan" description={error} action={…} />

Skeleton & Spinner

Skeletons mirror the shape of the content being loaded (used while the AI agent plans). Wrap in role=status with a visually hidden label. Spinner is for inline/button loading.

Loading…
Spinner
Usage
tsx
<div role="status"><span className="sr-only">Loading…</span><Skeleton className="h-4 w-2/3" /></div>
<Spinner label="Loading" />