Skip to content

shadcn-ui Setup & Components

Whity Core uses shadcn-ui with a custom preset for the web dashboard, providing a collection of accessible, unstyled component primitives built on Radix UI.

shadcn-ui is not a component library you install — it’s a collection of copy-paste components built on:

  • Radix UI — Unstyled, accessible primitives
  • Tailwind CSS — Utility-first styling
  • React Hook Form — Form state management

Components are copied into your project source code, giving you full control over styling and behavior.

Our web application is set up with shadcn-ui at /web with a custom preset:

Terminal window
# The preset includes:
# - RTL (Right-to-Left) language support
# - Pointer event enhancements
# - Custom theme tokens
npx shadcn@latest init --preset b1D0eTWj --template next --rtl --pointer
  • components.json — shadcn config with import paths
  • lib/utils.ts — Tailwind merge utilities
  • app/globals.css — Design tokens and theme system
  • Base components installed: Button, UI utilities

Add any component from the shadcn registry:

Terminal window
cd web
# Add a single component
npx shadcn-ui@latest add button
# Add multiple components
npx shadcn-ui@latest add input textarea select
# Add with aliases
npx shadcn-ui@latest add dialog --alias "use-dialog"

Popular components ready to install:

Forms & Input

  • input — Text, email, password inputs
  • textarea — Multi-line text
  • checkbox — Checkbox inputs
  • radio-group — Radio buttons
  • select — Dropdown select
  • switch — Toggle switches
  • slider — Range slider
  • form — Complete form system with validation

Display

  • button — Primary, secondary, outline variants
  • badge — Label badges
  • card — Card containers
  • table — Data tables
  • avatar — User avatars
  • image — Optimized images

Navigation

  • sidebar — Collapsible navigation
  • breadcrumb — Breadcrumb navigation
  • navigation-menu — Dropdown menus
  • tabs — Tab navigation
  • pagination — Page navigation

Feedback

  • alert — Alert messages
  • alert-dialog — Confirmation dialogs
  • dialog — Modal dialogs
  • toast — Notification toasts
  • progress — Progress bars
  • skeleton — Loading skeletons

Disclosure

  • accordion — Collapsible sections
  • popover — Floating popovers
  • dropdown-menu — Context menus
  • sheet — Side panels
  • tooltip — Hover tooltips

See the full registry: https://ui.shadcn.com/docs/components/

Components are imported from @/components/ui:

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export default function Dashboard() {
return (
<Card>
<CardHeader>
<CardTitle>Welcome</CardTitle>
</CardHeader>
<CardContent>
<Input placeholder="Enter your name" />
<Button>Submit</Button>
</CardContent>
</Card>
);
}

Each component accepts standard HTML props:

// Button variants
<Button>Default</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Delete</Button>
<Button disabled>Disabled</Button>
// Sizes
<Button size="sm">Small</Button>
<Button size="default">Default</Button>
<Button size="lg">Large</Button>
// States
<Button onClick={handleClick}>Click me</Button>
<Button loading>Loading...</Button>
<Button aria-label="Close">×</Button>

Components use Tailwind CSS classes. Edit them in /web/components/ui/:

components/ui/button.tsx
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
// ... more variants
},
},
}
);

Create wrapper components for project-specific behavior:

components/dashboard-button.tsx
import { Button } from "@/components/ui/button";
export function DashboardButton(props) {
return (
<Button
size="lg"
className="w-full"
{...props}
/>
);
}

All components automatically use the design tokens from globals.css:

/* In globals.css */
:root {
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--background: oklch(1 0 0);
/* ... more tokens ... */
}
/* Components use these via Tailwind */
<Button className="bg-primary text-primary-foreground">

Change theme tokens and all components update automatically.

shadcn components are copied into your project. Feel free to modify them:

Terminal window
# This copies the component to your project
npx shadcn-ui@latest add button
# Edit it freely — it's now your code
# components/ui/button.tsx

Prefer semantic names over generic colors:

// ✅ Good
<Button variant="destructive">Delete</Button>
// ❌ Avoid
<Button className="bg-red-500">Delete</Button>

Build complex UIs from simple components:

<Card>
<CardHeader>
<CardTitle>Settings</CardTitle>
</CardHeader>
<CardContent>
<form>
<div className="space-y-4">
<Input placeholder="Email" />
<Select>
<SelectItem value="admin">Admin</SelectItem>
</Select>
<Button>Save</Button>
</div>
</form>
</CardContent>
</Card>

All components follow WAI-ARIA standards. Use semantic HTML:

// Components handle accessibility
<Button aria-label="Close menu">×</Button>
<Dialog open={open} onOpenChange={setOpen}>

Components automatically support dark mode via .dark class:

// In layout.tsx
export default function RootLayout({ children }) {
const [isDark, setIsDark] = useState(false);
return (
<html className={isDark ? "dark" : ""}>
<body>{children}</body>
</html>
);
}

Our custom preset (b1D0eTWj) includes:

  • ✅ RTL support for international applications
  • ✅ Pointer event enhancements for better mobile UX
  • ✅ Optimized component defaults
  • ✅ Integrated theme token system

To regenerate the preset or use a different one:

Terminal window
npx shadcn-ui@latest init --preset <preset-id>

Browse presets: https://ui.shadcn.com/create

import { useForm } from "react-hook-form";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
export function LoginForm() {
const { register, handleSubmit } = useForm();
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Input {...register("email")} type="email" />
<Input {...register("password")} type="password" />
<Button type="submit">Login</Button>
</form>
);
}
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
export function ConfirmDialog({ open, onConfirm, onCancel }) {
return (
<Dialog open={open} onOpenChange={onCancel}>
<DialogContent>
<DialogHeader>
<DialogTitle>Confirm Action</DialogTitle>
</DialogHeader>
<div className="flex gap-2 justify-end">
<Button variant="outline" onClick={onCancel}>Cancel</Button>
<Button onClick={onConfirm}>Confirm</Button>
</div>
</DialogContent>
</Dialog>
);
}

Sharing Components via the Whity Registry (WC-168)

Section titled “Sharing Components via the Whity Registry (WC-168)”

web/registry.json declares every components/ui/* and components/admin/* component as a shadcn registry item (with its npm and intra-registry dependencies). The registry build output is generated, never committed:

  • npm run registry:build (also runs automatically as prebuild before next build) writes the distributable item JSONs to web/public/r/, so any built deployment serves its registry at https://<host>/r/{name}.json.

  • A consuming app adds to its own components.json:

    "registries": { "@whity": "https://<whity-host>/r/{name}.json" }

    and pulls components by copy-in: npx shadcn add @whity/data-table. The CLI resolves intra-registry dependencies (e.g. data-table brings @whity/skeleton) and installs the npm packages the item declares.

This is deliberate copy-in distribution — no published npm package — per the Option C decision (#168): no publish/version burden until a real second consumer demands it.

Notes:

  • The @whity mapping committed in whity’s own components.json points at http://localhost:3000 as a local-dev default: pulling from it requires npm run registry:build first AND the dev server running (/public/r/ is gitignored; prebuild only fires on next build). Deployed hosts serve it out of the box.
  • Always consume via the @whity namespace mapping, not raw item URLs — the items’ registryDependencies (e.g. @whity/skeleton) only resolve through the configured namespace.