# Bevel > Bevel is a shadcn-style registry of tactile, skeuomorphic React components built on Base UI (@base-ui/react) and Tailwind CSS v4. Components install as source with the shadcn CLI; each one is a real object doing a real job in a product (a cover over a deploy switch, a meter for live spend, alarm lamps that latch). Docs: https://bevel-ui.vercel.app. Source: https://github.com/hiteshbandhu/bevel. Index of these docs: https://bevel-ui.vercel.app/llms.txt. ## Install Bevel needs a React 19 project with Tailwind CSS v4 and shadcn set up (`npx shadcn@latest init`). Add the registry to `components.json` once: ```json { "registries": { "@bevel": "https://bevel-ui.vercel.app/r/{name}.json" } } ``` Then add components by name. Each brings the Bevel foundation stylesheet, its own styles (merged into global CSS) and any Bevel parts it is built from: ```bash npx shadcn@latest add @bevel/ ``` Or with no configuration, from a URL: `npx shadcn@latest add https://bevel-ui.vercel.app/r/.json`. Files land in `components/ui/.tsx` and import as `@/components/ui/`. The registry index is at https://bevel-ui.vercel.app/r/registry.json, so `npx shadcn@latest search @bevel` and the shadcn MCP server (`npx shadcn@latest mcp`) can browse it. ## Rules for writing code with Bevel - Compose parts. Each component file exports named parts (`Wallet`, `WalletCard`, `WalletPocket`), not one component with many props. Follow the anatomy shown for each component. - Polymorphism is Base UI's `render` prop (`} />`). There is no `as` or `asChild`. - Every rendered part sets `data-slot=""` and exposes state as `data-*` attributes (`data-pressed`, `data-checked`, `data-disabled`, `data-status`, `data-variant`). Style from those, e.g. `data-checked:...` in Tailwind, not with JS-switched classes. - `className` is merged last with `cn()` and always wins. - Never hard-code colours, shadows, radii, durations, easings or type sizes. Read the foundation tokens through Tailwind: `rounded-(--bevel-radius-md)`, `duration-(--bevel-duration-move) ease-(--bevel-ease-out)`, `text-(length:--bevel-amount)`. Apply depth and materials with the utilities `bevel-{recessed,flush,raised,pressed,object}` and `bevel-{leather,plastic,metal}`. - One light source, from the top. Nested radii are concentric (inner = outer - inset). - Motion is CSS transitions on `transform`, `translate`, `opacity` and `filter` only; never animate `height`, `width`, `top`/`left`, `margin` or `padding`. Components honour `prefers-reduced-motion`. - Whatever an object shows with light or position, it also says in text, for screen readers and everyone else. - Dark mode follows a `.dark` class on `` (the shadcn default). Theme a component by overriding its CSS variables, not by forking it. ## Foundation tokens Defined in `bevel.css`, installed with the first component. Use these instead of raw values. ### Light - `--bevel-highlight` - `--bevel-highlight-strong` - `--bevel-shade` - `--bevel-shade-strong` - `--bevel-cast` - `--bevel-deboss`: Stamped into a surface: shaded on the upper inner edge, lit below. ### Material defaults - `--bevel-material` - `--bevel-material-edge` - `--bevel-material-recess`: neutral dark; materials set their own - `--bevel-grain` - `--bevel-grain-size` ### Depth - `--bevel-depth-recessed` - `--bevel-depth-flush` - `--bevel-depth-raised` - `--bevel-depth-pressed` - `--bevel-depth-object`: A whole object resting on the page. - `--bevel-depth-cast-sm`: The shadow under a loose object, for filter: drop-shadow(). - `--bevel-depth-sheet`: A thin object (a card) lying on another surface. ### Motion - `--bevel-ease-out` - `--bevel-ease-in-out` - `--bevel-ease-spring`: Underdamped spring, ~8% overshoot. Pure CSS; pair with --bevel-duration-move or longer. - `--bevel-duration-press`: a control giving under a finger - `--bevel-duration-small`: hover lifts, tabs, small state changes - `--bevel-duration-move`: objects moving: cards, drawers - `--bevel-duration-ambient`: whole-scene changes: lighting - `--bevel-stagger` - `--bevel-lift`: how far an object rises on hover - `--bevel-perspective`: viewing distance for parts that turn in 3D (covers, flaps) - `--bevel-dim`: Unchosen items in a group sit back in shadow. ### Shape - `--bevel-radius-xs` - `--bevel-radius-sm` - `--bevel-radius-md` - `--bevel-radius-lg` - `--bevel-radius-xl` - `--bevel-stitch-width` - `--bevel-stitch-inset` - `--bevel-amount`: Amounts: the figure, its secondary parts (currency, cents), labels, fine print. - `--bevel-amount-minor` - `--bevel-label` - `--bevel-fine` - `--bevel-tracking-label`: stamped labels - `--bevel-tracking-print`: printed marks on cards ### Paper and ink - `--bevel-paper` - `--bevel-paper-foreground` - `--bevel-paper-muted` - `--bevel-paper-rule` - `--bevel-paper-margin` - `--bevel-fibre` - `--bevel-ink-texture`: Ink soaks unevenly: mostly solid with pale specks. Used as a mask. - `--bevel-ink-red` - `--bevel-ink-blue` - `--bevel-ink-green` - `--bevel-ink-black` - `--bevel-ink-amber` ### Brass and ivory - `--bevel-brass` - `--bevel-brass-edge` - `--bevel-ivory` - `--bevel-ivory-foreground` - `--bevel-needle` ### Desk stationery - `--bevel-pad`: yellow legal pad - `--bevel-pad-foreground` - `--bevel-pad-rule` - `--bevel-pad-margin` - `--bevel-pen-red`: correcting pen - `--bevel-pen-blue`: writing pen - `--bevel-pencil`: not written in ink yet - `--bevel-sticker`: exhibit sticker - `--bevel-sticker-foreground` - `--bevel-font-marker`: Handwriting. Bring a marker font (e.g. Permanent Marker) for the full effect; these are fallbacks. ## Categories - Essentials: Everyday controls (buttons, fields, switches, overlays) made of Bevel's materials. The real Base UI control, with a body. - Money: Balances, payment methods and credits for checkout, billing and usage-based products. - Instruments: Readouts and controls from a panel: rolling counters, gauges, dials and breakers. - Coordination: Guards and holds for consequential work shared by people and agents: a cover over a critical switch, a check-in that keeps a run alive, a lockout everyone must release, and a take-a-number queue. - Records: Seeing many things at once: notched cards that drop out when filtered, signals along a file's edge, a contact sheet, a departures board, a due-date slip and a time card. - Agents: Plans, logs, delegation and approvals for AI agents at work. Who's doing what, how far along, and what needs you. - Documents: Sources and edits: citations that open their passage, and redlines you accept or reject. - Marks: Ink and relief on paper: status stamps, embossed seals and redactions. ## Blocks - Relay (`npx shadcn@latest add @bevel/agent-console`): AI agent control room. Supervise a coding agent and its subagents: set its autonomy, balance effort between subagents, hold the dead man's switch, and lift the cover to ship. Uses engine-telegraph, mixer-strip, vigilance, annunciator, guarded-switch, push-to-talk, task-plan. - Meterline (`npx shadcn@latest add @bevel/billing-desk`): Usage billing. A usage-based account page: the balance in a wallet, spend running on a meter, caps on breakers, the ledger, and API keys that expire on a library slip. Uses wallet, meter, breaker, ledger, due-date-slip, receipt. - Shipyard (`npx shadcn@latest add @bevel/release-hall`): Release coordination. A release board for a team: regions on a split-flap board, services moving through stages, a deploy queue, a freeze everyone must lift, and the promote switch. Uses split-flap, visible-file, take-a-number, lockout, guarded-switch, annunciator. - Casefile (`npx shadcn@latest add @bevel/review-desk`): Legal review desk. A lawyer's desk with an AI assistant: pull contracts with needles, mark up a clause, check the answer's sources, redact, and seal before it leaves. Uses keysort-filter, redline, citation, redaction, approval, stamp. ## Components ## Key A button made as a keycap: a cap with a lit top edge on a visible side, which travels down by the side's depth when pressed by pointer, Space or Enter. While its action runs it latches down with a small lamp lit, instead of a spinner, and pops back up when done. - Category: Essentials - Has parts that work alone: Icon key - Docs: https://bevel-ui.vercel.app/components/key - Import from: `@/components/ui/key` ### Install ```bash npx shadcn@latest add @bevel/key ``` npm dependencies (installed by the CLI): `@base-ui/react`, `cn`. ### Anatomy ```tsx Save changes ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `Key` | Button | variant primary (a lit amber key), secondary, ghost (no body until pressed) or danger; size sm, md or lg (32, 36 and 40px tall, side included, like shadcn's buttons). pending latches the key down, sets aria-busy and keeps focus; disabled sinks it into its housing. Render a link with render={} and nativeButton={false}. Icons from lucide size themselves. | | `KeyLamp` | span (decorative) | A small lamp on the cap: dark at rest, lit while the key is pending. Put it first or last. | ### Example: Saving a form Cancel and Save at the foot of a settings form. Save latches down with its lamp lit while the request runs, then pops back up. ```tsx "use client" import * as React from "react" import { Key, KeyLamp } from "@/components/ui/key" /** A settings form. Save latches down with its lamp lit while the request runs, then pops back up. */ export function KeyForm() { const [name, setName] = React.useState("Acme Robotics") const [saved, setSaved] = React.useState("Acme Robotics") const [saving, setSaving] = React.useState(false) const dirty = name.trim() !== saved async function save(event: React.FormEvent) { event.preventDefault() setSaving(true) await new Promise((resolve) => setTimeout(resolve, 1400)) setSaved(name.trim()) setSaving(false) } return (
setName(event.target.value)} disabled={saving} className="h-9 rounded-md border bg-background px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring" />

Shown on invoices and in invitation emails.

{saving ? "Saving…" : dirty ? "Unsaved changes" : "All changes saved"}

setName(saved)}> Cancel Save
) } ``` ### Example: Variants and sizes Primary, secondary, ghost and danger at sm, md and lg, with icons, and disabled. ```tsx import { ArrowRight, Download, Trash2 } from "lucide-react" import { Key } from "@/components/ui/key" /** Every variant at every size. Heights match shadcn's buttons, so keys drop into existing layouts. */ export function KeyVariants() { return (
{(["sm", "md", "lg"] as const).map((size) => (
Continue Export Skip Delete
))}
Disabled Disabled
) } ``` ## Icon key A square keycap for a single icon: copy, close, toolbar actions. It must be given an aria-label, and is secondary by default so a row of them stays quiet. - Category: Essentials - Part of: Key (https://bevel-ui.vercel.app/components/key.md) - Built from: Key - Docs: https://bevel-ui.vercel.app/components/key/icon-key - Import from: `@/components/ui/icon-key` ### Install ```bash npx shadcn@latest add @bevel/icon-key ``` npm dependencies (installed by the CLI): . ### Anatomy ```tsx ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `IconKey` | Key | Every Key prop except square. aria-label is required. Sizes sm, md and lg are 32, 36 and 40px square. | ### Example: Toolbar and copy button Ghost keys in a formatting toolbar, and a copy key that latches while it writes to the clipboard. ```tsx "use client" import * as React from "react" import { Bold, Check, Copy, Italic, Link, Underline } from "lucide-react" import { IconKey } from "@/components/ui/icon-key" const command = "npx shadcn@latest add @bevel/key" /** Ghost keys in a toolbar, and a copy key that latches while it writes to the clipboard. */ export function IconKeyToolbar() { const [marks, setMarks] = React.useState(["bold"]) const [copying, setCopying] = React.useState(false) const [copied, setCopied] = React.useState(false) const toggle = (mark: string) => setMarks((all) => (all.includes(mark) ? all.filter((m) => m !== mark) : [...all, mark])) async function copy() { setCopying(true) try { await navigator.clipboard.writeText(command) setCopied(true) setTimeout(() => setCopied(false), 1600) } catch { // Clipboard can be blocked; the command is still on screen to select. } finally { setCopying(false) } } return (
{[ { mark: "bold", label: "Bold", icon: }, { mark: "italic", label: "Italic", icon: }, { mark: "underline", label: "Underline", icon: }, { mark: "link", label: "Link", icon: }, ].map(({ mark, label, icon }) => ( toggle(mark)} > {icon} ))}
{command} {copied ? : }
) } ``` ## Kbd A keycap printed in a line of text, for shortcuts and docs. Symbols such as ⌘ are read aloud by name. With listen, the cap goes down when the reader presses that key on their own keyboard, so a shortcut list shows its keys working. - Category: Essentials - Docs: https://bevel-ui.vercel.app/components/kbd - Import from: `@/components/ui/kbd` ### Install ```bash npx shadcn@latest add @bevel/kbd ``` npm dependencies (installed by the CLI): `cn`. ### Anatomy ```tsx ⌘ K ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `Kbd` | kbd | Its text is the legend. listen: true listens for the printed key (⌘ is Meta, ⇧ is Shift, Esc is Escape); a string names it, such as "k", "Escape" or "mod" (⌘ on Apple, Ctrl elsewhere). Keys typed into a field don't press caps. | | `KbdGroup` | kbd | Caps pressed together. Each cap listens for its own key, so holding ⌘ presses only ⌘. | ### Example: Keyboard shortcuts A shortcuts sheet whose caps go down as you press them. Try ⌘K or Esc. ```tsx import { Kbd, KbdGroup } from "@/components/ui/kbd" const shortcuts = [ { action: "Open the command menu", keys: ["⌘", "K"] }, { action: "New conversation", keys: ["⌘", "⇧", "O"] }, { action: "Stop the agent", keys: ["Esc"] }, { action: "Send message", keys: ["↵"] }, { action: "Previous message", keys: ["↑"] }, ] /** A shortcuts sheet. Each cap goes down when you press that key on your own keyboard. */ export function KbdShortcuts() { return (

Keyboard shortcuts

Try them: the caps press with your keys.

