Components
Every component imports from its own entry point (@viliha/vui-ui/<name>), so you ship only what you use. Live examples below.
Button
Seven variants and four sizes; the primary variant carries the brand blue.
import { Button } from "@viliha/vui-ui/button";
<Button>Default</Button>
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
<Button variant="destructive">Destructive</Button>Badge
import { Badge } from "@viliha/vui-ui/badge";
<Badge>Default</Badge>
<Badge variant="secondary">Secondary</Badge>
<Badge variant="outline">Outline</Badge>
<Badge variant="muted">Muted</Badge>
<Badge variant="success">Success</Badge>
<Badge variant="warning">Warning</Badge>
<Badge variant="destructive">Destructive</Badge>Card
Team
import { Card, CardContent, CardHeader, CardTitle } from "@viliha/vui-ui/card";
<Card>
<CardHeader><CardTitle>Team</CardTitle></CardHeader>
<CardContent>12 members across 3 regions.</CardContent>
</Card>Input
import { Input } from "@viliha/vui-ui/input";
<Input placeholder="you@example.com" />Checkbox
import { Checkbox } from "@viliha/vui-ui/checkbox";
<Checkbox checked={checked} onChange={(e) => setChecked(e.target.checked)} />Select
import { Select } from "@viliha/vui-ui/select";
<Select
value={value}
onValueChange={setValue}
options={[
{ value: "apple", label: "Apple" },
{ value: "banana", label: "Banana" },
]}
/>Combobox
A searchable single-select. Same API as Select, but the popover leads with a type-to-filter input. Use it for long option lists (an FK / country picker) where scrolling a Select is painful.
import { Combobox } from "@viliha/vui-ui/combobox";
<Combobox
value={value}
onValueChange={setValue}
options={countries} // [{ value, label }, …]
placeholder="Select country…"
searchPlaceholder="Search countries…"
/>Cascading combobox
One searchable Combobox per fixed, named level(Region → Country → State → City). Picking a level narrows the next from the selected node's children and clears everything downstream; a level stays disabled until its parent is chosen. Depth comes from the data: pass a 3-, 4-, or N-level tree.
import { CascadingCombobox } from "@viliha/vui-ui/cascading-combobox";
<CascadingCombobox
levels={[
{ key: "region", label: "Region" },
{ key: "country", label: "Country" },
{ key: "state", label: "State" },
{ key: "city", label: "City" },
]}
items={LOCATIONS} // tree: node.children feeds the next level
value={path} // string[], one value per level
onValueChange={(next) => setPath(next)}
orientation="horizontal"
/>Toast
Global notifications for errors and events. Mount <Toaster /> once in your root layout, then call toast(...) from anywhere (event handlers, catch blocks), no provider needed. Supports a title, description, an action button (e.g. Undo), and success/error/warning variants.
import { toast, Toaster } from "@viliha/vui-ui/toast";
// once, in the root layout:
<Toaster />
// anywhere:
toast({
title: "Event created",
description: "Sunday, December 3 at 9:00 AM",
action: { label: "Undo", onClick: restore },
});
toast.success("Saved");
toast.error("Couldn't save", { description: "Try again." });Tooltip
A themed, dependency-free tooltip matching the shadcn design (dark bubble + arrow), portal-positioned with hover/focus. side defaults to "auto", it prefers bottom and flips to toponly when there isn't room below (e.g. the last rows of a table). RecordView uses it for truncated cells; reach for it instead of the native title attribute.
import { Tooltip } from "@viliha/vui-ui/tooltip";
<Tooltip content="Full text shown on hover" side="auto">
<span className="underline decoration-dotted">Hover me</span>
</Tooltip>Avatar
import { Avatar, AvatarFallback } from "@viliha/vui-ui/avatar";
<Avatar><AvatarFallback>SB</AvatarFallback></Avatar>Dropdown
import { Dropdown, DropdownItem, DropdownLabel } from "@viliha/vui-ui/dropdown-menu";
<Dropdown label="Actions">
<DropdownLabel>Manage</DropdownLabel>
<DropdownItem onSelect={() => {}}>Edit</DropdownItem>
<DropdownItem onSelect={() => {}}>Duplicate</DropdownItem>
</Dropdown>RecordView
The full data table: editable cells, auto-sizing columns, a sticky header, sort/filter/pagination, row actions, a buffered add/edit form, and CSV/JSON/Excel/PDF import & export. Configure it with a fields array and mock or real data, then see it live in the backoffice demo.
import { RecordView, type RecordField } from "@viliha/vui-ui/record-view";
const fields: RecordField<Org>[] = [
{ key: "name", label: "Name", editable: true, required: true, hideInTable: true },
{ key: "domain", label: "Domain", editable: true, copyable: true },
{ key: "country", label: "Country", editable: true },
];
<RecordView
title="Organizations"
singular="Organization"
fields={fields}
initialData={data}
makeEmptyRow={() => ({ id: Date.now(), name: "", domain: "", country: "" })}
getPrimary={(row) => ({ title: row.name, initials: row.name.slice(0, 2) })}
/>Table
Low-level, unstyled-to-theme table primitives (Table, TableHeader, TableBody, TableRow, TableHead, TableCell, plus TableFooter / TableCaption). Use these for a plain static table; for a data grid with sorting, filtering, paging and editing, reach for RecordView instead.
| Name | Role |
|---|---|
| Ada Lovelace | Admin |
| Alan Turing | Member |
import {
Table, TableHeader, TableBody, TableRow, TableHead, TableCell,
} from "@viliha/vui-ui/table";
<Table>
<TableHeader>
<TableRow><TableHead>Name</TableHead><TableHead>Role</TableHead></TableRow>
</TableHeader>
<TableBody>
<TableRow><TableCell>Ada</TableCell><TableCell>Admin</TableCell></TableRow>
</TableBody>
</Table>Kbd & Shortcut
Keyboard key caps. Kbd renders a single cap; Shortcut renders a combo from a keys array, joined with +, used in menus and the ⌘K launchers.
import { Kbd, Shortcut } from "@viliha/vui-ui/kbd";
<Kbd>Esc</Kbd>
<Shortcut keys={["⌘", "K"]} />Required mark
The mandatory-field asterisk used across forms, filters and auth: one consistent cue. Place it next to a label; it takes no props.
import { RequiredMark } from "@viliha/vui-ui/required-mark";
<label className="flex items-center gap-1">Email <RequiredMark /></label>Code
A minimal inline <code> element for referencing a token, path or value in running text (as opposed to CodeBlock, which is for multi-line snippets).
pnpm dev to start.import { Code } from "@viliha/vui-ui/code";
Run <Code>pnpm dev</Code> to start.