The web app uses shadcn/ui components built on Base UI primitives for accessibility and flexibility.
Component Architecture
components/
├── ui/ # shadcn/ui primitives
│ ├── button.tsx
│ ├── card.tsx
│ ├── heading.tsx
│ └── dropdown-menu.tsx
├── blocks/ # Composite components
│ └── theme/
│ ├── theme-provider.tsx
│ └── theme-toggle.tsx
└── lib/ # Utilities
└── utils.ts
Composition over inheritance - Build complex UIs from simple components
Accessibility first - All components use proper ARIA attributes
Type safety - Full TypeScript support with variants
Styling flexibility - Tailwind + CVA for consistent variants
Base UI primitives - Headless components with full control
Core Utilities
The cn() Helper
Combines Tailwind classes safely:
import { clsx , type ClassValue } from "clsx" ;
import { twMerge } from "tailwind-merge" ;
export function cn ( ... inputs : ClassValue []) {
return twMerge ( clsx ( inputs ));
}
Usage:
import { cn } from "@/components/lib/utils" ;
// Merge classes with conditional logic
const classes = cn (
"base-class" ,
isActive && "active-class" ,
variant === "primary" && "primary-class" ,
className // Allow prop overrides
);
cn() handles class conflicts intelligently. Later classes override earlier ones: cn("p-4", "p-2") → "p-2"
UI Components
Fully typed button with variants using CVA (Class Variance Authority):
The code below is simplified for clarity. The actual implementation in web/components/ui/button.tsx includes additional variants (secondary, link), size options (xs, icon-xs, icon-sm, icon-lg), dark mode support, and aria-expanded states. See the source file for the complete implementation.
"use client"
import { Button as ButtonPrimitive } from "@base-ui/react/button"
import { cva , type VariantProps } from "class-variance-authority"
import { cn } from "@/components/lib/utils"
const buttonVariants = cva (
"focus-visible:ring-3 inline-flex items-center justify-center rounded-md text-sm font-medium transition-all disabled:opacity-50" ,
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/80" ,
outline: "border-border bg-background hover:bg-muted" ,
ghost: "hover:bg-muted hover:text-foreground" ,
destructive: "bg-destructive/10 text-destructive hover:bg-destructive/20" ,
},
size: {
default: "h-9 px-2.5" ,
sm: "h-8 px-2.5" ,
lg: "h-10 px-2.5" ,
icon: "size-9" ,
},
},
defaultVariants: {
variant: "default" ,
size: "default" ,
},
}
)
function Button ({
className ,
variant = "default" ,
size = "default" ,
... props
} : ButtonPrimitive . Props & VariantProps < typeof buttonVariants >) {
return (
< ButtonPrimitive
className = { cn ( buttonVariants ({ variant , size , className })) }
{ ... props }
/>
)
}
export { Button , buttonVariants }
Usage:
import { Button } from "@/components/ui/button" ;
< Button variant = "default" > Primary Action </ Button >
< Button variant = "outline" > Secondary </ Button >
< Button variant = "ghost" > Tertiary </ Button >
< Button variant = "destructive" > Delete </ Button >
Card Components
Composite card layout system:
import { cn } from "@/components/lib/utils"
function Card ({ className , size = "default" , ... props }) {
return (
< div
data-slot = "card"
className = { cn (
"bg-card text-card-foreground rounded-xl shadow-xs ring-1 ring-foreground/10" ,
"gap-6 py-6 data-[size=sm]:gap-4 data-[size=sm]:py-4" ,
className
) }
{ ... props }
/>
)
}
function CardHeader ({ className , ... props }) {
return < div className = { cn ( "px-6" , className ) } { ... props } />
}
function CardTitle ({ className , ... props }) {
return < div className = { cn ( "text-base font-medium" , className ) } { ... props } />
}
function CardDescription ({ className , ... props }) {
return < div className = { cn ( "text-sm text-muted-foreground" , className ) } { ... props } />
}
function CardContent ({ className , ... props }) {
return < div className = { cn ( "px-6" , className ) } { ... props } />
}
function CardFooter ({ className , ... props }) {
return < div className = { cn ( "px-6 flex items-center" , className ) } { ... props } />
}
export { Card , CardHeader , CardTitle , CardDescription , CardContent , CardFooter }
Usage:
import { Card , CardHeader , CardTitle , CardDescription , CardContent } from "@/components/ui/card" ;
< Card >
< CardHeader >
< CardTitle > Card Title </ CardTitle >
< CardDescription > Supporting description text </ CardDescription >
</ CardHeader >
< CardContent >
< p > Main card content goes here. </ p >
</ CardContent >
</ Card >
Heading Component
Semantic headings with variant styling:
components/ui/heading.tsx
import { cva , type VariantProps } from "class-variance-authority" ;
import { cn } from "@/components/lib/utils" ;
const headingVariants = cva ( "scroll-m-20 tracking-tight" , {
variants: {
variant: {
h1: "text-4xl font-semibold lg:text-5xl" ,
h2: "pb-2 text-3xl font-semibold" ,
h3: "text-2xl font-semibold" ,
h4: "text-xl font-semibold" ,
},
},
defaultVariants: { variant: "h2" },
});
function Heading ({ className , variant = "h2" , ... props }) {
const Tag = variant ?? "h2" ;
return < Tag className = { cn ( headingVariants ({ variant , className })) } { ... props } /> ;
}
export { Heading };
Usage:
import { Heading } from "@/components/ui/heading" ;
< Heading variant = "h1" > Page Title </ Heading >
< Heading variant = "h2" > Section Title </ Heading >
< Heading variant = "h3" > Subsection </ Heading >
The variant prop controls both the semantic HTML tag (<h1>, <h2>) and the visual styling.
Accessible dropdown built on Base UI Menu primitives:
Basic Usage
With Groups & Labels
Radio Group
import {
DropdownMenu ,
DropdownMenuTrigger ,
DropdownMenuContent ,
DropdownMenuItem ,
} from "@/components/ui/dropdown-menu" ;
import { Button } from "@/components/ui/button" ;
< DropdownMenu >
< DropdownMenuTrigger render = { < Button variant = "outline" /> } >
Open Menu
</ DropdownMenuTrigger >
< DropdownMenuContent >
< DropdownMenuItem > Profile </ DropdownMenuItem >
< DropdownMenuItem > Settings </ DropdownMenuItem >
< DropdownMenuItem variant = "destructive" > Logout </ DropdownMenuItem >
</ DropdownMenuContent >
</ DropdownMenu >
Block Components
Theme Provider
Wraps the app to enable dark mode:
components/blocks/theme/theme-provider.tsx
"use client" ;
import { ThemeProviderProps } from "next-themes" ;
import dynamic from "next/dynamic" ;
const NextThemesProvider = dynamic (
() => import ( "next-themes" ). then (( e ) => e . ThemeProvider ),
{ ssr: false }
);
export function ThemeProvider ({ children , ... props } : ThemeProviderProps ) {
return < NextThemesProvider { ... props } > { children } </ NextThemesProvider > ;
}
Usage in Layout:
import { ThemeProvider } from "@/components/blocks/theme/theme-provider" ;
export default function RootLayout ({ children }) {
return (
< html lang = "en" >
< body >
< ThemeProvider attribute = "class" defaultTheme = "system" enableSystem >
{ children }
</ ThemeProvider >
</ body >
</ html >
);
}
Theme Toggle
Button to switch between light/dark/system themes:
components/blocks/theme/theme-toggle.tsx
"use client" ;
import { useTheme } from "next-themes" ;
import { Button } from "@/components/ui/button" ;
import { DropdownMenu , /* ... */ } from "@/components/ui/dropdown-menu" ;
import { MoonIcon , SunIcon , ComputerIcon } from "@hugeicons/core-free-icons" ;
export function ThemeToggle () {
const { theme , setTheme , resolvedTheme } = useTheme ();
return (
< DropdownMenu >
< DropdownMenuTrigger render = { < Button variant = "outline" size = "icon" /> } >
< HugeiconsIcon icon = { resolvedTheme === "dark" ? MoonIcon : SunIcon } />
</ DropdownMenuTrigger >
< DropdownMenuContent >
< DropdownMenuRadioGroup value = { theme } onValueChange = { setTheme } >
< DropdownMenuRadioItem value = "light" > Light </ DropdownMenuRadioItem >
< DropdownMenuRadioItem value = "dark" > Dark </ DropdownMenuRadioItem >
< DropdownMenuRadioItem value = "system" > System </ DropdownMenuRadioItem >
</ DropdownMenuRadioGroup >
</ DropdownMenuContent >
</ DropdownMenu >
);
}
Component Best Practices
Client Components Mark interactive components with "use client" directive
Export Variants Export variants cva objects for reuse: export { buttonVariants }
Forward Props Spread ...props to preserve all HTML attributes
Composition Build complex components from simple primitives
Adding New Components
Use the shadcn CLI to add components:
npx shadcn@latest add [component-name]
Available components:
button, card, dialog, dropdown-menu
input, label, select, textarea
tabs, accordion, popover, tooltip
Full list
After adding components, run bun lint and bun format to ensure code quality.
Next Steps
Features Learn feature-based architecture
State Management Manage state with Zustand