{shortcuts.map(({ action, keys }) => (
{action}
{keys.map((key) => ( {key} ))}
))}
) } ``` ## Badge A small printed plate with an indicator lamp, for status in tables, lists and headings. Five tones; a lamp is lit while its condition holds and dark when it doesn't. Lamps burn steady and never blink. - Category: Essentials - Docs: https://bevel-ui.vercel.app/components/badge - Import from: `@/components/ui/badge` ### Install ```bash npx shadcn@latest add @bevel/badge ``` npm dependencies (installed by the CLI): `@base-ui/react`, `cn`. ### Anatomy ```tsx Deployed ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `Badge` | span | tone neutral, info, success, warning or danger; lit (default true). The label is the status; colour only backs it up. Render a link with render. | | `BadgeLamp` | span (decorative) | The indicator lamp, in the tone's colour. Leave it out for a plain plate. | ### Example: Deployments A deployments table: the lamp is lit while a build runs or a release is live, and dark once it's superseded. ```tsx import { Badge, BadgeLamp, type BadgeProps } from "@/components/ui/badge" type Deploy = { commit: string; message: string; age: string; status: string; tone: BadgeProps["tone"]; lit: boolean } const deploys: Deploy[] = [ { commit: "9c1e4a2", message: "Stream tool calls in the agent log", age: "now", status: "Building", tone: "warning", lit: true }, { commit: "4f2a7d0", message: "Fix rounding on prepaid balances", age: "2h", status: "Live", tone: "success", lit: true }, { commit: "b81c3e9", message: "Retry webhook deliveries with backoff", age: "5h", status: "Failed", tone: "danger", lit: true }, { commit: "77de015", message: "Bump base-ui to 1.9", age: "1d", status: "Superseded", tone: "success", lit: false }, { commit: "e0a9b44", message: "Preview: new pricing page", age: "2d", status: "Preview", tone: "info", lit: true }, ] /** A deployments table. Lamps are lit while a build runs or a release is live, and dark once superseded. */ export function BadgeDeploys() { return (
{deploys.map((deploy) => ( ))}
Recent deployments
Commit Message Status Age
{deploy.commit} {deploy.message} {deploy.status} {deploy.age}
) } ``` ## Text field A text input cut into the panel as a slot, with a printed label above and description and error lines below. Focus lights the slot's edge amber, the colour of a caret; an invalid field edges in red and lights a lamp by its label. Inputs, textareas, search boxes with an icon and a key hint, amounts with a unit. - Category: Essentials - Docs: https://bevel-ui.vercel.app/components/text-field - Import from: `@/components/ui/text-field` ### Install ```bash npx shadcn@latest add @bevel/text-field ``` npm dependencies (installed by the CLI): `@base-ui/react`, `cn`. ### Anatomy ```tsx ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `TextField` | Field.Root | size sm or md. Everything Base UI Field takes: name, disabled, invalid, validate, validationMode. Wires the label, description and error to the control. | | `TextFieldLabel` | Field.Label | The printed label. Its lamp lights red while the field is invalid. | | `TextFieldSlot` | div | The slot, when there are adornments. Pressing anywhere in it focuses the control. Optional: a bare TextFieldInput is its own slot. | | `TextFieldInput` | Input | The input. value, defaultValue, onValueChange, and every input attribute. | | `TextFieldTextarea` | Field.Control (textarea) | Several lines, resizable vertically. | | `TextFieldAdornment` | span | An icon, unit or prefix printed in the slot. | | `TextFieldKbd` | kbd | A small keycap: the shortcut that focuses the field. | | `TextFieldDescription` | Field.Description | What goes in it. Describes the control. | | `TextFieldError` | Field.Error (alert) | Shown while invalid and announced as it appears. match a validity state (valueMissing, typeMismatch…) or leave it off to show validate's message or the form's. | ### Example: Workspace settings Fields validate as you leave them and again on save; the server's answer comes back on the field it's about. Save as is: the URL is taken. ```tsx "use client" import * as React from "react" import { Form } from "@base-ui/react/form" import { TextField, TextFieldAdornment, TextFieldDescription, TextFieldError, TextFieldInput, TextFieldLabel, TextFieldSlot, } from "@/components/ui/text-field" import { ToggleLever, ToggleLeverDescription, ToggleLeverField, ToggleLeverLabel } from "@/components/ui/toggle-lever" import { LatchCheckbox, LatchCheckboxField, LatchCheckboxLabel } from "@/components/ui/latch-checkbox" /** * Workspace settings. Fields validate when you leave them and again on save; * the server's answer comes back as an error on the field it's about. */ export function TextFieldSettings() { const [errors, setErrors] = React.useState>({}) const [saved, setSaved] = React.useState(null) return (
{ setSaved(null) // Pretend the server owns the slug namespace. if (values.slug === "acme") { setErrors({ slug: "acme is taken. Try acme-labs." }) return } setErrors({}) setSaved(`Saved ${values.name}.`) }} className="flex w-full flex-col gap-5 rounded-xl border bg-card p-5" > Workspace name Give the workspace a name. !value ? "Choose a URL." : /^[a-z0-9-]+$/.test(String(value)) ? null : "Only lowercase letters, numbers and dashes." } > URL bevel.app/ Lowercase letters, numbers and dashes. Try saving as is. {/* Shows the validate message, or the server's. */} Billing email Invoices need somewhere to go. That isn't a whole email address.
Weekly digest Runs, spend and failures, every Monday. Email me when spend passes the monthly cap

{saved}

) } ``` ### Example: Search box An icon and a key hint in the slot. ⌘K or / focuses it from anywhere on the page. ```tsx "use client" import * as React from "react" import { Search } from "lucide-react" import { TextField, TextFieldAdornment, TextFieldInput, TextFieldKbd, TextFieldLabel, TextFieldSlot, } from "@/components/ui/text-field" const items = ["Text field", "Toggle lever", "Latch checkbox", "Preset keys", "Wallet", "Meter", "DIP switches"] /** A site search box: ⌘K or / focuses it from anywhere on the page. */ export function TextFieldSearch() { const [query, setQuery] = React.useState("") const ref = React.useRef(null) React.useEffect(() => { const onKeyDown = (event: KeyboardEvent) => { const typing = (event.target as HTMLElement).closest("input, textarea, [contenteditable]") if ((event.key === "k" && (event.metaKey || event.ctrlKey)) || (event.key === "/" && !typing)) { event.preventDefault() ref.current?.focus() } } window.addEventListener("keydown", onKeyDown) return () => window.removeEventListener("keydown", onKeyDown) }, []) const matches = items.filter((item) => item.toLowerCase().includes(query.trim().toLowerCase())) return (
Search components ⌘K
    {matches.length ? ( matches.map((item) => (
  • {item}
  • )) ) : (
  • Nothing matches “{query}”.
  • )}
) } ``` ## Toggle lever An everyday on/off switch, as a bat-handle toggle on a small plate. The lever swings through its bushing toward the side that's on and settles with a little spring; a lamp at the end of the plate lights. For settings rows. (For banks of config bits use DIP switches; for dangerous actions, the guarded switch.) - Category: Essentials - Docs: https://bevel-ui.vercel.app/components/toggle-lever - Import from: `@/components/ui/toggle-lever` ### Install ```bash npx shadcn@latest add @bevel/toggle-lever ``` npm dependencies (installed by the CLI): `@base-ui/react`, `cn`. ### Anatomy ```tsx ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `ToggleLever` | Switch | checked, defaultChecked, onCheckedChange, disabled, name. size md prints OFF and ON on the plate; sm is lever and lamp only. Space flips it. Alone, give it an aria-label. | | `ToggleLeverField` | Field.Root | A settings row: label and description on the left, the lever on the right. disabled locks the row. | | `ToggleLeverLabel` | Field.Label | Names the switch. Clicking it throws the lever. | | `ToggleLeverDescription` | Field.Description | Describes the switch. | ### Example: Notification settings A lever per row, and a master lever that pauses them all. ```tsx "use client" import * as React from "react" import { ToggleLever, ToggleLeverDescription, ToggleLeverField, ToggleLeverLabel } from "@/components/ui/toggle-lever" const settings = [ { id: "failures", label: "Run failures", description: "The moment an agent run fails.", on: true }, { id: "approvals", label: "Approvals waiting", description: "When an agent stops to ask you.", on: true }, { id: "digest", label: "Weekly digest", description: "Runs, spend and failures, every Monday.", on: false }, { id: "product", label: "Product news", description: "New models and features, about monthly.", on: false }, ] /** Notification settings: one lever per row, and a master lever that silences them all. */ export function ToggleLeverNotifications() { const [paused, setPaused] = React.useState(false) return (
Email notifications {paused ? "Paused. Nothing will be sent." : "Sent to you@acme.com."} setPaused(!on)} />
{settings.map((s) => ( {s.label} {s.description} ))}
) } ``` ## Latch checkbox A checkbox as a latching push-key. Press it and it sinks and stays down with its mark lit amber; press again and it springs back up. Mixed holds it half-down with a dash. Small enough for lists and table rows. - Category: Essentials - Docs: https://bevel-ui.vercel.app/components/latch-checkbox - Import from: `@/components/ui/latch-checkbox` ### Install ```bash npx shadcn@latest add @bevel/latch-checkbox ``` npm dependencies (installed by the CLI): `@base-ui/react`, `cn`. ### Anatomy ```tsx ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `LatchCheckbox` | Checkbox | checked, defaultChecked, onCheckedChange, indeterminate, disabled, name. In a group: value, or parent to toggle them all. Space presses it. Alone, give it an aria-label. | | `LatchCheckboxField` | Field.Root | The key with its label beside it and an optional description below. | | `LatchCheckboxLabel` | Field.Label | Names the checkbox. Clicking it presses the key. | | `LatchCheckboxDescription` | Field.Description | Describes the checkbox. | | `LatchCheckboxGroup` | CheckboxGroup | value (the values that are down), defaultValue, onValueChange, allValues for a parent key. Name it with aria-labelledby. | ### Example: Agent permissions Which tools an agent may use. The top key presses them all, and sits half-down while only some are on. ```tsx "use client" import * as React from "react" import { LatchCheckbox, LatchCheckboxDescription, LatchCheckboxField, LatchCheckboxGroup, LatchCheckboxLabel, } from "@/components/ui/latch-checkbox" const tools = [ { value: "read", label: "Read files", description: "Anything in the repository." }, { value: "write", label: "Edit files", description: "Changes land on a branch, never main." }, { value: "shell", label: "Run commands", description: "Tests, builds and linters, in a sandbox." }, { value: "web", label: "Browse the web", description: "Docs and package registries." }, ] /** What an agent may do. The top key presses or releases them all, and sits half-down when some are on. */ export function LatchCheckboxPermissions() { const [allowed, setAllowed] = React.useState(["read", "shell"]) return (

Tools for the review agent

{allowed.length} of {tools.length} allowed

t.value)} className="gap-3" > All tools {tools.map((tool) => ( {tool.label} {tool.description} ))}
) } ``` ## Preset keys Tabs and segmented controls as interlocked preset keys, like a car radio's: press one and it latches down while the one that was down springs back up. Only one is ever down. PresetTabs switches panels; PresetKeys picks a value. - Category: Essentials - Docs: https://bevel-ui.vercel.app/components/preset-keys - Import from: `@/components/ui/preset-keys` ### Install ```bash npx shadcn@latest add @bevel/preset-keys ``` npm dependencies (installed by the CLI): `@base-ui/react`, `cn`. ### Anatomy ```tsx ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `PresetTabs` | Tabs.Root | value, defaultValue, onValueChange, orientation. Arrow keys move along the keys; Enter or Space presses one. | | `PresetTabsList` | Tabs.List | The keybed. size sm for a Preview/Code row or a toolbar, md for a screen's sections. activateOnFocus to press as you arrow. | | `PresetTabsTab` | Tabs.Tab | One key, with a lamp lit while it's down. value, disabled. | | `PresetTabsPanel` | Tabs.Panel | The panel a key shows. value, keepMounted. | | `PresetKeys` | RadioGroup | A segmented control: value, defaultValue, onValueChange, name, disabled, size. Exactly one key is down; arrow keys move and press. Give it an aria-label. | | `PresetKey` | Radio | One key. value, disabled. | ### Example: Preview and code The tabs over a docs example, at the small size. ```tsx "use client" import { PresetTabs, PresetTabsList, PresetTabsPanel, PresetTabsTab } from "@/components/ui/preset-keys" const code = ` Preview Code … … ` /** Preview and Code for a docs example: the small keys, over the panels they switch. */ export function PresetKeysExample() { return ( Preview Code The component, live.
{code}
) } ``` ### Example: Time range A segmented range picker over a usage chart. ```tsx "use client" import * as React from "react" import { PresetKey, PresetKeys } from "@/components/ui/preset-keys" const ranges = { "24h": { label: "Last 24 hours", runs: [4, 7, 3, 9, 12, 8, 6, 10, 14, 9, 5, 7] }, "7d": { label: "Last 7 days", runs: [52, 61, 48, 70, 66, 31, 28] }, "30d": { label: "Last 30 days", runs: [210, 260, 240, 300, 280, 330, 310, 350, 290, 320] }, "90d": { label: "Last 90 days", runs: [800, 950, 1100, 1020, 1240, 1180, 1300, 1420, 1390, 1500, 1610, 1580] }, } as const type Range = keyof typeof ranges /** A time-range picker over a usage chart: one range is always down. */ export function PresetKeysRange() { const [range, setRange] = React.useState("7d") const { label, runs } = ranges[range] const max = Math.max(...runs) const total = runs.reduce((a, b) => a + b, 0) return (

Agent runs

{label}: {total.toLocaleString("en-US")}

aria-label="Range" size="sm" value={range} onValueChange={setRange}> {(Object.keys(ranges) as Range[]).map((r) => ( key={r} value={r}> {r} ))}
{runs.map((n, i) => ( ))}
) } ``` ## Tooltip A label-maker strip: dark embossing tape with raised pale letters and a tab pointing at the trigger. It feeds out a short way from the trigger's side; in a group, the next one prints at once. - Category: Essentials - Docs: https://bevel-ui.vercel.app/components/tooltip - Import from: `@/components/ui/tooltip` ### Install ```bash npx shadcn@latest add @bevel/tooltip ``` npm dependencies (installed by the CLI): `@base-ui/react`, `cn`. ### Anatomy ```tsx Copy link ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `TooltipProvider` | Tooltip provider | Shares the open delay (delay, default 400ms; closeDelay). Once one tooltip shows, the next opens instantly and without animation. | | `Tooltip` | Tooltip root | One trigger and its label. open, defaultOpen, onOpenChange, disabled. | | `TooltipTrigger` | button | Opens on hover and keyboard focus. Give an icon-only trigger an aria-label; pass render to use your own button. | | `TooltipContent` | Tooltip popup | The tape, with its tab. tone: black (default), red, blue. side, align, sideOffset, alignOffset. Text prints in capitals; keep it to a few words. | ### Example: Icon buttons An editor toolbar. Hover one button and slide along: each label prints at once. ```tsx "use client" import { Bold, Code, Italic, Link, Strikethrough } from "lucide-react" import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" const tools = [ { label: "Bold", keys: "⌘B", shortcut: "Meta+B", icon: Bold }, { label: "Italic", keys: "⌘I", shortcut: "Meta+I", icon: Italic }, { label: "Strikethrough", keys: "⌘⇧X", shortcut: "Meta+Shift+X", icon: Strikethrough }, { label: "Inline code", keys: "⌘E", shortcut: "Meta+E", icon: Code }, { label: "Add link", keys: "⌘K", shortcut: "Meta+K", icon: Link }, ] /** An editor toolbar. Hover one button, then slide along: the next label prints at once. */ export function TooltipToolbar() { return (
{tools.map(({ label, keys, shortcut, icon: Icon }) => ( {label} {keys} ))}
) } ``` ### Example: Sides and tapes Each label feeds out from its own side. Red and blue tape are for the label that must stand out. ```tsx "use client" import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" const key = "inline-flex h-8 items-center justify-center rounded-(--bevel-radius-sm) border bg-background px-3 text-sm outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring" /** Black tape for everyday labels; red and blue for the one that must stand out. Each feeds out from its own side. */ export function TooltipTapes() { return (
Top Saved 2 min ago Bottom Shared with 4 people Red tape Deletes for everyone Blue tape New
) } ``` ## Popover A sheet of panel stock lifted off the page on the shared light, with a folded tab at its trigger. It lifts toward you and settles; Escape or a click outside puts it down and returns focus. - Category: Essentials - Docs: https://bevel-ui.vercel.app/components/popover - Import from: `@/components/ui/popover` ### Install ```bash npx shadcn@latest add @bevel/popover ``` npm dependencies (installed by the CLI): `@base-ui/react`, `cn`. ### Anatomy ```tsx ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `Popover` | Popover root | open, defaultOpen, onOpenChange, modal. | | `PopoverTrigger` | button | Opens the sheet. Pass render to use your own button. | | `PopoverContent` | Popover popup | The sheet, 20rem wide (--popover-width), padded and stacked. side (default bottom), align, sideOffset, alignOffset, collisionPadding. tab={false} leaves out the folded tab. | | `PopoverHeader` | div | Title and description; leaves room for a corner close. | | `PopoverTitle` | h2 | Names the sheet for screen readers. | | `PopoverDescription` | p | Muted text under the title. | | `PopoverClose` | button | With no children, a × in the top corner labelled “Close”. With children, an unstyled close button. | ### Example: Share a page A link to copy and who can see it. ```tsx "use client" import * as React from "react" import { Check, Copy, Share } from "lucide-react" import { Popover, PopoverClose, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger, } from "@/components/ui/popover" /** Share a page: a link to copy, and who can see it. */ export function PopoverShare({ url = "https://bevel-ui.vercel.app/components/popover" }: { url?: string }) { const [copied, setCopied] = React.useState(false) const timer = React.useRef(undefined) const inputId = React.useId() React.useEffect(() => () => window.clearTimeout(timer.current), []) async function copy() { await navigator.clipboard?.writeText(url).catch(() => {}) setCopied(true) window.clearTimeout(timer.current) timer.current = window.setTimeout(() => setCopied(false), 2000) } return ( Share Share this page Anyone with the link can view it.
event.currentTarget.select()} className="h-8 min-w-0 flex-1 rounded-(--bevel-radius-sm) border border-current/15 bg-transparent px-2 font-mono text-xs text-ellipsis shadow-(--bevel-depth-pressed) outline-none focus-visible:ring-2 focus-visible:ring-(--bevel-pen-blue)/50" />

