Skip to main content
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

Core Utilities

The cn() Helper

Combines Tailwind classes safely:
components/lib/utils.ts
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

Button Component

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.
components/ui/button.tsx
"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:
components/ui/card.tsx
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:
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:
app/layout.tsx
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

Build docs developers (and LLMs) love