Vertz ships a full set of ready-to-use UI components with a shadcn-inspired theme. Use them instead of building from scratch.
Architecture
The component system has two layers:
@vertz/ui-primitives — Headless, accessible components with WAI-ARIA patterns. No styling, just behavior.
@vertz/theme-shadcn — Wraps every primitive with shadcn-inspired styles, plus adds common components like Button, Input, Card, and Table.
Most apps only import from @vertz/theme-shadcn.
Setup
Call configureTheme() once and register it with registerTheme():
// src/styles/theme.ts
import { configureTheme } from '@vertz/theme-shadcn';
import { registerTheme } from '@vertz/ui';
const config = configureTheme({ palette: 'zinc', radius: 'md' });
registerTheme(config);
export const appTheme = config.theme;
export const themeGlobals = config.globals;
Then import components from @vertz/ui/components anywhere in your app:
import { Button, Input, Dialog, Select } from '@vertz/ui/components';
Direct components (Button, Card, etc.) are simple styled wrappers. Primitive
components (Dialog, Select, etc.) are themed wrappers around @vertz/ui-primitives with
compound sub-components (e.g., Dialog.Title, Dialog.Footer, Select.Content).
Triggers an action or event. Supports intents and sizes.
import { Button } from '@vertz/ui/components';
<Button intent="primary" size="md">Save</Button>
<Button intent="outline" size="sm">Cancel</Button>
<Button intent="destructive" size="md">Delete</Button>
<Button intent="ghost" size="md">Ghost</Button>
Intents: primary, secondary, outline, ghost, destructive, link. Sizes: sm, md, lg.
Text input field for forms.
import { Input } from '@vertz/ui/components';
<Input placeholder="Enter your email..." />
<Input type="password" placeholder="Password" />
<Input disabled placeholder="Disabled" />
Textarea
Multi-line text input.
import { Textarea } from '@vertz/ui/components';
<Textarea placeholder="Write a message..." />;
Label
Accessible label for form controls.
import { Label } from '@vertz/ui/components';
<Label for="email">Email</Label>
<Input id="email" name="email" placeholder="you@example.com" />
Groups form controls with error display.
import { FormGroup, Label, Input } from '@vertz/ui/components';
<FormGroup.FormGroup>
<Label>Email</Label>
<Input name="email" placeholder="Enter email" />
<FormGroup.FormError>Please enter a valid email address</FormGroup.FormError>
</FormGroup.FormGroup>;
Select
Dropdown selection control.
import { Select } from '@vertz/ui/components';
<Select defaultValue="Select a fruit...">
<Select.Content>
<Select.Group label="Fruits">
<Select.Item value="apple">Apple</Select.Item>
<Select.Item value="banana">Banana</Select.Item>
</Select.Group>
<Select.Separator />
<Select.Group label="Vegetables">
<Select.Item value="carrot">Carrot</Select.Item>
</Select.Group>
</Select.Content>
</Select>;
Layout
Card
Container with header, content, and footer.
import { Card, Button } from '@vertz/ui/components';
<Card.Card>
<Card.CardHeader>
<Card.CardTitle>Card Title</Card.CardTitle>
<Card.CardDescription>Card description goes here.</Card.CardDescription>
</Card.CardHeader>
<Card.CardContent>This is the card content area.</Card.CardContent>
<Card.CardFooter>
<Button intent="outline" size="sm">
Cancel
</Button>
<Button intent="primary" size="sm">
Save
</Button>
</Card.CardFooter>
</Card.Card>;
Separator
Visual divider between content.
import { Separator } from '@vertz/ui/components';
<Separator />;
Accordion
Expandable/collapsible content sections.
import { Accordion } from '@vertz/ui/components';
<Accordion>
<Accordion.Item value="item-1">
<Accordion.Trigger>Is it accessible?</Accordion.Trigger>
<Accordion.Content>Yes. It adheres to WAI-ARIA patterns.</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="item-2">
<Accordion.Trigger>Is it styled?</Accordion.Trigger>
<Accordion.Content>Yes. It matches the theme.</Accordion.Content>
</Accordion.Item>
</Accordion>;
Tabs
Tabbed content organization.
import { Tabs } from '@vertz/ui/components';
<Tabs defaultValue="account">
<Tabs.List>
<Tabs.Trigger value="account">Account</Tabs.Trigger>
<Tabs.Trigger value="password">Password</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="account">Account settings here.</Tabs.Content>
<Tabs.Content value="password">Password settings here.</Tabs.Content>
</Tabs>;
Supports variant="line" for an underline tab style.
Data Display
Badge
Small status or count indicator.
import { Badge } from '@vertz/ui/components';
<Badge color="blue">New</Badge>
<Badge color="green">Active</Badge>
<Badge color="red">Error</Badge>
<Badge color="yellow">Warning</Badge>
<Badge color="gray">Draft</Badge>
Avatar
User profile image with fallback.
import { Avatar } from '@vertz/ui/components';
<Avatar.Avatar>
<Avatar.AvatarFallback>JD</Avatar.AvatarFallback>
</Avatar.Avatar>;
Table
Tabular data display.
import { Table } from '@vertz/ui/components';
<Table.Table>
<Table.TableHeader>
<Table.TableRow>
<Table.TableHead>Name</Table.TableHead>
<Table.TableHead>Status</Table.TableHead>
<Table.TableHead>Role</Table.TableHead>
</Table.TableRow>
</Table.TableHeader>
<Table.TableBody>
<Table.TableRow>
<Table.TableCell>Alice Johnson</Table.TableCell>
<Table.TableCell>Active</Table.TableCell>
<Table.TableCell>Admin</Table.TableCell>
</Table.TableRow>
</Table.TableBody>
</Table.Table>;
Skeleton
Loading placeholder with built-in pulse animation. Comes with Text and Circle sub-components for common patterns.
import { Skeleton } from '@vertz/ui/components';
{/* Basic box skeleton */}
<Skeleton width="200px" height="16px" />
{/* Multi-line text skeleton (3 lines by default, last line shorter) */}
<Skeleton.Text />
{/* Custom line count and last-line width */}
<Skeleton.Text lines={5} lastLineWidth="50%" />
{/* Circular skeleton for avatars */}
<Skeleton.Circle />
<Skeleton.Circle size="48px" />
EmptyState
Compound component for empty-data placeholders with icon, title, description, and action slots.
import { Button, EmptyState } from '@vertz/ui/components';
<EmptyState>
<EmptyState.Icon>
<InboxIcon />
</EmptyState.Icon>
<EmptyState.Title>No issues yet</EmptyState.Title>
<EmptyState.Description>Create your first issue to get started.</EmptyState.Description>
<EmptyState.Action>
<Button intent="primary" size="sm">
New Issue
</Button>
</EmptyState.Action>
</EmptyState>;
All slots are optional — use only what you need:
<EmptyState>
<EmptyState.Title>No results</EmptyState.Title>
<EmptyState.Description>Try adjusting your filters.</EmptyState.Description>
</EmptyState>
Overlays
Dialog
All dialogs use useDialogStack() with native <dialog> + showModal(). Dialog sub-components
(Dialog.Header, Dialog.Title, etc.) are used inside the dialog component.
import { useDialogStack } from '@vertz/ui';
import type { DialogHandle } from '@vertz/ui';
import { Button, Dialog } from '@vertz/ui/components';
function EditProfileDialog({ dialog }: { dialog: DialogHandle<void> }) {
return (
<>
<Dialog.Header>
<Dialog.Title>Edit profile</Dialog.Title>
<Dialog.Description>Make changes to your profile here.</Dialog.Description>
</Dialog.Header>
<Dialog.Body>{/* Form fields go here */}</Dialog.Body>
<Dialog.Footer>
<Dialog.Cancel>Cancel</Dialog.Cancel>
<Button intent="primary" size="md" onClick={() => dialog.close()}>
Save changes
</Button>
</Dialog.Footer>
</>
);
}
// Open via DialogStack
const dialogs = useDialogStack();
await dialogs.open(EditProfileDialog, {});
Confirm Dialog
Quick confirmation via useDialogStack().confirm():
import { useDialogStack } from '@vertz/ui';
import { Button } from '@vertz/ui/components';
function DeleteButton() {
const dialogs = useDialogStack();
async function handleDelete() {
const confirmed = await dialogs.confirm({
title: 'Are you absolutely sure?',
description: 'This action cannot be undone.',
confirm: 'Continue',
cancel: 'Cancel',
intent: 'danger',
});
if (confirmed) {
// perform delete
}
}
return (
<Button intent="destructive" size="md" onClick={handleDelete}>
Delete Account
</Button>
);
}
Sheet
Side panel that slides in from edge.
import { Button } from '@vertz/ui/components';
import { Sheet } from '@vertz/ui/components';
<Sheet>
<Sheet.Trigger>
<Button intent="outline" size="md">
Open Sheet
</Button>
</Sheet.Trigger>
<Sheet.Content>
<Sheet.Title>Edit profile</Sheet.Title>
<Sheet.Description>Make changes to your profile here.</Sheet.Description>
{/* Content goes here */}
</Sheet.Content>
</Sheet>;
Supports side="left", side="right" (default), side="top", side="bottom".
Popover
Floating content anchored to trigger.
import { Button } from '@vertz/ui/components';
import { Popover } from '@vertz/ui/components';
<Popover>
<Popover.Trigger>
<Button intent="outline" size="md">
Open popover
</Button>
</Popover.Trigger>
<Popover.Content>
<div style="padding: 16px; width: 280px;">
<h4>Dimensions</h4>
<p>Set the dimensions for the layer.</p>
</div>
</Popover.Content>
</Popover>;
Brief info on hover or focus.
import { Button } from '@vertz/ui/components';
import { Tooltip } from '@vertz/ui/components';
<Tooltip>
<Tooltip.Trigger>
<Button intent="outline" size="md">
Hover me
</Button>
</Tooltip.Trigger>
<Tooltip.Content>Add to library</Tooltip.Content>
</Tooltip>;
Navigation
Breadcrumb
Navigation breadcrumb trail.
import { Breadcrumb } from '@vertz/ui/components';
<Breadcrumb
items={[
{ label: 'Home', href: '/' },
{ label: 'Projects', href: '/projects' },
{ label: 'Current Project' },
]}
/>;
The last item renders as the current page (no link). Supports a custom separator prop (defaults to /).
Page navigation controls.
import { Pagination } from '@vertz/ui/components';
<Pagination
currentPage={2}
totalPages={10}
onPageChange={(page) => {
/* navigate to page */
}}
/>;
Supports siblingCount to control how many page numbers show around the current page.
Menu triggered by a button click.
import { Button } from '@vertz/ui/components';
import { DropdownMenu } from '@vertz/ui/components';
<DropdownMenu>
<DropdownMenu.Trigger>
<Button intent="outline" size="md">
Open
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Label>My Account</DropdownMenu.Label>
<DropdownMenu.Separator />
<DropdownMenu.Item value="profile">Profile</DropdownMenu.Item>
<DropdownMenu.Item value="billing">Billing</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item value="logout">Log out</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu>;
Feedback
Alert
Inline alert messages.
import { Alert } from '@vertz/ui/components';
<Alert.Alert>
<Alert.AlertTitle>Heads up!</Alert.AlertTitle>
<Alert.AlertDescription>You can add components using the CLI.</Alert.AlertDescription>
</Alert.Alert>
<A.Alert variant="destructive">
<Alert.AlertTitle>Error</Alert.AlertTitle>
<Alert.AlertDescription>Your session has expired.</Alert.AlertDescription>
</Alert.Alert>
Factory-based primitives
These components use an imperative factory API — they return DOM elements rather than JSX compound components. Import them from @vertz/ui/components.
These primitives haven’t been converted to JSX compound components yet. They work by returning DOM
elements that you embed in your JSX tree. JSX versions are planned.
Checkbox
Toggle control for boolean values.
import { Checkbox } from '@vertz/ui/components';
<label style="display: flex; align-items: center; gap: 8px">
{Checkbox({ defaultChecked: true })}
Accept terms and conditions
</label>;
Switch
Toggle between on and off states.
import { Switch } from '@vertz/ui/components';
{
Switch({ defaultChecked: true });
}
Supports size: 'sm' for compact variant.
RadioGroup
Select one option from a set.
import { RadioGroup } from '@vertz/ui/components';
const radio = RadioGroup({ defaultValue: 'comfortable' });
radio.Item('default', 'Default');
radio.Item('comfortable', 'Comfortable');
radio.Item('compact', 'Compact');
// Use radio.root in your JSX
{
radio.root;
}
Slider
Range input with track and thumb.
import { Slider } from '@vertz/ui/components';
{
Slider({ defaultValue: 50, min: 0, max: 100 }).root;
}
Toggle
Toggle button with pressed state.
import { Toggle } from '@vertz/ui/components';
{
Toggle();
}
Progress
Shows task completion percentage.
import { Progress } from '@vertz/ui/components';
{
Progress({ defaultValue: 66 }).root;
}
Toast
Temporary notification popup.
import { Button } from '@vertz/ui/components';
import { Toast } from '@vertz/ui/components';
const t = Toast({});
document.body.appendChild(t.region);
<Button intent="outline" size="md" onClick={() => t.announce('Event has been created.')}>
Show Toast
</Button>;
Additional primitives
These components are available via @vertz/ui/components, but are less commonly used:
| Component | Description |
|---|
| Calendar | Date grid with month navigation |
| DatePicker | Date picker composing Calendar + Popover |
| Carousel | Slide navigation with prev/next controls |
| Collapsible | Expandable/collapsible content section |
| Command | Searchable command palette |
| ContextMenu | Right-click context menu |
| Drawer | Bottom/side panel wrapping Dialog |
| HoverCard | Hover-triggered interactive card |
| Menubar | Horizontal menu bar with dropdowns |
| NavigationMenu | Site navigation with hover dropdowns |
| ResizablePanel | Resizable panel layout with drag handles |
| ScrollArea | Custom scrollbars |
| ToggleGroup | Group of toggle buttons |
All are importable from @vertz/ui/components after registering a theme with registerTheme().
Headless usage
For custom styling, import directly from @vertz/ui-primitives and apply your own classes:
import { Dialog } from '@vertz/ui-primitives';
const dialog = Dialog.Root({ modal: true });
See Styling for details on css() and variants().
Theme configuration
configureTheme() accepts palette, radius, and color override options:
const config = configureTheme({
palette: 'zinc', // zinc, slate, stone, gray, neutral
radius: 'md', // none, sm, md, lg, full
colors: {
primary: { DEFAULT: 'oklch(0.55 0.2 260)', _dark: 'oklch(0.65 0.25 260)' },
'primary-foreground': { DEFAULT: '#fff', _dark: '#fff' },
},
});
The colors key deep-merges into the selected palette. Use DEFAULT for light mode and _dark for dark mode. You can also add custom token names.
config.theme — Resolved CSS variables
config.globals — Global CSS string to inject
config.styles — Pre-built style objects for manual styling
config.components — Used internally by registerTheme() — import components from @vertz/ui/components instead