{copied ? "Link copied" : ""}

) } ``` ## Menu A selector plate: items engraved into brushed metal, the highlighted one pressed in, a lamp for checked settings, engraved rules between groups and keycaps for shortcuts. Dropdown, submenu and context menu. - Category: Essentials - Docs: https://bevel-ui.vercel.app/components/menu - Import from: `@/components/ui/menu` ### Install ```bash npx shadcn@latest add @bevel/menu ``` npm dependencies (installed by the CLI): `@base-ui/react`, `cn`. ### Anatomy ```tsx Duplicate ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `Menu` | Menu root | open, defaultOpen, onOpenChange, modal. | | `MenuTrigger` | button | Opens the plate. Pass render to use your own button. | | `MenuContent` | Menu popup | The plate. side (default bottom), align (default start), sideOffset, alignOffset, collisionPadding. Scrolls when tall. | | `MenuItem` | div (menuitem) | Pressed in while highlighted. onClick runs it. variant="destructive" engraves it in red. Icons are muted and sized for you. | | `MenuLinkItem` | a (menuitem) | An item that is a real link: href, target. | | `MenuCheckboxItem` | div (menuitemcheckbox) | A pill lamp, lit when checked. checked, onCheckedChange. Stays open when toggled. | | `MenuRadioGroup` | div (group) | value, onValueChange. | | `MenuRadioItem` | div (menuitemradio) | A round lamp, lit when chosen. value. | | `MenuGroup` | div (group) | Groups items under a label. | | `MenuGroupLabel` | div | A small stamped heading. | | `MenuSeparator` | div (separator) | An engraved rule. | | `MenuShortcut` | span | keys: one keycap per key. Hidden from screen readers; put aria-keyshortcuts on the item. | | `MenuSub` | Submenu root | Holds a submenu. | | `MenuSubTrigger` | div (menuitem) | Opens the submenu on hover, → or Enter. Has a chevron. | | `MenuSubContent` | Menu popup | The submenu's plate, beside its trigger. | | `ContextMenu` | Context menu root | Opens on right-click or long-press. | | `ContextMenuTrigger` | div | The area that opens it. | | `ContextMenuContent` | Menu popup | The plate at the pointer. Fill it with the same items. | ### Example: Copy page The menu on a docs page with a Markdown version: copy it, view it, or open it in ChatGPT or Claude. Pass markdownUrl. ```tsx "use client" import * as React from "react" import { Check, ChevronDown, Copy, ExternalLink, FileText } from "lucide-react" import { Menu, MenuContent, MenuItem, MenuLinkItem, MenuSeparator, MenuTrigger, } from "@/components/ui/menu" import { IconKey } from "@/components/ui/icon-key" import { Key } from "@/components/ui/key" type CopyPageMenuProps = { /** The page as Markdown, e.g. "/components/menu.md". Relative URLs resolve against the current page. */ markdownUrl?: string } const prompt = (url: string) => `Read ${url} so I can ask questions about it.` /** * Copies the page as Markdown into the clipboard. Safari only lets a page * write to the clipboard during the click, so the text goes in as a promise * where ClipboardItem exists; elsewhere it's fetched first. */ async function copyMarkdown(url: string) { const text = () => fetch(url).then((response) => { if (!response.ok) throw new Error(`${response.status} fetching ${url}`) return response.text() }) if (typeof ClipboardItem !== "undefined" && navigator.clipboard?.write) { const blob = text().then((t) => new Blob([t], { type: "text/plain" })) await navigator.clipboard.write([new ClipboardItem({ "text/plain": blob })]) } else { await navigator.clipboard.writeText(await text()) } } /** The "Copy page" menu for a docs page that has a Markdown version. */ export function MenuCopyPage({ markdownUrl = "/components/menu.md", }: CopyPageMenuProps) { const [state, setState] = React.useState<"idle" | "copied" | "failed">("idle") // Assistants need an absolute address; resolve it once mounted. const [absolute, setAbsolute] = React.useState(markdownUrl) const timer = React.useRef(undefined) React.useEffect( () => setAbsolute(new URL(markdownUrl, window.location.href).href), [markdownUrl] ) React.useEffect(() => () => window.clearTimeout(timer.current), []) function copy() { copyMarkdown(markdownUrl).then( () => setState("copied"), () => setState("failed") ) window.clearTimeout(timer.current) timer.current = window.setTimeout(() => setState("idle"), 2000) } return ( // A split key: the label does what it says, the chevron holds the other ways to read the page.
{state === "copied" ? : } {state === "copied" ? "Copied" : "Copy page"} } > Copy as Markdown View as Markdown Open in ChatGPT Open in Claude {state === "copied" ? "Page copied as Markdown" : state === "failed" ? "Couldn't copy the page" : ""}
) } ``` ### Example: File actions Shortcuts as keycaps, a submenu, a setting with a lamp, a sort order and a destructive item. ```tsx "use client" import * as React from "react" import { Copy, FolderInput, MoreHorizontal, Pencil, Trash2 } from "lucide-react" import { Menu, MenuCheckboxItem, MenuContent, MenuGroup, MenuGroupLabel, MenuItem, MenuRadioGroup, MenuRadioItem, MenuSeparator, MenuShortcut, MenuSub, MenuSubContent, MenuSubTrigger, MenuTrigger, } from "@/components/ui/menu" /** A file's ⋯ menu: actions with shortcuts, a submenu, a setting with a lamp, a sort order, and delete. */ export function MenuFileActions() { const [preview, setPreview] = React.useState(true) const [sort, setSort] = React.useState("modified") const [last, setLast] = React.useState(null) return (
quarterly-report.pdf setLast("Rename")} aria-keyshortcuts="F2"> Rename setLast("Duplicate")} aria-keyshortcuts="Meta+D"> Duplicate Move to {["Drafts", "Finance", "Archive"].map((folder) => ( setLast(`Moved to ${folder}`)}> {folder} ))} Show preview Sort by Name Date modified Size setLast("Deleted")} aria-keyshortcuts="Meta+Backspace"> Delete

