Components
The design system's components, each with a live example and its key props.
Every component below comes from zidane-ui and paints from the theme tokens, so it matches the desktop app in every theme. To import one module alone, see Entry points.
import { buttonClass, IconButton, Menu, MenuRow, Segmented } from 'zidane-ui'Buttons#
buttonClass({ variant, size }) returns a class string, so the same look fits a <button>, an <a> or a router link.
| Option | Values | Description |
|---|---|---|
variant | 'secondary' (default), 'primary', 'solid', 'ghost', 'danger', 'danger-solid' | solid: accent fill, one per view (send, save). primary: accent wash. danger: red text, for the step that asks (Remove). danger-solid: red fill, for the destructive commit (Delete, Deny) |
size | 'xs', 'sm', 'md' (default), 'lg' | xs toolbars (24px), sm rows (28px), md card actions, form submits, dialog footers (32px), lg page-level calls to action (36px) |
Icon buttons and toggles#
IconButton is the square icon control. IconToggle is its on/off variant for toolbars, placed in a .zg-segment.
IconButton prop | Type | Description |
|---|---|---|
icon | string | ReactNode | An icon class (i-ph-gear) or a node (a SwapIcon) |
label | string | Accessible name, and title unless you pass one |
size | 'xs' | 'sm' | 'md' | 'lg' | 20, 24, 28, 32px. Default md |
active | boolean | Pressed or open state |
tone | 'neutral' | 'danger' | |
children | ReactNode | Overlays on the glyph: a badge, a dot |
render | (props) => ReactElement | Render another element (a link) with the button's props |
IconToggle takes active, onClick, icon (a class), label, badge? and size? ('xs' | 'sm', default sm).
Segments and tabs#
Segmented is a single choice on one track. TabPill is a closable tab in a .zg-segment, with a CountBadge in its trailing slot.
Segmented prop | Type | Description |
|---|---|---|
value, onChange | T, (v: T) => void | Controlled. T is a string union |
options | SegmentedOption<T>[] | { value, label, icon?, title?, tooltip?, disabled? } |
size | 'sm' | 'md' | |
ariaLabel | string | Name of the radio group |
TabPill prop | Type | Description |
|---|---|---|
label, selected | ReactNode, boolean | |
onSelect, onClose, closeLabel | () => void, () => void, string | Close button shows on hover (always with closeAlways). Delete or Backspace closes the focused tab |
leading, trailing, marker | ReactNode | An icon, a CountBadge (text, tone: 'attention' | 'info' | 'neutral'), a status mark |
size | 'sm' | 'md' |
Surfaces and empty states#
Tray is the bed. Raised is a card or control lifted off it. Both take <div> props plus radius ('sm' | 'md' | 'lg' | 'xl', default lg). Raised adds live (one step up while a run streams).
Lighter surfaces are classes: .zg-track-chip, .zg-well, .zg-fill.
| Component | Props | Description |
|---|---|---|
SectionLabel | children | The section label (sentence case) |
Meta | children, title? | Metadata voice: mono, tabular, small, muted |
EmptyState | icon?, title, hint?, action?, framed? | A panel's empty view, in a tray unless framed={false} |
PanelEmpty | icon?, title, hint?, warn? | Compact empty or error line for a side panel. PanelLoading is its loading state |
Error boundaries#
ErrorBoundary keeps a throw in one region from blanking the others: <ErrorBoundary region="Sidebar" resetKey={sessionId}>. The region shows ErrorFallback (what failed, the message, Try again) until Try again or a new resetKey. fallback replaces it (null renders nothing), actions adds buttons, framed={false} drops the tray.
Each distinct error is logged once to the console through reportRendererError(error, { source }). installRendererErrorHandlers() does the same for the window's uncaught errors and unhandled rejections. configureErrorReports({ sink, showStacks }) at boot sends every report to your log too, and shows the JS and component stacks in the fallback (a development build).
Menus and rows#
MenuRow is the row of every menu, list, picker and nav in the app. Menu gives rows their padding and role ('menu' | 'listbox' | 'group'). Put it in a .zg-float surface or a Popover. Your menu code handles the keyboard. Move active with the arrow keys.
MenuRow prop | Type | Description |
|---|---|---|
title | ReactNode | The label |
icon, description, trailing | ReactNode | Leading glyph, second line, trailing keycap or value |
size | 'sm' | 'md' | 'lg' | lg stacks the description (two lines max) |
active | boolean | Keyboard cursor |
checked | boolean | Current choice: a trailing check |
toggle | boolean | On/off option: a trailing switch (use role="menuitemcheckbox") |
destructive | boolean | Danger tone |
render | (props) => ReactElement | Render a link with the row's look. aria-current="page" marks the current page |
MenuSection (label, trailing) groups rows and MenuSeparator divides them. MenuNote (icon) is a hint or empty state inside a menu. MenuSwitch (on) is the switch glyph alone.
Floating surfaces#
Floating surfaces use the overlay tone, position against their anchor (flipping and clamping to the viewport) and close on Escape or an outside click.
| Component | Key props | Description |
|---|---|---|
Popover | trigger(props, open), children(close), ariaLabel, side, align, width, role | Opens on click. Spread props on your trigger |
HoverCard | trigger(props, open), children, ariaLabel, side, align, width | Opens on hover. Escape closes it |
Tooltip | label, side ('top' | 'bottom'), align ('center' | 'end'), portal, maxWidth | Wraps its trigger. portal escapes a clipping scroller |
RowActionsMenu | open, onClose, items, anchorRef or position | The 3-dots and right-click menu |
side: 'top' | 'bottom' | 'left' | 'right'. align: 'start' | 'center' | 'end'. RowActionsMenu items: { id, label, onSelect, destructive?, disabled?, shortcut? }.
To build your own, use the parts of Popover: FloatingSurface, usePopover and useAnchoredPosition.
Dialogs and overlays#
Overlay owns the layer: portal, scrim, enter and exit, Escape, focus return, scroll lock. Put the state it passes on your surface's data-state. ConfirmDialog is a confirmation surface for it that focuses its confirm button on mount.
Overlay prop | Type | Description |
|---|---|---|
open, onClose | boolean, () => void | |
children | (state: 'open' | 'closed') => ReactNode | Rendered through the exit animation |
className, scrimClassName | string | Position the layer (fixed inset-0 …), style the scrim |
closeOnEscape, trapFocus | boolean, boolean | Both default to true. trapFocus moves focus in, keeps Tab inside and makes the rest of body inert. exitMs is deprecated: the exit lasts as long as its motion |
inertSelector | string | Elements made inert while open (main, footer) |
ConfirmDialog prop | Type | Description |
|---|---|---|
title, message | string, ReactNode | The title has id CONFIRM_TITLE_ID, for your layer's aria-labelledby |
onConfirm, onCancel | () => void | |
confirmLabel, cancelLabel | string | Default to the confirm and cancel labels |
destructive | boolean | Danger-toned confirm button |
useLightbox() returns { open(items, index, origin?), close, element }. Render element once. Call open with { key, name, src?, body? } items to page through images or text previews. The image grows out of the origin element.
An image in the lightbox shows in ZoomableImage, the kit's image viewer, which you can also use on its own. It fills its parent. The wheel and a trackpad pinch zoom toward the pointer, from fit up to 8× the image's real size. A drag pans while zoomed in, and a double-click switches between fit and the real size. While zoomed, a chip shows the share of the real size; click it to go back to fit. Right-click the image to copy it as an image (PNG), as Base64 or as a data URL. Its ref (ZoomableImageHandle) has zoomIn, zoomOut, reset and zoomed, for an owner that handles the keys: the lightbox maps + / − / 0 to them, and the arrow keys still step. MediaNav draws the lightbox's ‹ › chips (and an optional "n / N" chip) over any box, for your own media sets.
Code, copy and keys#
const agent = createAgent({ ...basic, provider })
await agent.run({ prompt: 'build a REST API' })cat packages/server/src/handlers/very-long-folder-name/another-nested-directory/session-title-generator.ts | head -n 200 # a long line scrolls inside the frame
$ bun test 41 passed
| Component | Key props | Description |
|---|---|---|
CodeFrame | code, children, caption?, icon?, copyLabel? | Code panel with a copy button. code is the copied text. children is the highlighted markup, with no background of its own: the frame draws the code's tone and its edge, and a background would cover that edge |
CodePanel | children, wrap?, maxHeight? | The bare panel, for raw output |
CopyButton | text, label?, variant? ('overlay' | 'inline') | Copy with a check swap |
KeyCombo | spec, size? ('sm' | 'md') | One chord from a spec (cmd+shift+enter), with platform glyphs |
Keycap | children | A single key |
Forms#
The desktop app's settings layout: sections of rows, each row a label, an optional description and one control.
| Component | Key props | Description |
|---|---|---|
FormSection | title?, trailing?, layout? ('rows' | 'cards') | A titled group of rows or cards |
FormCard | <div> props | A raised card in a cards section |
SwitchRow | label, description?, on, onToggle | The whole row toggles |
SegmentedRow | label, description?, value, onChange, options | A Segmented on the right |
SliderRow | label, description?, value, min, max, step, format, onChange, onReset? | |
NavRow | action: { label, description, icon, onSelect } | Opens another page |
RowText | label, description? | The label column, for your own rows |
ToggleSwitch | on, onChange, label, disabled? | Standalone role="switch" |
Switch, Checkbox | on / checked | Glyphs only. The surrounding control owns the state |
MetaChip | tone? (muted, text, accent, success, warn, danger, info) | A value set into a surface |
ErrorNote, OpLine | children. OpLine adds kind ('busy' | 'notice' | 'error') | Inline outcome lines |
For your own fields: INPUT_CLASS and TEXTAREA_CLASS.
List furniture#
| Component | Key props | Description |
|---|---|---|
Breadcrumb | path, onFolder(dir), onFile() | File path that truncates from the left |
EditableTitle | text, editing, onStartEdit, onChange, onCommit, onCancel | Rename in place. Enter commits, Escape cancels |
DiffCounts | added, removed | +14 −2 |
RowBadge | text, tone? ('neutral' | 'diff' | 'success' | 'warn') | Small badge after a row title |
Highlighted | text, indices | Fuzzy-match highlighting |
SegmentSpinner | size? ('xs' | 'sm' | 'md'), tone? ('inherit' | 'accent' | 'warn' | 'muted'), label? | Working indicator |
ZidaneMark | size? (px), speed? (loops per second, 0 still), tone? ('inherit' | 'accent' | 'warn' | 'muted'), label?, decorative? | The logo as the agent's working indicator: its bands flow across the sphere; still under reduced motion |
Motion#
Helpers for a status glyph swapping, a count rolling, a chip growing. All hold still when html[data-motion] is off or reduced (motion switch).
| Helper | Key props | Description |
|---|---|---|
Crossfade | activeKey, children, variant? ('scale' | 'text'), align? | Swaps content when activeKey changes |
Presence | show, children, variant?, display? ('inline' | 'block') | Fades and scales content in and out |
SwapIcon | icons (key → icon class), current | Swaps glyphs (copy → check) |
RollingText | text, value? | Rolls changed characters. value sets the direction |
SmoothWidth | children | Eases its width to its content's |
HeldWidth | holdKey, children | Keeps its widest width until holdKey changes |
Grow | appear, as?, children | Grows in and shrinks out, inside GrowPresence |
useReducedMotion() | True when motion is off or reduced. Skip your own JS animation then |
Lower-level motion modules, also exported from zidane-ui, for your own components:
| Module | Exports | Use |
|---|---|---|
motion/presence | usePresence(open, { enter?, exitMs? }), transitionState, useScrollLock, readPanelMs, readExitMs | Keeps a node mounted through its CSS exit. The phase goes entering, open, closing, closed. Map it to data-state with transitionState. Pass exitMs as readPanelMs or readExitMs so the motion setting applies |
motion/collapse | useStaysMounted(open) | True while open, or while the close still plays |
motion/swap | useCadencedValue(value, ms) | Holds a fast-changing value to one change per ms |
motion/shimmer | syncShimmer (a ref callback), SHIMMER_MS | Starts a .zg-shimmer label's sweep on a shared clock, so a swapped label keeps the band where it was |
motion/title-text | TitleText | A title that writes itself in word by word when it changes |
motion/springs | useMotionTransition(name), useWithFade, useMenuTransition, MOTIONS | The named curves that every animation reads (snap, pop, panel, slide, settle, exit, fade, fade-out, menu, menu-exit) as motion transitions. Movement springs. Opacity, color and the menus never spring |
motion/provider | MotionProvider (level?, springs?, panelMs?) | One motion policy for an app: writes the motion variables on <html> and wraps the tree in motion's MotionConfig |
The springs and provider modules import the motion peer, as do Grow, Overlay, FloatingSurface and Lightbox.
Hooks#
From zidane-ui:
| Hook | Returns | Use |
|---|---|---|
usePersistedState(key, fallback, parse, serialize?) | [value, setValue] | useState kept in localStorage: a stored value parse doesn't accept falls back, null removes the key, storage that throws is ignored. readPersisted / writePersisted are the same outside React; oneOf(members) and parseBoolean are ready-made parses |
useMinuteTick() | nothing | Re-renders once a minute for relative times ("5m"), on one shared interval that runs only while read and the window is visible |
useKeyedValue(store, key) | the value or null | Reads one key of a createKeyedStore(): a write to one key re-renders only that key's readers |
useNearViewport(ref, margin?) | boolean | Turns true once the element comes within margin of its scroll container's viewport, and stays true. Use it for lazy work such as code highlighting. It shares one IntersectionObserver per scroll root and margin. Where IntersectionObserver is missing, it is true at once |
useResizeDrag({ value, min, max, defaultValue, edge, axis?, target, onCommit }) | { handleProps, active } | Resizes a panel by dragging its edge. Each frame writes the width into target's CSS variable, with no React state. onCommit fires once on release. A double-click resets to defaultValue. edge is end for a panel on the left, start for one on the right. axis: 'y' resizes a height |
import type { CSSProperties } from 'react'
import { useResizeDrag } from 'zidane-ui'
import { useRef, useState } from 'react'
export function Sidebar() {
const el = useRef<HTMLDivElement>(null)
const [width, setWidth] = useState(260)
const drag = useResizeDrag({ value: width, min: 200, max: 480, defaultValue: 260, edge: 'end', target: { ref: el, cssVar: '--sidebar-w' }, onCommit: setWidth })
return (
<div ref={el} style={{ '--sidebar-w': `${width}px`, width: 'var(--sidebar-w)' } as CSSProperties}>
<div {...drag.handleProps} data-active={drag.active} className="zg-resize-handle" />
</div>
)
}Path helpers#
From zidane-ui, browser-safe (no node:path): basename(path), splitPath(path) ({ dir, base }), compactPath(path, maxChars?) (/…/agent/zidane: the start collapses, the last segments stay), formatPathForCwd(path, projectRoot, cwd) (relative below cwd, absolute above it, never ../), tildePath(path, home) (/Users/ada/app → ~/app) and tildeText(text, home) (the same for each path in a line, such as in /Users/ada/app).
The kit's components show paths with their machine's home folder as ~ when you give them the home folders. Wrap the app in PathDisplayProvider homes={{ local: '/Users/ada', [serverId]: '/home/ada' }}. Keys are host ids (HostView.id, 'local' for this computer). A component asks useDisplayPath().path(path, hostId), and PathDisplayScope hostId names the host for everything below it (one session's transcript). This is display only: tooltips, copies and what goes to the agent keep the real path. Without a provider, paths show in full. A zidane host reports its home folder in its ipc.hello (homeDir).
Reference#
components/button, components/IconButton, components/Menu, components/Surface, components/Segmented, components/TabPill, components/Form, components/floating/Popover, components/ConfirmDialog, components/CodeFrame.