npm

Guides

Using shadcn/ui

Vui Starter is built to pair with shadcn/ui. Lean on shadcn's large, accessible component set for everyday UI (forms, dialogs, tabs, and the rest), and keep Vui's RecordView for datatables. Because they share the same tokens, shadcn components adopt the Vui look with nothing extra to configure.

Why this works

shadcn/ui components are styled entirely with CSS-variable tokens (--background, --primary, --border, …). Vui's theme.css defines exactly those tokens, so the moment you import the Vui theme, every shadcn component renders in the Vui style with no restyling and no overrides.

Division of labor

Reach for shadcn/ui on general UI: buttons, inputs, forms, dialogs, tabs, popovers, and so on. Reach for @viliha/vui-ui's RecordViewon the datatable, with its editable cells, import/export, and add/edit form. That's the one piece shadcn doesn't provide.

See it live

The backoffice demo shows roughly 35 unmodified shadcn components at /components and a fully validated shadcn Form (every field type, backed by Zod) at /forms, all themed by theme.css with zero overrides.

Setup

1. Import the Vui theme

This provides every token shadcn expects, along with Vui's baseline.

app/globals.css
@import "tailwindcss";
@import "@viliha/vui-ui/theme.css";

2. Initialize shadcn/ui

terminal
npx shadcn@latest init

Match these choices so the setup lines up with Vui:

  • Base color: Neutral
  • CSS variables: Yes
  • Icon library: Lucide (shadcn's new-york components ship with Lucide icons baked in; see the Icons note below)

A matching components.json:

components.json
{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "config": "",
    "css": "app/globals.css",
    "baseColor": "neutral",
    "cssVariables": true
  },
  "iconLibrary": "lucide",
  "aliases": { "components": "@/components", "utils": "@/lib/utils" }
}

3. Let Vui own the tokens

shadcn init writes a :root / .darktoken block into your CSS. Since Vui's theme.css already defines those, delete the block shadcn added, or keep the @import after it so Vui wins. One source of truth for tokens means one consistent look.

4. Add components

terminal
npx shadcn@latest add button input label form dialog dropdown-menu tabs

They render in the Vui style right away, with the same radius, colors, borders, and focus rings.

Forms

shadcn's form (React Hook Form with Zod) is the recommended way to build forms on top of Vui:

example.tsx
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Form, FormField, FormItem, FormLabel, FormControl } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";

const schema = z.object({ email: z.string().email() });

export function SignupForm() {
  const form = useForm({ resolver: zodResolver(schema) });
  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(console.log)} className="space-y-4">
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Email</FormLabel>
              <FormControl><Input placeholder="you@example.com" {...field} /></FormControl>
            </FormItem>
          )}
        />
        <Button type="submit">Save</Button>
      </form>
    </Form>
  );
}

Good to know

  • Primary button: shadcn's default Button uses --primary(neutral). For the brand blue, reach for Vui's <Button variant="primary" /> from @viliha/vui-ui/button, or point shadcn's button at the --button-primary token.
  • Dark mode and radius come from the same tokens, so shadcn and Vui components switch together.
  • Icons: shadcn's new-york components ship with Lucideicons internally: the chevron in Select, the check in Checkbox, and so on. Leave those as-is. Vui's global "icon chip" rule targets svg[width="15"] (Radix Icons), so Lucide's icons pass through untouched and stay clean. For your own app chrome (nav, page headers), use @radix-ui/react-icons to get the bordered-chip treatment.