{last ?? `Preview ${preview ? "on" : "off"}, sorted by ${sort}.`}

) } ``` ## Dialog The lights go down and a panel rises onto the desk, screwed or stitched at its corners. Content has no padding of its own, so it hosts forms and command palettes alike; AlertDialog asks before something can't be undone. - Category: Essentials - Docs: https://bevel-ui.vercel.app/components/dialog - Import from: `@/components/ui/dialog` ### Install ```bash npx shadcn@latest add @bevel/dialog ``` npm dependencies (installed by the CLI): `@base-ui/react`, `cn`. ### Anatomy ```tsx ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `Dialog` | Dialog root | open, defaultOpen, onOpenChange, modal. Escape, the backdrop and Close put it away; focus returns to the trigger. | | `DialogTrigger` | button | Opens the dialog. Pass render to use your own button. | | `DialogContent` | Dialog popup | Dims the room and raises the panel. No padding. align: center or top (for a palette). fasteners: screws (default), stitch, none. showCloseButton (default true). Width from --dialog-width; override with className. | | `DialogHeader` | div | Title and description, padded, clear of the corner ×. | | `DialogTitle` | h2 | Names the dialog for screen readers. Required; hide it with sr-only if it must not show. | | `DialogDescription` | p | Muted text under the title. | | `DialogBody` | div | Padded; scrolls when the panel runs out of room. | | `DialogFooter` | div | A ledge behind an engraved rule, holding the keys. Stacks on small screens. | | `DialogClose` | button | A raised key that closes. tone: default or destructive. | | `AlertDialog` | Alert dialog root | Doesn't close on an outside click. | | `AlertDialogContent` | Alert dialog popup | The same panel, narrower (--dialog-alert-width), with no corner ×. fasteners. | | `AlertDialogCancel` | button | The safe answer. Put it first: it takes focus. Says “Cancel” by default. | | `AlertDialogAction` | button | Does it and closes; onClick runs first. A red key by default (tone="destructive"). | ### Example: Delete a project An alert dialog with a red key. Cancel takes focus, and a stray click outside does nothing. ```tsx "use client" import * as React from "react" import { Trash2 } from "lucide-react" import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/dialog" /** Deleting a project: an alert dialog that won't close on a stray click. Cancel takes focus first. */ export function DialogDeleteProject({ project = "acme-web" }: { project?: string }) { const [deleted, setDeleted] = React.useState(false) return (
Delete project Delete {project}? Its 14 deployments, environment variables and domains go with it. This can’t be undone. setDeleted(true)}>Delete project

{deleted ? `${project} was deleted.` : `${project} · 14 deployments`}

{deleted && ( )}
) } ``` ### Example: A form Rename a document. Enter saves; Escape, × or Cancel leave it. ```tsx "use client" import * as React from "react" import { Dialog, DialogBody, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog" /** A small form in a dialog. Enter saves; Escape, the × or Cancel leave it as it was. */ export function DialogRename() { const [open, setOpen] = React.useState(false) const [name, setName] = React.useState("Quarterly report") const fieldId = React.useId() function save(event: React.FormEvent) { event.preventDefault() const value = new FormData(event.currentTarget).get("name")?.toString().trim() if (value) setName(value) setOpen(false) } return (
Rename
Rename document Everyone with access sees the new name. Cancel

{name}

) } ``` ### Example: Command palette ⌘K opens a palette in the dialog: top-aligned, no padding, no fasteners. ```tsx "use client" import * as React from "react" import { Autocomplete } from "@base-ui/react/autocomplete" import { Search } from "lucide-react" import { Dialog, DialogContent, DialogTitle, DialogTrigger } from "@/components/ui/dialog" const commands = [ "New document", "Open recent", "Share page", "Copy link", "Toggle dark mode", "Go to settings", "Invite a teammate", "Sign out", ] /** A ⌘K command palette hosted in the dialog: top-aligned, no padding, no corner ×. */ export function DialogCommand() { const [open, setOpen] = React.useState(false) const [ran, setRan] = React.useState(null) React.useEffect(() => { function onKeyDown(event: KeyboardEvent) { if (event.key.toLowerCase() === "k" && (event.metaKey || event.ctrlKey)) { event.preventDefault() setOpen((o) => !o) } } window.addEventListener("keydown", onKeyDown) return () => window.removeEventListener("keydown", onKeyDown) }, []) return (
Run a command… ⌘K Command palette

No commands match.

{(command: string) => ( { setRan(command) setOpen(false) }} className="flex h-9 cursor-default items-center rounded-(--bevel-radius-sm) px-3 text-sm outline-none select-none data-highlighted:bg-current/8" > {command} )}

{ran ? `Ran “${ran}”.` : "Press ⌘K or Ctrl K."}

) } ``` ## Wallet A customer's stored balance, shown as a leather wallet. Saved cards sit in it, and picking one pulls it out of the pocket. - Category: Money - Built from: Payment card, Amount - Has parts that work alone: Payment card - Docs: https://bevel-ui.vercel.app/components/wallet - Import from: `@/components/ui/wallet` ### Install ```bash npx shadcn@latest add @bevel/wallet ``` npm dependencies (installed by the CLI): `@base-ui/react`, `cn`. ### Anatomy ```tsx ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `Wallet` | div | The leather panel. variant picks the finish (saddle, noir, oxblood); status sets the light and meter colour. Takes render to become a button or label. | | `WalletCards` | div or RadioGroup | Holds any number of cards. Pass value, defaultValue or onValueChange to make them a radio group; the chosen card is pulled out. | | `WalletCard` | PaymentCard | A PaymentCard placed in the stack, so it takes the same parts and tone. value identifies it when the cards are selectable. Cards added after mount slide in. | | `WalletPocket` | div | The stitched front pocket that holds everything else. | | `WalletStatus` | span | A status light with text from the wallet's status. Children replace the text. | | `WalletBalance` | p | The amount, stamped into the leather. value, currency (ISO 4217, omit for plain numbers), locale and format. | | `WalletAmount` | span | An inline amount with the same formatting, for detail rows. | | `WalletDetails` | dl | Label and value rows: WalletDetail, WalletDetailLabel (dt), WalletDetailValue (dd). | | `WalletMeter` | Meter | A groove that fills with the remaining amount. value, min and max, plus label and value parts. | | `WalletAction` | Button | A raised leather tab that sinks when pressed. | ### Example: Checkout Pay from stored balance. Pay once to drain it, then pick the card that covers the rest. ```tsx "use client" import * as React from "react" import { Wallet, WalletAction, WalletAmount, WalletBalance, WalletCard, WalletCards, WalletDetail, WalletDetailLabel, WalletDetails, WalletDetailValue, WalletFooter, WalletHeader, WalletLabel, WalletPocket, WalletSeparator, WalletStatus, } from "@/components/ui/wallet" import { PaymentCardBrand, PaymentCardNumber } from "@/components/ui/payment-card" const cards = [ { id: "amex-1005", brand: "Amex", last4: "1005" }, { id: "debit-8210", brand: "Debit", last4: "8210" }, { id: "visa-4242", brand: "Visa", last4: "4242" }, ] const total = 86.4 export function WalletCheckout() { const [balance, setBalance] = React.useState(124.5) const [cardId, setCardId] = React.useState("visa-4242") const card = cards.find((c) => c.id === cardId)! const covers = balance >= total const remainder = Math.abs(balance - total) return ( {cards.map((c) => ( {c.brand} ))} Pay with balance Order total {covers ? "Left after payment" : `Rest on ${card.brand} ${card.last4}`} {covers ? ( setBalance((b) => +(b - total).toFixed(2))}> Pay $86.40 ) : ( setBalance(124.5)}> Pay ${remainder.toFixed(2)} with •••• {card.last4} )} ) } ``` ### Example: Billing Account credit and saved cards. Add a card and it slides into the wallet. ```tsx "use client" import * as React from "react" import { History, Plus } from "lucide-react" import { Wallet, WalletAction, WalletAmount, WalletBalance, WalletCard, WalletCards, WalletDetail, WalletDetailLabel, WalletDetails, WalletDetailValue, WalletFooter, WalletHeader, WalletLabel, WalletPocket, WalletStatus, } from "@/components/ui/wallet" import { PaymentCardBrand, PaymentCardNumber } from "@/components/ui/payment-card" type SavedCard = { id: string; brand: string; last4: string } export function WalletBilling() { const [cards, setCards] = React.useState([ { id: "amex-1005", brand: "Amex", last4: "1005" }, { id: "debit-8210", brand: "Debit", last4: "8210" }, ]) function addCard() { const last4 = String(1000 + Math.floor(Math.random() * 9000)) setCards((c) => [...c, { id: `visa-${last4}`, brand: "Visa", last4 }]) } return ( {cards.map((c) => ( {c.brand} ))} Account credit Next invoice, Nov 1 Auto top-up below = 5}> Add card History ) } ``` ### Example: Usage Prepaid credits running down over a billing cycle. Low balance turns the light and meter amber. ```tsx "use client" import { ArrowUpRight } from "lucide-react" import { Wallet, WalletAction, WalletBalance, WalletCard, WalletCards, WalletFooter, WalletHeader, WalletLabel, WalletMeter, WalletMeterLabel, WalletMeterValue, WalletPocket, WalletStatus, } from "@/components/ui/wallet" const allowance = 50000 const remaining = 9480 export function WalletUsage() { return ( Credits remaining Of 50,000 this cycle {(_, value) => `${Math.round((value / allowance) * 100)}%`} Buy credits ) } ``` ## Payment card A plastic payment card at real proportions. Use it on its own, as a radio in a picker, or inside a Wallet. - Category: Money - Part of: Wallet (https://bevel-ui.vercel.app/components/wallet.md) - Docs: https://bevel-ui.vercel.app/components/wallet/payment-card - Import from: `@/components/ui/payment-card` ### Install ```bash npx shadcn@latest add @bevel/payment-card ``` npm dependencies (installed by the CLI): `@base-ui/react`, `cn`. ### Anatomy ```tsx ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `PaymentCard` | div | The card, at ID-1 proportions (85.60 × 53.98 mm). tone picks sapphire, gold or graphite. Pass render={} to make it selectable. | | `PaymentCardBrand` | span | The card's mark. Text by default; put your own SVG logo inside. | | `PaymentCardNumber` | span | last4 shown as •••• 4242 and read as “ending in 4242”. | | `PaymentCardChip` | span | The metal contact chip. Decorative and hidden from screen readers. | ### Example: Saved payment methods Cards as a radio group, without a wallet. The chosen card lifts; arrow keys move between them. ```tsx "use client" import { Radio } from "@base-ui/react/radio" import { RadioGroup } from "@base-ui/react/radio-group" import { PaymentCard, PaymentCardBrand, PaymentCardChip, PaymentCardNumber, type PaymentCardTone, } from "@/components/ui/payment-card" const cards: { id: string; brand: string; last4: string; tone: PaymentCardTone }[] = [ { id: "visa-4242", brand: "Visa", last4: "4242", tone: "graphite" }, { id: "amex-1005", brand: "Amex", last4: "1005", tone: "sapphire" }, { id: "debit-8210", brand: "Debit", last4: "8210", tone: "gold" }, { id: "corp-7731", brand: "Corporate", last4: "7731", tone: "graphite" }, ] export function PaymentCardPicker() { return ( {cards.map((card) => ( } className="cursor-pointer transition-[translate,filter] duration-(--bevel-duration-small) ease-(--bevel-ease-out) not-data-checked:brightness-(--bevel-dim) data-checked:-translate-y-1.5 motion-reduce:transition-none" > {card.brand} ))} ) } ``` ## Amount A formatted figure with steady digits, in any currency and locale. Every amount in Bevel is one of these. - Category: Money - Docs: https://bevel-ui.vercel.app/components/amount - Import from: `@/components/ui/amount` ### Install ```bash npx shadcn@latest add @bevel/amount ``` npm dependencies (installed by the CLI): `cn`. ### Anatomy ```tsx ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `Amount` | span | value, currency (ISO 4217; omit for plain numbers), locale (fixed to en-US by default so server and client match), format, and signed for a leading +. Exposes data-sign. | ### Example: Currencies and locales Digits are tabular so columns line up; separators keep their natural width in any font. ```tsx import { Amount } from "@/components/ui/amount" const rows = [ { label: "US dollars", value: 1240.5, currency: "USD" }, { label: "Euros, German format", value: 1240.5, currency: "EUR", locale: "de-DE" }, { label: "Yen, no minor unit", value: 98765, currency: "JPY", locale: "ja-JP" }, { label: "Rupees, Indian grouping", value: 1234567.8, currency: "INR", locale: "en-IN" }, { label: "Credits, signed", value: 500, signed: true }, ] export function AmountFormats() { return (
{rows.map(({ label, ...amount }) => (
{label}
))}
) } ``` ## Meter A brass metering instrument with a rolling reading: live spend that ticks up, or a prepaid balance that counts down. - Category: Money - Built from: Number wheel - Docs: https://bevel-ui.vercel.app/components/meter - Import from: `@/components/ui/meter` ### Install ```bash npx shadcn@latest add @bevel/meter ``` npm dependencies (installed by the CLI): `@base-ui/react`, `cn`. ### Anatomy ```tsx ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `Meter` | div (group) | The bezel and face. state is idle, running or stopped; variant is brass or graphite. | | `MeterLabel` | p | An engraved label. | | `MeterFlag` | span | A lamp that lights while running, with Ready, Running or Stopped. Children replace the text. | | `MeterReading` | NumberWheel | The reading. Silent to screen readers while running; announced once it stops. | | `MeterUnit` | span | What the reading counts. | | `MeterButton` | Button | A brass push-button that sinks when pressed. | ### Example: Live usage The cost of a chat turn, ticking while the model writes. Run it, stop it, clear it. ```tsx "use client" import * as React from "react" import { Pause, Play } from "lucide-react" import { Meter, MeterButton, MeterFlag, MeterFooter, MeterHeader, MeterLabel, MeterReading, MeterUnit, } from "@/components/ui/meter" /** A chat turn's cost, ticking while the model writes. */ export function MeterLiveUsage() { const [running, setRunning] = React.useState(false) const [spent, setSpent] = React.useState(0) React.useEffect(() => { if (!running) return const timer = setInterval(() => setSpent((s) => s + 0.001 + Math.random() * 0.003), 400) return () => clearInterval(timer) }, [running]) return ( 0 ? "stopped" : "idle"} aria-label="This turn's cost"> This turn
so far
setRunning((r) => !r)}> {running ? : } {running ? "Stop" : "Run"} setSpent(0)}> Clear
) } ``` ### Example: Prepaid balance Like a postage meter: each call stamps a little off; add credit to refill. ```tsx "use client" import * as React from "react" import { Meter, MeterButton, MeterFlag, MeterFooter, MeterHeader, MeterLabel, MeterReading, } from "@/components/ui/meter" /** A prepaid balance, like a postage meter: each call stamps a little off. */ export function MeterPostage() { const [balance, setBalance] = React.useState(24.6) return ( Prepaid credit {balance < 5 ? "Low" : "In credit"} setBalance((b) => +(b - 0.42).toFixed(2))}> Run a call, $0.42 setBalance((b) => +(b + 10).toFixed(2))}>Add $10 ) } ``` ## Receipt Thermal paper with torn edges. Lines print in as they're added; a stamp or seal lands when it's paid. - Category: Money - Built from: Amount, Stamp and seal - Docs: https://bevel-ui.vercel.app/components/receipt - Import from: `@/components/ui/receipt` ### Install ```bash npx shadcn@latest add @bevel/receipt ``` npm dependencies (installed by the CLI): `@base-ui/react`, `cn`. ### Anatomy ```tsx ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `Receipt` | div | The paper. variant thermal, or carbon for a yellow copy behind. | | `ReceiptItems` | dl | The lines. | | `ReceiptItem` | div | One line: ReceiptLabel (dt, with a dotted leader) and ReceiptValue (dd). Lines added later print in. | | `ReceiptTotal` | div | The total line, printed bold. | | `ReceiptMark` | div | Holds a Stamp or Seal where an attendant would press it. | ### Example: Buying credits Pick a pack and the receipt reprints; pay and it's stamped. Ticket sheet, receipt and stamp together. ```tsx "use client" import * as React from "react" import { Amount } from "@/components/ui/amount" import { Receipt, ReceiptFooter, ReceiptHeader, ReceiptItem, ReceiptItems, ReceiptLabel, ReceiptMark, ReceiptMeta, ReceiptSeparator, ReceiptTitle, ReceiptTotal, ReceiptValue, } from "@/components/ui/receipt" import { Stamp, StampDetail } from "@/components/ui/stamp" import { Ticket, TicketLabel, TicketNote, TicketSheet, TicketValue, } from "@/components/ui/ticket-sheet" const packs = [ { id: "10", credits: 10, tone: "vermilion" }, { id: "50", credits: 50, tone: "ultramarine" }, { id: "100", credits: 100, tone: "emerald" }, { id: "200", credits: 200, tone: "ochre" }, ] as const const feeFor = (credits: number) => +(0.3 + credits * 0.029).toFixed(2) /** Pick a pack, watch the receipt print, pay and it gets stamped. */ export function CreditsCheckout() { const [packId, setPackId] = React.useState("50") const [paid, setPaid] = React.useState(false) const pack = packs.find((p) => p.id === packId)! const fee = feeFor(pack.credits) return (
{ setPackId(value) setPaid(false) }} className="w-full" > {packs.map((p) => ( Credits ${p.credits} + ${feeFor(p.credits).toFixed(2)} fee ))} Credit purchase 17 Oct 2026, 14:02 {pack.credits} credits Card fee Total {paid ? ( Paid 17 Oct ) : ( )} Credits never expire.
) } ``` ## Ticket sheet A sheet of perforated tickets, one per option. Picking one tears it off. A radio group underneath. - Category: Money - Docs: https://bevel-ui.vercel.app/components/ticket-sheet - Import from: `@/components/ui/ticket-sheet` ### Install ```bash npx shadcn@latest add @bevel/ticket-sheet ``` npm dependencies (installed by the CLI): `@base-ui/react`, `cn`. ### Anatomy ```tsx ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `TicketSheet` | RadioGroup | The sheet. value, defaultValue, onValueChange. | | `Ticket` | Radio | One perforated ticket. tone: vermilion, ultramarine, emerald, ochre. Lifts and tilts when chosen. | | `TicketValue` | span | The big figure. | | `TicketLabel` | span | What it is. | | `TicketNote` | span | Small print: a fee, a limit. | ### Example: Plans Plans as tickets. Arrow keys move between them; a sold-out one is disabled. ```tsx "use client" import { Ticket, TicketLabel, TicketNote, TicketSheet, TicketValue, } from "@/components/ui/ticket-sheet" /** Plans as a sheet of tickets. Arrow keys move between them. */ export function TicketSheetPlans() { return ( Solo $12 per month Team $48 up to 10 seats Firm $190 unlimited seats Custom Talk sold out this quarter ) } ``` ### Example: Credit packs with a receipt The sheet driving a receipt. ```tsx "use client" import * as React from "react" import { Amount } from "@/components/ui/amount" import { Receipt, ReceiptFooter, ReceiptHeader, ReceiptItem, ReceiptItems, ReceiptLabel, ReceiptMark, ReceiptMeta, ReceiptSeparator, ReceiptTitle, ReceiptTotal, ReceiptValue, } from "@/components/ui/receipt" import { Stamp, StampDetail } from "@/components/ui/stamp" import { Ticket, TicketLabel, TicketNote, TicketSheet, TicketValue, } from "@/components/ui/ticket-sheet" const packs = [ { id: "10", credits: 10, tone: "vermilion" }, { id: "50", credits: 50, tone: "ultramarine" }, { id: "100", credits: 100, tone: "emerald" }, { id: "200", credits: 200, tone: "ochre" }, ] as const const feeFor = (credits: number) => +(0.3 + credits * 0.029).toFixed(2) /** Pick a pack, watch the receipt print, pay and it gets stamped. */ export function CreditsCheckout() { const [packId, setPackId] = React.useState("50") const [paid, setPaid] = React.useState(false) const pack = packs.find((p) => p.id === packId)! const fee = feeFor(pack.credits) return (
{ setPackId(value) setPaid(false) }} className="w-full" > {packs.map((p) => ( Credits ${p.credits} + ${feeFor(p.credits).toFixed(2)} fee ))} Credit purchase 17 Oct 2026, 14:02 {pack.credits} credits Card fee Total {paid ? ( Paid 17 Oct ) : ( )} Credits never expire.
) } ``` ## Ledger A ruled page of numbered entries with a red margin. History and audit logs that read like a passbook. - Category: Money - Built from: Amount, Stamp and seal - Docs: https://bevel-ui.vercel.app/components/ledger - Import from: `@/components/ui/ledger` ### Install ```bash npx shadcn@latest add @bevel/ledger ``` npm dependencies (installed by the CLI): `cn`. ### Anatomy ```tsx ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `Ledger` | div | The ruled page. Every row sits exactly on one rule. | | `LedgerTable` | table | A real table, with LedgerHeader, LedgerBody, LedgerRow, LedgerHead, LedgerCell. | | `LedgerNumber` | span | The entry number, “No. 0042”, read as “Entry 0042”. | | `LedgerAmount` | Amount | Signed: credits in green ink, debits in black. | ### Example: Credit history Numbered entries with the balance after each, and stamped statuses. ```tsx import { Ledger, LedgerAmount, LedgerBody, LedgerCell, LedgerHead, LedgerHeader, LedgerNumber, LedgerRow, LedgerTable, LedgerTitle, } from "@/components/ui/ledger" import { Amount } from "@/components/ui/amount" import { Stamp } from "@/components/ui/stamp" const entries = [ { no: 45, date: "17 Oct", what: "Credits bought", amount: 50, after: 74.6, status: "Received", tone: "green" }, { no: 44, date: "16 Oct", what: "Usage, 212 calls", amount: -18.4, after: 24.6, status: "Auto", tone: "blue" }, { no: 43, date: "14 Oct", what: "Refund, case 118", amount: 12, after: 43, status: "Returned", tone: "red" }, { no: 42, date: "12 Oct", what: "Credits bought", amount: 10, after: 31, status: "Held", tone: "black" }, ] as const export function LedgerHistory() { return ( Credit ledger No. Date Entry Amount Balance Status {entries.map((entry) => ( {entry.date} {entry.what} {entry.status} ))} ) } ``` ## Keycard An API key on a plastic card that slides into a brass reader. For bring-your-own-key and credential screens. - Category: Money - Built from: Payment card - Docs: https://bevel-ui.vercel.app/components/keycard - Import from: `@/components/ui/keycard` ### Install ```bash npx shadcn@latest add @bevel/keycard ``` npm dependencies (installed by the CLI): `@base-ui/react`, `cn`. ### Anatomy ```tsx … ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `Keycard` | PaymentCard | The card, with a magnetic stripe. Takes PaymentCard's tone. | | `KeycardLabel` | span | Whose key it is. | | `KeycardKey` | span | The key, masked to its prefix and last four (read as “ending in”). revealed shows it all. | | `KeycardSlot` | div | A brass reader. inserted slides the card in; status text on the plate is announced politely. | | `KeycardSwipe` | div, card as button | A swipe reader: drag the card left to right, or flick it. onSwipe fires when it completes; accepted lights the lamp. Enter or Space swipes from the keyboard. | ### Example: Bring your own key Switch the breaker and the key goes into the reader: usage bills to it instead of credits. ```tsx "use client" import * as React from "react" import { Breaker } from "@/components/ui/breaker" import { Keycard, KeycardKey, KeycardLabel, KeycardSlot } from "@/components/ui/keycard" /** Bring your own key: put it in the reader and usage bills to it instead of credits. */ export function KeycardByok() { const [inUse, setInUse] = React.useState(false) return (
Your OpenAI key
) } ``` ### Example: Swipe to use Drag the card through the reader, or flick it. A short drag springs back. Keyboard: focus the card, press Enter. ```tsx "use client" import * as React from "react" import { Keycard, KeycardKey, KeycardLabel, KeycardSwipe } from "@/components/ui/keycard" /** Swipe the key through the reader to switch billing to it; swipe again to switch back. */ export function KeycardSwipeDemo() { const [inUse, setInUse] = React.useState(false) return ( setInUse((v) => !v)} label={inUse ? "Swipe to stop using your key" : "Swipe to use your key"} status={inUse ? "Billed to your key" : "Swipe to bill to your key"} > Your OpenAI key ) } ``` ## Number wheel Mechanical rolling digits. Each digit is a printed drum that turns to its figure, lowest digit first. - Category: Instruments - Built from: Amount - Docs: https://bevel-ui.vercel.app/components/number-wheel - Import from: `@/components/ui/number-wheel` ### Install ```bash npx shadcn@latest add @bevel/number-wheel ``` npm dependencies (installed by the CLI): `cn`. ### Anatomy ```tsx ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `NumberWheel` | span | value with the same formatting options as Amount. tone is ivory or black. live sets whether screen readers hear changes: leave it off while a value ticks. | ### Example: Counter Change the value and the drums roll to it. Sized by font size. ```tsx "use client" import * as React from "react" import { NumberWheel } from "@/components/ui/number-wheel" export function NumberWheelCounter() { const [value, setValue] = React.useState(1240) const buttons = [ { label: "+1", change: 1 }, { label: "+99", change: 99 }, { label: "−250", change: -250 }, ] return (
{buttons.map((button) => ( ))}
) } ``` ## Gauge and dial A needle on a brass-bezelled scale. Gauge reads a value; Dial sets one, turned by drag or keyboard. - Category: Instruments - Built from: Amount - Docs: https://bevel-ui.vercel.app/components/dial - Import from: `@/components/ui/dial` ### Install ```bash npx shadcn@latest add @bevel/dial ``` npm dependencies (installed by the CLI): `@base-ui/react`, `cn`. ### Anatomy ```tsx }> ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `Gauge` | Meter | Read-only. value, min, max, ticks, marks. Announced as a meter; pass getAriaValueText to describe marks. | | `GaugeLabel / GaugeValue` | Meter parts | The name and the formatted value. | | `Dial` | div (slider) | Adjustable. value, defaultValue, onValueChange, min, max, step, label, and Amount formatting for the readout. Arrows, Page Up/Down, Home, End. | | `DialMark` | span | A painted mark on the scale: red, amber or green. Decorative. | ### Example: Runway Days of credit left, with the auto top-up level marked. ```tsx "use client" import { DialMark, Gauge, GaugeLabel, GaugeValue, } from "@/components/ui/dial" /** Days of credit left, with the auto top-up level marked in amber. */ export function DialRunway() { return (
`${value} days left; auto top-up at 5 days` } marks={} > Runway {(_, value) => `${value} days`}

Amber mark: auto top-up at 5 days

) } ``` ### Example: Auto top-up Set like a thermostat: the level to keep above, and how much to add. ```tsx "use client" import * as React from "react" import { Dial, DialMark } from "@/components/ui/dial" /** Auto top-up, set like a thermostat: the level to keep above, and how much to add. */ export function DialAutoTopUp() { const [threshold, setThreshold] = React.useState(30) const [amount, setAmount] = React.useState(10) return (
} className="w-40" />

When the balance drops below ${threshold}, we add ${amount}.

) } ``` ## Breaker A circuit breaker: on, off, or tripped until someone resets it. For spending caps, kill switches and feature flags. - Category: Instruments - Docs: https://bevel-ui.vercel.app/components/breaker - Import from: `@/components/ui/breaker` ### Install ```bash npx shadcn@latest add @bevel/breaker ``` npm dependencies (installed by the CLI): `@base-ui/react`, `cn`. ### Anatomy ```tsx ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `BreakerPanel` | div (group) | The panel. Give it an aria-label. | | `Breaker` | Switch | checked, defaultChecked, onCheckedChange, disabled, plus tripped: the lever drops to the middle and a red flag shows. Switching a tripped breaker resets it; clear tripped in onCheckedChange. | ### Example: Spending caps The per-person cap has tripped; switch it to reset. Overage is locked to owners. ```tsx "use client" import * as React from "react" import { Breaker, BreakerPanel, BreakerPanelTitle, BreakerRow } from "@/components/ui/breaker" /** Spending caps as breakers. A cap that's hit trips; switching it resets it. */ export function BreakerCaps() { const [workspace, setWorkspace] = React.useState(true) const [perPerson, setPerPerson] = React.useState({ on: true, tripped: true }) const [perCase, setPerCase] = React.useState(false) return ( Spending caps setPerPerson({ on, tripped: false })} /> ) } ``` ## Engine telegraph A ship's engine order telegraph: you set an order, the other side answers by moving its own pointer to match. Both show on one dial, so a pending order is visible at a glance. For autonomy levels, deploy stages and any setting that takes effect only once acknowledged. - Category: Instruments - Docs: https://bevel-ui.vercel.app/components/engine-telegraph - Import from: `@/components/ui/engine-telegraph` ### Install ```bash npx shadcn@latest add @bevel/engine-telegraph ``` npm dependencies (installed by the CLI): `@base-ui/react`, `cn`. ### Anatomy ```tsx Ask Act ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `EngineTelegraph` | div (group) + RadioGroup | value and onValueChange for your order; confirmed for the other side's answer. Give it an aria-label. | | `EngineTelegraphDial` | div (decorative) | Your handle (dark, long) and the answer pointer (red, short) over the orders, in the order they're listed. | | `EngineTelegraphOrders` | div | Lays out the order plates. | | `EngineTelegraphOrder` | Radio | One order; its text is its label on the dial too. Arrow keys move between orders. | | `EngineTelegraphStatus` | p (status) | “Ordered: Act. Waiting for confirmation”, or “Confirmed: Act.” Describes the group. | ### Example: Agent autonomy Set how much the agent may do alone. It answers once its current step is done. ```tsx "use client" import * as React from "react" import { EngineTelegraph, EngineTelegraphDial, EngineTelegraphOrder, EngineTelegraphOrders, EngineTelegraphStatus, } from "@/components/ui/engine-telegraph" /** * Set how much the agent may do on its own. The agent finishes its current * step before it answers, so the red pointer follows a moment later. */ export function EngineTelegraphAutonomy() { const [value, setValue] = React.useState("ask") const [confirmed, setConfirmed] = React.useState("ask") React.useEffect(() => { if (value === confirmed) return const timer = setTimeout(() => setConfirmed(value), 1800) return () => clearTimeout(timer) }, [value, confirmed]) return ( Stop Plan Ask Act Auto ) } ``` ## Annunciator A panel of alarm lamps from a control room. A new alarm flashes until someone acknowledges it, then stays lit while its condition lasts; one that clears unseen keeps flashing, so nothing passes unnoticed. For agent, build and system alerts. - Category: Instruments - Docs: https://bevel-ui.vercel.app/components/annunciator - Import from: `@/components/ui/annunciator` ### Install ```bash npx shadcn@latest add @bevel/annunciator ``` npm dependencies (installed by the CLI): `@base-ui/react`, `cn`. ### Anatomy ```tsx Over budget Tests failing ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `Annunciator` | div (group) | Latches alarms and announces each one as it comes in and clears. Give it an aria-label. | | `AnnunciatorPanel` | div | The grid of lamp windows. | | `AnnunciatorTile` | button | id, active, tone red, amber, green or white. Named with its state (“Over budget: new alarm”); pressing it acknowledges it. With reduced motion, a dot marks it new instead of flashing. | | `AnnunciatorAcknowledge` | Button | “Acknowledge 2”: acknowledges every unseen alarm. Disabled, but focusable, when there are none. | ### Example: An agent's alert panel Conditions come and go on their own. Acknowledge them one by one, or all at once. ```tsx "use client" import * as React from "react" import { Annunciator, AnnunciatorAcknowledge, AnnunciatorPanel, AnnunciatorTile, } from "@/components/ui/annunciator" const conditions = [ { id: "budget", label: "Over budget", tone: "red" }, { id: "tests", label: "Tests failing", tone: "amber" }, { id: "rate", label: "Rate limited", tone: "amber" }, { id: "context", label: "Context full", tone: "amber" }, { id: "approval", label: "Needs approval", tone: "white" }, { id: "deploy", label: "Deploy live", tone: "green" }, ] as const /** An agent's alert panel. Conditions come and go; each new one flashes until you acknowledge it. */ export function AnnunciatorAgent() { const [active, setActive] = React.useState>({ tests: true }) React.useEffect(() => { const timer = setInterval(() => { const c = conditions[Math.floor(Math.random() * conditions.length)]! setActive((a) => ({ ...a, [c.id]: !a[c.id] })) }, 2600) return () => clearInterval(timer) }, []) return ( {conditions.map((c) => ( {c.label} ))} ) } ``` ## Detent knob A rotary switch that clicks into fixed positions and stops hard at both ends. For a few ordered settings where the order matters: reasoning effort, quality presets, verbosity. - Category: Instruments - Docs: https://bevel-ui.vercel.app/components/detent-knob - Import from: `@/components/ui/detent-knob` ### Install ```bash npx shadcn@latest add @bevel/detent-knob ``` npm dependencies (installed by the CLI): `cn`. ### Anatomy ```tsx Low Medium High ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `DetentKnob` | div (slider) | value, defaultValue, onValueChange, disabled. Arrow keys, Home and End, or drag round; it gives a little at the ends. Reads as “High, 3 of 4”. | | `DetentKnobStop` | nothing | One position, in order. Its text is drawn round the dial. | ### Example: Reasoning effort Turn the knob to trade speed and cost for how hard the model thinks. ```tsx "use client" import * as React from "react" import { DetentKnob, DetentKnobStop } from "@/components/ui/detent-knob" const what: Record = { low: "Quick answers. Fastest and cheapest.", medium: "Thinks before answering. The default.", high: "Works through hard problems step by step.", max: "Takes as long as it needs. Slowest and most expensive.", } /** How hard the model thinks, on a switch that clicks into place. */ export function DetentKnobEffort() { const [effort, setEffort] = React.useState("medium") return (
Low Medium High Max

{what[effort]}

) } ``` ## Push to talk A radio's push-to-talk key: it listens only while held, and letting go stops it. Nothing records by accident. For voice input to agents, walkie-talkie chat and dictation. - Category: Instruments - Docs: https://bevel-ui.vercel.app/components/push-to-talk - Import from: `@/components/ui/push-to-talk` ### Install ```bash npx shadcn@latest add @bevel/push-to-talk ``` npm dependencies (installed by the CLI): `cn`. ### Anatomy ```tsx ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `PushToTalk` | div | talking, onTalkingChange(talking), disabled. The radio body and speaker grille. | | `PushToTalkKey` | button (aria-pressed) | Pressed only while held, with the pointer or Space. Releasing, leaving the window or losing focus stops it; Enter does nothing, since it can't be held. | | `PushToTalkStatus` | p + status | The on-air lamp and its words: idle and live, “Not listening” and “Listening” by default. | ### Example: Voice notes to an agent Hold the key, or Space, to record a note. Let go to send it. ```tsx "use client" import * as React from "react" import { PushToTalk, PushToTalkKey, PushToTalkStatus } from "@/components/ui/push-to-talk" /** Talk to the agent only while the key is held. Hold Space, or press and hold with the pointer. */ export function PushToTalkAgent() { const [talking, setTalking] = React.useState(false) const [seconds, setSeconds] = React.useState(0) const [clips, setClips] = React.useState([]) React.useEffect(() => { if (!talking) return const started = Date.now() const timer = setInterval(() => setSeconds((Date.now() - started) / 1000), 100) return () => { clearInterval(timer) setClips((c) => [...c.slice(-2), (Date.now() - started) / 1000]) setSeconds(0) } }, [talking]) return (
{/* Shown, not announced: a live region that ticks would talk over you. */} {talking ? `${seconds.toFixed(1)}s` : ""}
    {clips.map((c, i) => (
  • Sent a {c.toFixed(1)}s voice note
  • ))}
) } ``` ## Mixer strip One channel of a mixing console per agent or source: its name, mute and solo keys, and a motorized fader that an orchestrator can move and you can take. For balancing subagents, models or data sources. - Category: Instruments - Built from: Motorized fader - Has parts that work alone: Motorized fader - Docs: https://bevel-ui.vercel.app/components/mixer-strip - Import from: `@/components/ui/mixer-strip` ### Install ```bash npx shadcn@latest add @bevel/mixer-strip ``` npm dependencies (installed by the CLI): `@base-ui/react`, `cn`. ### Anatomy ```tsx ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `Mixer` | div (group) | A row of strips; scrolls sideways when narrow. Give it an aria-label. | | `MixerStrip` | div (group) | name labels the group and every control in it. muted dims it. | | `MixerStripName` | p | The lit scribble strip with the name. | | `MixerStripMute` | Toggle | “Mute Research”. Lit red when pressed. | | `MixerStripSolo` | Toggle | “Solo Research”. Lit yellow when pressed. What solo silences is up to you. | | `MixerStripFader` | MotorizedFader | “Research level”. Takes every MotorizedFader prop. | ### Example: Subagents on a console A lead agent rebalances effort as it goes. Take a fader to override it; mute pauses a subagent, solo runs one alone. ```tsx "use client" import * as React from "react" import { Mixer, MixerStrip, MixerStripFader, MixerStripMute, MixerStripName, MixerStripSolo, } from "@/components/ui/mixer-strip" type Channel = { name: string; share: number; by: string; muted: boolean } const start: Channel[] = [ { name: "Research", share: 60, by: "Lead", muted: false }, { name: "Code", share: 80, by: "Lead", muted: false }, { name: "Tests", share: 45, by: "Lead", muted: false }, { name: "Docs", share: 20, by: "Lead", muted: false }, ] /** * A lead agent splits effort between its subagents and rebalances as it * goes. Take a fader to override it; mute pauses a subagent, solo runs one alone. */ export function MixerStripAgents() { const [channels, setChannels] = React.useState(start) const [solo, setSolo] = React.useState(null) const update = (name: string, patch: Partial) => setChannels((all) => all.map((c) => (c.name === name ? { ...c, ...patch } : c))) React.useEffect(() => { const timer = setInterval(() => { setChannels((all) => { const led = all.filter((c) => c.by === "Lead") const pick = led[Math.floor(Math.random() * led.length)] if (!pick) return all const share = Math.min(100, Math.max(5, pick.share + (Math.random() > 0.5 ? 25 : -25))) return all.map((c) => (c === pick ? { ...c, share } : c)) }) }, 2400) return () => clearInterval(timer) }, []) const running = (c: Channel) => !c.muted && (solo === null || solo === c.name) return (
{channels.map((c) => ( update(c.name, { muted })} /> setSolo(on ? c.name : null)} /> `${v}%`} onValueChange={(share) => update(c.name, { share, by: "You" })} /> ))}

Running: {channels.filter(running).map((c) => c.name).join(", ") || "nothing"}

) } ``` ## Motorized fader A console fader with a motor: when someone else sets it, the cap travels there on its own and says who moved it. Grab it and it's yours. For settings an agent, a teammate or an automation can change too. - Category: Instruments - Part of: Mixer strip (https://bevel-ui.vercel.app/components/mixer-strip.md) - Docs: https://bevel-ui.vercel.app/components/mixer-strip/motorized-fader - Import from: `@/components/ui/motorized-fader` ### Install ```bash npx shadcn@latest add @bevel/motorized-fader ``` npm dependencies (installed by the CLI): `cn`. ### Anatomy ```tsx { setValue(v); setChangedBy("You") }} /> ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `MotorizedFader` | div (slider) | value, onValueChange, min, max, step, changedBy, you, format, disabled. Arrow keys, Page Up and Down, Home and End, or drag. Reads as “0.70, set by Agent”. With reduced motion it jumps instead of travelling. | | `MotorizedFaderReadout` | p | A scribble strip: the value and who set it, lit when it wasn't you. Works on its own. | ### Example: Temperature the agent tunes The agent adjusts its own temperature; grab the fader to take over, then hand it back. ```tsx "use client" import * as React from "react" import { MotorizedFader, MotorizedFaderReadout } from "@/components/ui/motorized-fader" const format = (v: number) => (v / 100).toFixed(2) /** * The agent tunes its own temperature as it works, and the fader follows. * Grab it and the setting is yours until you hand it back. */ export function MotorizedFaderTemperature() { const [value, setValue] = React.useState(70) const [changedBy, setChangedBy] = React.useState("Agent") React.useEffect(() => { if (changedBy !== "Agent") return const timer = setInterval(() => setValue(10 + Math.round(Math.random() * 18) * 5), 2200) return () => clearInterval(timer) }, [changedBy]) return (
{ setValue(v) setChangedBy("You") }} />
) } ``` ## DIP switches A bank of tiny numbered slide switches, ON at the top, as on a circuit board. Every flag is visible at once and the bank reads as one value. For feature flags, permission bits and config words. - Category: Instruments - Docs: https://bevel-ui.vercel.app/components/dip-switch - Import from: `@/components/ui/dip-switch` ### Install ```bash npx shadcn@latest add @bevel/dip-switch ``` npm dependencies (installed by the CLI): `@base-ui/react`, `cn`. ### Anatomy ```tsx Streamed replies Long-term memory ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `DipSwitch` | div (group) | value (the values that are on), defaultValue, onValueChange, disabled, variant red, blue or black. Give it an aria-label. | | `DipSwitchBank` | div | The housing, with ON printed above. Left and right arrow keys move along it. | | `DipSwitchToggle` | Switch | value; its text is its label. Numbered by position, from 1, and named “3: Web search tool”. Space flips it; disabled locks one switch. | | `DipSwitchLegend` | ol (decorative) | Each switch's number, label and ON or OFF, printed beside the bank. Hidden from screen readers, which read the switches. | | `DipSwitchReadout` | span | The bank as a number: format hex (0x0B), binary (0b00001011) or decimal. Switch 1 is the lowest bit. | ### Example: Feature flags Flip a workspace's flags; the bank is also the config value you ship. One is locked by the organisation. ```tsx "use client" import * as React from "react" import { DipSwitch, DipSwitchBank, DipSwitchLegend, DipSwitchReadout, DipSwitchToggle, } from "@/components/ui/dip-switch" const flags = [ { value: "new-editor", label: "New editor" }, { value: "streaming", label: "Streamed replies" }, { value: "web-search", label: "Web search tool" }, { value: "memory", label: "Long-term memory" }, { value: "batch-api", label: "Batch API" }, { value: "sso", label: "Enforce SSO" }, { value: "audit-export", label: "Audit log export" }, { value: "beta-models", label: "Beta models" }, ] /** A workspace's feature flags on a bank of DIP switches. The bank is also the config value you ship. */ export function DipSwitchFlags() { const [on, setOn] = React.useState(["new-editor", "streaming", "sso"]) return (

Feature flags: acme-staging

Enforce SSO is locked on by your organisation.

{flags.map((flag) => ( {flag.label} ))}
{on.length} of {flags.length} on FLAGS=
) } ``` ## Patch bay A rack of jacks joined by patch cables: every route from a source to a destination is a cable you can see and pull. For webhook events to handlers, agent tools to agents, and inputs to outputs. - Category: Instruments - Docs: https://bevel-ui.vercel.app/components/patch-bay - Import from: `@/components/ui/patch-bay` ### Install ```bash npx shadcn@latest add @bevel/patch-bay ``` npm dependencies (installed by the CLI): `@base-ui/react`, `cn`. ### Anatomy ```tsx invoice.paid Send receipt ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `PatchBay` | div (group) | value and defaultValue: [source, destination] pairs. onValueChange, disabled. Announces each patch, and lists the routes for screen readers. Give it an aria-label. | | `PatchBayPanel` | div | The rack panel. Draws the cables, which hang a little between the rows, in a colour per source. | | `PatchBayRow` | div (group) | side source or destination, and a label naming the row. Sources sit on top, destinations below. | | `PatchBayJack` | Button | value; its text is its name. Press one jack, then one in the other row, to patch them; patch the same pair again to pull it. Escape cancels; Delete pulls all its cables. Described as “Patched to Send receipt and Update ledger.” | | `PatchBayRoutes` | ul | Optional: a visible list of routes with an Unpatch button each. Replaces the built-in screen-reader list. | ### Example: Webhook routing Patch a billing provider's events to your handlers. One event can feed several handlers. ```tsx "use client" import * as React from "react" import { PatchBay, PatchBayJack, PatchBayPanel, PatchBayRoutes, PatchBayRow, type PatchBayRoute, } from "@/components/ui/patch-bay" const events = ["invoice.paid", "charge.failed", "trial.ending", "sub.deleted"] const handlers = [ { value: "receipt", label: "Send receipt" }, { value: "ledger", label: "Update ledger" }, { value: "slack", label: "Slack #billing" }, { value: "dunning", label: "Dunning email" }, { value: "seats", label: "Revoke seats" }, ] /** Route a billing provider's webhook events to your handlers. Press an event, then a handler. */ export function PatchBayWebhooks() { const [routes, setRoutes] = React.useState([ ["invoice.paid", "receipt"], ["invoice.paid", "ledger"], ["charge.failed", "dunning"], ]) const unrouted = events.filter((e) => !routes.some(([from]) => from === e)) return ( {events.map((e) => ( {e} ))} {handlers.map((h) => ( {h.label} ))}

{routes.length} route{routes.length === 1 ? "" : "s"} {unrouted.length > 0 && ( {" "} · {unrouted.join(", ")} {unrouted.length === 1 ? "goes" : "go"} nowhere )}

) } ``` ## Guarded switch A critical switch under a flip-up cover, as in a cockpit. Lift the cover, then flip it; closing the cover returns it to safe, and the cover closes itself if left. - Category: Coordination - Docs: https://bevel-ui.vercel.app/components/guarded-switch - Import from: `@/components/ui/guarded-switch` ### Install ```bash npx shadcn@latest add @bevel/guarded-switch ``` npm dependencies (installed by the CLI): `@base-ui/react`, `cn`. ### Anatomy ```tsx ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `GuardedSwitch` | div (group) | checked, defaultChecked, onCheckedChange. safeOnClose (default true) turns it off when the cover closes; autoClose closes the cover after --guarded-switch-close-after. disabled locks the cover shut; say why with aria-describedby. | | `GuardedSwitchLabel` | span | Names the group and the switch. | | `GuardedSwitchControl` | button + Switch | The cover (a button with aria-expanded) over a Base UI Switch that's disabled while the cover is closed. | | `GuardedSwitchStatus` | span (status) | Off, cover open, or on, in words. | ### Example: Agent permissions and deploys Lift a cover, then flip the switch. Leave it and the cover closes, turning it off. ```tsx "use client" import * as React from "react" import { GuardedSwitch, GuardedSwitchControl, GuardedSwitchLabel, GuardedSwitchStatus, } from "@/components/ui/guarded-switch" /** Two critical switches. Lift a cover, then flip. Leave it and the cover closes, turning it off. */ export function GuardedSwitchDeploy() { const [shell, setShell] = React.useState(false) return (
Agent shell access Deploy to prod

{shell ? "The agent can run shell commands without asking." : "The agent asks before running shell commands."}

) } ``` ## Dead man's switch A train cab's vigilance control for supervised agent runs: the run keeps going only while someone checks in. Miss the window and it warns, then pauses. - Category: Coordination - Docs: https://bevel-ui.vercel.app/components/vigilance - Import from: `@/components/ui/vigilance` ### Install ```bash npx shadcn@latest add @bevel/vigilance ``` npm dependencies (installed by the CLI): `@base-ui/react`, `cn`. ### Anatomy ```tsx ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `Vigilance` | div (group) | interval (ms), warnAt and stopAt (share left), running, onTrip, onCheckIn, onResume. | | `VigilanceLamps` | div | Clear, Warning, Stopping lamps (steady, never flashing) and the stage in words. Only stage changes are announced; the final warning and the pause assertively. | | `VigilanceTimer` | Progress | Time until the next check-in, read in seconds. | | `VigilanceButton` | Button | Check in. While paused it becomes Resume. | ### Example: A supervised agent run Check in every 12 seconds to keep it going. Stop checking in and watch it warn, then pause. ```tsx "use client" import * as React from "react" import { Vigilance, VigilanceButton, VigilanceLamps, VigilanceTimer } from "@/components/ui/vigilance" /** A supervised agent run: it keeps going only while you check in every 12 seconds. */ export function VigilanceAgent() { const [log, setLog] = React.useState("Agent running with supervision.") return (
setLog("No check-in: the agent paused before its next action.")} onResume={() => setLog("Resumed. The agent carries on.")} onCheckIn={() => setLog("Checked in.")} >

{log}

) } ``` ## Lockout A lockout-tagout hasp: everyone working on something hangs their own padlock and tag, and it can't start until the last lock comes off. For freezes and holds. - Category: Coordination - Docs: https://bevel-ui.vercel.app/components/lockout - Import from: `@/components/ui/lockout` ### Install ```bash npx shadcn@latest add @bevel/lockout ``` npm dependencies (installed by the CLI): `@base-ui/react`, `cn`. ### Anatomy ```tsx Run migration ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `Lockout` | div (group) | Counts the locks on it. Give it an aria-label. | | `LockoutHasp` | ul | The bar the locks hang from. | | `LockoutLock` | li | id, owner, reason, shown on a danger tag. onRemove adds “Remove my lock”: pass it only for the current user's lock. | | `LockoutAction` | Button | Disabled while any lock remains, described as “Locked by 2: Priya, Sam”. | ### Example: A migration on hold Three locks hold the migration. Remove yours; the others come off as their work finishes. ```tsx "use client" import * as React from "react" import { Lockout, LockoutAction, LockoutHasp, LockoutLock } from "@/components/ui/lockout" const start = [ { id: "priya", owner: "Priya", reason: "Restoring a backup" }, { id: "you", owner: "You", reason: "Reviewing the schema" }, { id: "sam", owner: "Sam", reason: "Load test running" }, ] /** The migration waits for every lock. Remove yours; the others come off as their work finishes. */ export function LockoutMigration() { const [locks, setLocks] = React.useState(start) const [ran, setRan] = React.useState(false) const remove = (id: string) => setLocks((l) => l.filter((x) => x.id !== id)) React.useEffect(() => { if (locks.some((l) => l.id === "you")) return const others = locks.filter((l) => l.id !== "you") if (!others.length) return const timer = setTimeout(() => remove(others[0]!.id), 1600) return () => clearTimeout(timer) }, [locks]) return (
{locks.map((l) => ( remove("you") : undefined} /> ))} setRan(true)}>{ran ? "Migration started" : "Run migration"}
) } ``` ## Take a number A ticket dispenser and a Now Serving sign: your place in the queue is the number in your hand. For support queues, waitlists and jobs waiting their turn. - Category: Coordination - Built from: Number wheel - Docs: https://bevel-ui.vercel.app/components/take-a-number - Import from: `@/components/ui/take-a-number` ### Install ```bash npx shadcn@latest add @bevel/take-a-number ``` npm dependencies (installed by the CLI): `@base-ui/react`, `cn`. ### Anatomy ```tsx ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `TakeANumber` | div (group) | serving, and ticket: the number you hold, or null. Give it an aria-label. | | `TakeANumberSign` | div | Now Serving, on number wheels that roll to each new number. Its lamp lights on your turn. | | `TakeANumberDispenser` | Button | “Take a number”. Tears off your ticket; disabled, but focusable, while you hold one. | | `TakeANumberStatus` | p (status) | “You're 44: 2 ahead of you.” Announced once per number served. | ### Example: A support queue Take a number; the sign rolls on every two seconds until it's your turn. ```tsx "use client" import * as React from "react" import { TakeANumber, TakeANumberDispenser, TakeANumberSign, TakeANumberStatus, } from "@/components/ui/take-a-number" /** A busy support queue. Take a number and watch the sign roll until it's yours. */ export function TakeANumberSupport() { const [serving, setServing] = React.useState(17) const [issued, setIssued] = React.useState(20) const [ticket, setTicket] = React.useState(null) React.useEffect(() => { if (ticket === null || serving >= ticket) return const timer = setTimeout(() => setServing((s) => s + 1), 2000) return () => clearTimeout(timer) }, [serving, ticket]) return (
{ setTicket(issued) setIssued((n) => n + 1) }} /> {ticket !== null && serving >= ticket && ( )}
) } ``` ## Keysort filter Edge-notched cards from the 1950s office: each hole along the top is an attribute, notched open if the card has it. Push needles down the columns and the cards notched for all of them fall free. AND filtering you can see. For documents, tickets and records with tags. - Category: Records - Docs: https://bevel-ui.vercel.app/components/keysort-filter - Import from: `@/components/ui/keysort-filter` ### Install ```bash npx shadcn@latest add @bevel/keysort-filter ``` npm dependencies (installed by the CLI): `@base-ui/react`, `cn`. ### Anatomy ```tsx NDA Signed Acme mutual NDA ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `KeysortFilter` | div (group) | value, defaultValue and onValueChange: the needles pushed through. Give it an aria-label. | | `KeysortFilterNeedles` | div (group) | The needle handles, one above each column of holes. | | `KeysortFilterNeedle` | Toggle | One attribute, and the column of holes under it, in the order listed. Pushed in, a rod runs down the column. | | `KeysortFilterDeck` | ul | The cards, one per row, with the needles running down behind them. | | `KeysortFilterCard` | li | notches: the attributes it has. Caught cards show the needle through their hole; free ones get a tab and say “Matches”. | | `KeysortFilterCount` | p (status) | “2 of 8 cards match NDA and Signed.” Announced as needles go in and out. | ### Example: A drawer of contracts Push needles through to find the contracts with every attribute. ```tsx "use client" import { KeysortFilter, KeysortFilterCard, KeysortFilterCount, KeysortFilterDeck, KeysortFilterNeedle, KeysortFilterNeedles, } from "@/components/ui/keysort-filter" const contracts = [ { title: "Acme mutual NDA", tags: ["nda", "signed", "auto"] }, { title: "Globex NDA, redlined", tags: ["nda", "eu"] }, { title: "Initech supply agreement", tags: ["signed", "eu", "auto"] }, { title: "Umbrella term sheet", tags: [] }, { title: "Hooli data processing addendum", tags: ["signed", "eu", "dpa"] }, { title: "Stark consulting SOW", tags: ["signed", "auto"] }, { title: "Wayne one-way NDA", tags: ["nda"] }, { title: "Tyrell DPA, draft", tags: ["eu", "dpa"] }, ] /** A drawer of contracts. Push needles in to find the ones with every attribute. */ export function KeysortFilterContracts() { return ( NDA Signed EU Renews DPA {contracts.map((c) => ( {c.title} ))} ) } ``` ## Visible file A Kardex visible-index tray: each record shows only its edge, and coloured signal clips slide along it to mark where it stands, so a whole tray reads at a glance. For pipelines, renewals and task stages. - Category: Records - Docs: https://bevel-ui.vercel.app/components/visible-file - Import from: `@/components/ui/visible-file` ### Install ```bash npx shadcn@latest add @bevel/visible-file ``` npm dependencies (installed by the CLI): `cn`. ### Anatomy ```tsx ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `VisibleFile` | div (group) + ul | scale: the positions printed across the top. Give it an aria-label. | | `VisibleFileRow` | li | title and detail, typed on the card edge in its clear pocket. | | `VisibleFileSignal` | span | value (an index into the scale), label, tone. Slides when it changes; reads as “Stage: Build.” | ### Example: Agent tasks by stage Signals slide along each card as its task moves through the stages. ```tsx "use client" import * as React from "react" import { VisibleFile, VisibleFileRow, VisibleFileSignal } from "@/components/ui/visible-file" const stages = ["Queued", "Plan", "Build", "Test", "Review", "Done"] const start = [ { title: "Fix login redirect", agent: "Coder", stage: 3, blocked: false }, { title: "Billing refactor", agent: "Coder", stage: 2, blocked: false }, { title: "API reference", agent: "Writer", stage: 1, blocked: false }, { title: "Flaky checkout test", agent: "Tester", stage: 3, blocked: true }, { title: "Bump dependencies", agent: "Coder", stage: 0, blocked: false }, ] /** A tray of agent tasks. Signals slide along each card as its task moves through the stages. */ export function VisibleFileTasks() { const [tasks, setTasks] = React.useState(start) React.useEffect(() => { const timer = setInterval(() => { setTasks((all) => { const open = all.filter((t) => t.stage < stages.length - 1 && !t.blocked) const pick = open[Math.floor(Math.random() * open.length)] if (!pick) return start return all.map((t) => (t === pick ? { ...t, stage: t.stage + 1 } : t)) }) }, 1600) return () => clearInterval(timer) }, []) return ( {tasks.map((t) => ( ))} ) } ``` ## Contact sheet A photographer's contact sheet: every frame at once on a strip of film, marked with a grease pencil. Circle the keepers, cross out the rest. For reviewing generated images, design variants and screenshots. - Category: Records - Docs: https://bevel-ui.vercel.app/components/contact-sheet - Import from: `@/components/ui/contact-sheet` ### Install ```bash npx shadcn@latest add @bevel/contact-sheet ``` npm dependencies (installed by the CLI): `cn`. ### Anatomy ```tsx ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `ContactSheet` | div (group) | value, defaultValue, onValueChange: marks by frame id, “pick” or “reject”. columns. Counts the picks. Give it an aria-label. | | `ContactSheetFrame` | button | id, label; the picture is its children. Named “3. Dusk, low sun, picked”. Click cycles; arrow keys move by frame and row, P picks, X rejects, Backspace clears. | ### Example: Generated variants Eight variants of one prompt. Circle the keepers, cross out the rest. ```tsx "use client" import * as React from "react" import { ContactSheet, ContactSheetFrame } from "@/components/ui/contact-sheet" // Eight generated variants of one prompt, drawn as simple landscapes. const variants = [ { id: "v1", label: "Dusk, low sun", sky: 40, land: 60, sun: 0.7 }, { id: "v2", label: "Noon, haze", sky: 230, land: 120, sun: 0.25 }, { id: "v3", label: "Night, moon", sky: 270, land: 250, sun: 0.35 }, { id: "v4", label: "Dawn, pink", sky: 350, land: 30, sun: 0.85 }, { id: "v5", label: "Storm", sky: 250, land: 160, sun: 0.1 }, { id: "v6", label: "Desert", sky: 60, land: 70, sun: 0.5 }, { id: "v7", label: "Arctic", sky: 210, land: 220, sun: 0.6 }, { id: "v8", label: "Jungle", sky: 160, land: 140, sun: 0.4 }, ] function Scene({ sky, land, sun }: { sky: number; land: number; sun: number }) { return ( ) } /** Review eight generated variants. Circle the keepers, cross out the rest. */ export function ContactSheetVariants() { const [marks, setMarks] = React.useState>({ v1: "pick", v5: "reject" }) const picked = variants.filter((v) => marks[v.id] === "pick") return (
{variants.map((v) => ( ))}

Click to cycle, or use arrow keys with P, X and Backspace.{" "} {picked.length ? `Keeping: ${picked.map((v) => v.label).join(", ")}.` : "Nothing kept yet."}

) } ``` ## Split-flap board A station departure board: each character is a pair of flaps that falls to the next value, column by column. The text is the truth and changes are announced once per row. For deploy boards, queues and live status. - Category: Records - Docs: https://bevel-ui.vercel.app/components/split-flap - Import from: `@/components/ui/split-flap` ### Install ```bash npx shadcn@latest add @bevel/split-flap ``` npm dependencies (installed by the CLI): `cn`. ### Anatomy ```tsx ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `SplitFlap` | div (group) + ul | Gathers changes and announces them together: “Updated: US East, EU West.” Give it an aria-label. | | `SplitFlapRow` | li | name: how the row is announced when it changes. | | `SplitFlapText` | span | value, length (padded or cut), tone white or amber. Reads as its text; the flaps are decoration. With reduced motion, characters crossfade. | ### Example: A deploy board Each region's status flips over as its rollout moves on. ```tsx "use client" import * as React from "react" import { SplitFlap, SplitFlapRow, SplitFlapText } from "@/components/ui/split-flap" const regions = ["US-EAST", "US-WEST", "EU-WEST", "AP-SOUTH"] const stages = ["QUEUED", "BUILDING", "TESTING", "ROLLOUT", "LIVE"] /** A deploy board. Each region's status flips over as its rollout moves on. */ export function SplitFlapDeploys() { const [at, setAt] = React.useState([4, 2, 1, 0]) React.useEffect(() => { const timer = setInterval(() => { setAt((all) => { if (all.every((s) => s === stages.length - 1)) return [0, 0, 0, 0] const open = all.map((s, i) => (s < stages.length - 1 ? i : -1)).filter((i) => i >= 0) const pick = open[Math.floor(Math.random() * open.length)]! return all.map((s, i) => (i === pick ? s + 1 : s)) }) }, 2200) return () => clearInterval(timer) }, []) return ( {regions.map((r, i) => ( 3 ? 1 : 0)}`} length={5} /> ))} ) } ``` ## Time card A punch clock's time card: each session is a row with its date, time in, time out and how long it ran, and the key stamps the time now onto the next line. For timesheets, focus sessions and on-call shifts. - Category: Records - Docs: https://bevel-ui.vercel.app/components/time-card - Import from: `@/components/ui/time-card` ### Install ```bash npx shadcn@latest add @bevel/time-card ``` npm dependencies (installed by the CLI): `@base-ui/react`, `cn`. ### Anatomy ```tsx ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `TimeCard` | div + table | title (the table's caption), entries ({ in, out? }, oldest first; no out means still clocked in), onPunch({ kind, at }), rows, locale, timeZone. Durations and times print with Intl; without a timeZone they print once the browser's is known. New stamps land; the history doesn't. | | `TimeCardStatus` | p (status) | “Clocked in at 09:04.” or “Clocked out at 17:42. 10h 46m on this card.” Describes the punch key. | | `TimeCardPunch` | Button | Punch in, or Punch out while a session is open. Calls onPunch with the time now. | ### Example: A contractor's timesheet Three sessions on the card already. Punch in, then out, and watch the clock stamp each time. ```tsx "use client" import * as React from "react" import { TimeCard, TimeCardPunch, TimeCardStatus, type TimeCardEntry } from "@/components/ui/time-card" /** A contractor's timesheet for one client. Punch in, punch out; the clock stamps the time now. */ export function TimeCardTimesheet() { const [entries, setEntries] = React.useState([ { in: "2026-10-19T08:58:00Z", out: "2026-10-19T12:31:00Z" }, { in: "2026-10-19T13:15:00Z", out: "2026-10-19T17:42:00Z" }, { in: "2026-10-20T09:04:00Z", out: "2026-10-20T11:50:00Z" }, ]) return ( setEntries((all) => kind === "in" ? [...all, { in: at }] : all.map((e, i) => (i === all.length - 1 ? { ...e, out: at } : e)) ) } >
= 10} />
) } ``` ## Due-date slip The library card in the back of a book: each loan or renewal stamps the next ruled line, and the last date is when it's due. Overdue dates are stamped red. For expiring keys, trials, seats and access grants. - Category: Records - Built from: Stamp and seal - Docs: https://bevel-ui.vercel.app/components/due-date-slip - Import from: `@/components/ui/due-date-slip` ### Install ```bash npx shadcn@latest add @bevel/due-date-slip ``` npm dependencies (installed by the CLI): `@base-ui/react`, `cn`. ### Anatomy ```tsx Renew 30 days ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `DueDateSlip` | div (group) + ol | title, dates (oldest first; the last counts), now. New dates land with a stamp; the history doesn't. Dates print in UTC so server and client agree. | | `DueDateSlipStatus` | p (status) | “Due 16 Nov 2026, in 30 days.” or “Overdue by 3 days.” Describes the renew button. | | `DueDateSlipRenew` | Button | Your onClick adds the next date. | ### Example: An expiring API key The key lapsed four days ago. Renew it, and again, until the renewals run out. ```tsx "use client" import * as React from "react" import { DueDateSlip, DueDateSlipRenew, DueDateSlipStatus } from "@/components/ui/due-date-slip" const NOW = new Date("2026-10-17") const DAY = 86_400_000 const iso = (d: Date) => d.toISOString().slice(0, 10) /** An API key that expires unless renewed. Each renewal is stamped on the slip. */ export function DueDateSlipKey() { const [dates, setDates] = React.useState(["2026-07-15", "2026-08-14", "2026-10-13"]) const renew = () => { const last = new Date(dates.at(-1)!) const from = last < NOW ? NOW : last setDates((d) => [...d, iso(new Date(from.getTime() + 30 * DAY))]) } return ( = 6}> {dates.length >= 6 ? "No renewals left" : "Renew 30 days"} ) } ``` ## Name tag Who someone is: an agent, a person, a guest. The “HELLO my name is” sticker, a visitor badge for outside collaborators, and a plain tag for lists. - Category: Agents - Docs: https://bevel-ui.vercel.app/components/name-tag - Import from: `@/components/ui/name-tag` ### Install ```bash npx shadcn@latest add @bevel/name-tag ``` npm dependencies (installed by the CLI): `cn`. ### Anatomy ```tsx ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `NameTag` | div | variant: sticker, visitor or plain. greeting and caption translate the printed words, which screen readers skip. | | `NameTagName` | span | The name, in marker on the sticker and badge. | | `NameTagDetail` | span | A role, or who an agent acts for. | | `NameTagInput` | input | Write a new name on the tag. Labelled “Name”. | ### Example: Agents and guests Rename the agent by writing on its sticker. Guests get a visitor badge; lists use the plain tag. ```tsx "use client" import * as React from "react" import { NameTag, NameTagDetail, NameTagInput, NameTagName } from "@/components/ui/name-tag" /** Agents wear stickers, guests wear visitor badges, and lists use the plain tag. */ export function NameTagAgents() { const [name, setName] = React.useState("Drafting") return (
setName(e.target.value)} /> acting for Priya Jordan Lee Co-counsel, Hale & Ward
{name || "Unnamed"} Research for Priya Review
) } ``` ## Task plan An agent's plan on a legal pad. Finished steps are ticked in ink, the current one is underlined in red pen, the rest are still in pencil. - Category: Agents - Docs: https://bevel-ui.vercel.app/components/task-plan - Import from: `@/components/ui/task-plan` ### Install ```bash npx shadcn@latest add @bevel/task-plan ``` npm dependencies (installed by the CLI): `@base-ui/react`, `cn`. ### Anatomy ```tsx ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `TaskPlan` | div | The pad. Every row sits on a rule. | | `TaskPlanProgress` | Progress | value and max. Read as “2 of 5 done”; children replace the visible text. | | `TaskPlanSteps` | ol | The steps, in order. | | `TaskPlanStep` | li | status: done, current, pending, error or skipped, spoken before the step. The current step is marked aria-current. The tick draws itself when a step finishes. | ### Example: An agent at work The plan advances on its own. Each step is ticked in ink as it finishes. ```tsx "use client" import * as React from "react" import { TaskPlan, TaskPlanHeader, TaskPlanProgress, TaskPlanStep, TaskPlanSteps, TaskPlanTitle, type TaskPlanStatus, } from "@/components/ui/task-plan" const steps = [ "Read the lease and schedules", "List every notice clause", "Check notice periods against the playbook", "Draft a redline", "Summarise for Priya", ] /** An agent working down its plan. Each step is ticked in ink as it finishes. */ export function TaskPlanAgent() { const [done, setDone] = React.useState(1) React.useEffect(() => { if (done >= steps.length) return const timer = setTimeout(() => setDone((d) => d + 1), 1600) return () => clearTimeout(timer) }, [done]) const statusOf = (i: number): TaskPlanStatus => (i < done ? "done" : i === done ? "current" : "pending") return (
Review the Harbor Street lease {steps.map((step, i) => ( {step} ))}
) } ``` ## Activity log A docket of what an agent did: thoughts, actions, questions, responses and errors, each timed. Questions waiting on you are highlighted. - Category: Agents - Docs: https://bevel-ui.vercel.app/components/activity-log - Import from: `@/components/ui/activity-log` ### Install ```bash npx shadcn@latest add @bevel/activity-log ``` npm dependencies (installed by the CLI): `cn`. ### Anatomy ```tsx ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `ActivityLog` | section | The docket page. Give it an aria-label. | | `ActivityLogEntries` | div (log) | A live log: new entries are announced politely. Scrolls past --activity-log-height and follows new entries only while you're at the bottom. | | `ActivityLogEntry` | div | kind: thought, action, question, response or error (shown in words), time and dateTime. Entries added later print in. | ### Example: Two agents on a matter Entries arrive as the agents work. Who acted is a plain Name tag. ```tsx "use client" import * as React from "react" import { ActivityLog, ActivityLogEntries, ActivityLogEntry, ActivityLogTitle, type ActivityLogKind, } from "@/components/ui/activity-log" import { NameTag, NameTagName } from "@/components/ui/name-tag" type Entry = { kind: ActivityLogKind; agent: string; text: string } const script: Entry[] = [ { kind: "thought", agent: "Research", text: "The notice clause is probably in section 9." }, { kind: "action", agent: "Research", text: "Opened lease.pdf, page 14." }, { kind: "response", agent: "Research", text: "Found three notice clauses." }, { kind: "action", agent: "Drafting", text: "Started a redline of clause 9.2." }, { kind: "question", agent: "Drafting", text: "Should email count as written notice?" }, { kind: "error", agent: "Research", text: "Couldn't open schedule-b.pdf: it's password protected." }, { kind: "response", agent: "Drafting", text: "Redline ready for review." }, ] /** Two agents working a matter. Entries arrive as they happen. */ export function ActivityLogAgent() { const [count, setCount] = React.useState(2) React.useEffect(() => { if (count >= script.length) return const timer = setTimeout(() => setCount((c) => c + 1), 1400) return () => clearTimeout(timer) }, [count]) return (
Harbor Street lease {script.slice(0, count).map((entry, i) => ( {entry.agent} {entry.text} ))}
) } ``` ## Approval An action an agent wants to take, waiting for a person. Say what it does and what can't be undone; press and hold the brass seal to approve. - Category: Agents - Built from: Stamp and seal - Docs: https://bevel-ui.vercel.app/components/approval - Import from: `@/components/ui/approval` ### Install ```bash npx shadcn@latest add @bevel/approval ``` npm dependencies (installed by the CLI): `@base-ui/react`, `cn`. ### Anatomy ```tsx ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `Approval` | div (group) | state (pending, approved, rejected), defaultState, onApprove, onReject. Labelled by its title; the outcome is pressed into the card and announced. | | `ApprovalDescription` | p | What happens and what can't be undone. | | `ApprovalSignature` | li | A signature line; signed names are written over it. For dual approval. | | `ApprovalReject` | Button | Send it back. A single press. | | `ApprovalSeal` | Button | Press and hold (pointer, Enter or Space) for --approval-hold to approve; letting go early springs back. requireHold={false} for a single press. | ### Example: Before an agent sends email Hold the seal until it presses. Let go early and nothing happens. ```tsx "use client" import * as React from "react" import { Approval, ApprovalActions, ApprovalDescription, ApprovalReject, ApprovalSeal, ApprovalSignature, ApprovalSignatures, ApprovalTitle, type ApprovalState, } from "@/components/ui/approval" /** The drafting agent wants to send a redline. Hold the seal to approve it. */ export function ApprovalSend() { const [state, setState] = React.useState("pending") return (
setState("approved")} onReject={() => setState("rejected")}> Send the redline to opposing counsel The drafting agent will email it to j.lee@haleward.com. Sent email can't be recalled. Send back
) } ``` ## Delegation board Pigeonholes for delegated work, one per subagent: its task slip, a lamp and a word for its status, and its progress. Finished work rises out of its cubby. - Category: Agents - Built from: Name tag - Docs: https://bevel-ui.vercel.app/components/delegation-board - Import from: `@/components/ui/delegation-board` ### Install ```bash npx shadcn@latest add @bevel/delegation-board ``` npm dependencies (installed by the CLI): `@base-ui/react`, `cn`. ### Anatomy ```tsx … ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `DelegationBoard` | ul | The board. Give it an aria-label. | | `DelegationSlot` | li | One subagent. status: pending, active, awaiting, error or complete, shown as a lamp and in words. | | `DelegationAgent` | div | Who it is, with the status lamp. Holds a Name tag. | | `DelegationTask` | div | The task slip. Rises out of the cubby when complete. | | `DelegationProgress` | Progress | value and max; name it after the agent. | ### Example: A coordinator's subagents Four agents working in parallel. Drafting stops to ask you a question; answer it to unblock Review. ```tsx "use client" import * as React from "react" import { DelegationAgent, DelegationBoard, DelegationProgress, DelegationSlot, DelegationTask, type DelegationStatus, } from "@/components/ui/delegation-board" import { NameTag, NameTagName } from "@/components/ui/name-tag" type Agent = { name: string; task: string; progress: number; status: DelegationStatus } const start: Agent[] = [ { name: "Research", task: "Find cases on notice by email", progress: 35, status: "active" }, { name: "Drafting", task: "Redline clause 9.2", progress: 60, status: "active" }, { name: "Review", task: "Check the redline against the playbook", progress: 0, status: "pending" }, { name: "Citations", task: "Verify every authority cited", progress: 20, status: "active" }, ] /** A coordinator's subagents at work. Drafting stops to ask you something. */ export function DelegationBoardAgents() { const [agents, setAgents] = React.useState(start) React.useEffect(() => { const timer = setInterval(() => { setAgents((list) => list.map((a) => { if (a.status !== "active") return a const progress = Math.min(100, a.progress + 8 + Math.round(Math.random() * 8)) if (a.name === "Drafting" && progress >= 80) return { ...a, progress: 80, status: "awaiting" } return { ...a, progress, status: progress >= 100 ? "complete" : "active" } }) ) }, 900) return () => clearInterval(timer) }, []) const answer = () => setAgents((list) => list.map((a) => a.name === "Drafting" ? { ...a, status: "active" } : a.name === "Review" && a.status === "pending" ? { ...a, status: "active" } : a ) ) return (
{agents.map((a) => ( {a.name} {a.status === "awaiting" ? "Should email count as written notice?" : a.task} ))}
) } ``` ## Citation An exhibit sticker in the text that opens the exact passage it cites, with a stamp saying whether the source checks out. - Category: Documents - Built from: Stamp and seal - Docs: https://bevel-ui.vercel.app/components/citation - Import from: `@/components/ui/citation` ### Install ```bash npx shadcn@latest add @bevel/citation ``` npm dependencies (installed by the CLI): `@base-ui/react`, `cn`. ### Anatomy ```tsx 4 ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `Citation` | Popover | One cited source. open, defaultOpen, onOpenChange. | | `CitationTrigger` | button | The exhibit sticker, inline in text: “Ex. 4”, read as “Exhibit 4”. tone: yellow, blue, white. abbreviation and label change the prefix. | | `CitationContent` | Popover popup | The paper slip. Grows from the sticker; Escape closes it and returns focus. | | `CitationSource` | Popover title | The source's name; titles the slip. | | `CitationLocation` | p | Page, paragraph or clause. | | `CitationQuote` | blockquote | The passage, highlighted. | | `CitationStatus` | Stamp | verified, caution or unverified, always in words. Children replace the word. | ### Example: An answer with sources Open a sticker to read the cited passage. The third source has been appealed. ```tsx "use client" import { Citation, CitationContent, CitationLocation, CitationQuote, CitationSource, CitationStatus, CitationTrigger, } from "@/components/ui/citation" /** An agent's answer with its sources pinned in. Open a sticker to read the passage. */ export function CitationAnswer() { return (

The landlord can end the lease on thirty days' notice 1 Lease agreement, Harbor Street p. 14, clause 9.2 Either party may end this lease on thirty (30) days' written notice. , but notice must be written and delivered to the address in the schedule 2 Lease agreement, Schedule B p. 31 Notices: 400 Harbor Street, Suite 12, attention of the Tenant. .

Courts have read similar clauses strictly 3 Marlow v. Pierce Holdings (2019) ¶ 42 A notice not delivered as the lease requires is no notice at all. Appealed , so email alone may not count.

) } ``` ## Redline Proposed edits in proofreader's red ink, each with a sign-here flag to accept or reject it. Nothing is applied until someone decides. - Category: Documents - Docs: https://bevel-ui.vercel.app/components/redline - Import from: `@/components/ui/redline` ### Install ```bash npx shadcn@latest add @bevel/redline ``` npm dependencies (installed by the CLI): `cn`. ### Anatomy ```tsx … … ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `Redline` | div | Holds the decisions: decisions, defaultDecisions, onDecisionsChange ({ id: accepted \| rejected }). | | `RedlineDocument` | div | The text being edited. | | `RedlineChange` | span (group) | One edit: id, deletion, insertion. Uses real del and ins, read as “deleted” and “inserted”. Its flag holds Accept and Reject. | | `RedlineToolbar` | div (toolbar) | How many changes are left, with Accept all and Reject all. | ### Example: A notice clause Three suggested edits. Decide each one, or all at once. ```tsx "use client" import { Redline, RedlineChange, RedlineDocument, RedlineToolbar } from "@/components/ui/redline" /** The drafting agent's suggested edits to a notice clause. Nothing changes until you decide. */ export function RedlineClause() { return ( 9.2 Notice. Either party may end this lease on{" "} days' written notice delivered to the address in Schedule B . ) } ``` ## Stamp and seal A rubber-stamp impression in ink for statuses, and an embossed seal for the moment something becomes official. - Category: Marks - Docs: https://bevel-ui.vercel.app/components/stamp - Import from: `@/components/ui/stamp` ### Install ```bash npx shadcn@latest add @bevel/stamp ``` npm dependencies (installed by the CLI): `cn`. ### Anatomy ```tsx Received 17 Oct Paid ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `Stamp` | span | Inked text that soaks into the surface. tone (red, blue, green, black), shape (rect, round), animate to land with a press. | | `StampDetail` | span | A smaller second line: a date or reference. | | `Seal` | span (img) | Raised relief with text round the rim and a short centre mark. Has an accessible name. | ### Example: Statuses Stamps replace badges: Received, Refunded, Held, Auto. The seal marks something verified. ```tsx import { Seal, Stamp, StampDetail } from "@/components/ui/stamp" export function StampMarks() { return (
Received 17 Oct 2026 Refunded Held Auto Paid
) } ``` ## Redaction A black marker bar over withheld text, read as “Redacted” with the reason. Hidden text is never rendered; revealed text keeps a trace of the marker. - Category: Marks - Docs: https://bevel-ui.vercel.app/components/redaction - Import from: `@/components/ui/redaction` ### Install ```bash npx shadcn@latest add @bevel/redaction ``` npm dependencies (installed by the CLI): `cn`. ### Anatomy ```tsx {canSee ? ( the withheld text ) : ( )} ``` ### Parts | Part | Renders | Description | | --- | --- | --- | | `Redaction` | span | length (in characters) sizes the bar; reason is read to screen readers; revealed shows children. Only pass children to viewers allowed to read them: anything sent to the browser can be read there. | ### Example: Privilege marks A privileged memo, stamped. Switch who's reading: opposing counsel sees the redactions. ```tsx "use client" import * as React from "react" import { Redaction } from "@/components/ui/redaction" import { Stamp } from "@/components/ui/stamp" type Viewer = "firm" | "opposing" /** * A privileged memo. Our firm sees everything; opposing counsel sees the * redactions. Withheld text is only passed in for viewers allowed to read it. */ export function PrivilegeMarks() { const [viewer, setViewer] = React.useState("opposing") const canSee = viewer === "firm" return (
{( [ ["firm", "Our firm"], ["opposing", "Opposing counsel"], ] as const ).map(([value, label]) => ( ))}
Privileged Confidential

Memo: Harbor Street lease dispute

The client confirmed the notice was sent on 3 March by{" "} {canSee ? email only, not by hand : } . Our view is that{" "} {canSee ? ( the court will likely find notice invalid under clause 9.2 ) : ( )} , so we recommend settling before the hearing.

) } ```