
Security News
Ruby's Bundler 4.0.18 Extends Cooldown to bundle lock and bundle cache
The supply chain control that delays freshly published gems now covers lockfile generation and gem vendoring in Ruby projects.
@speakai/ui
Advanced tools
Speak AI design system — React + Tailwind UI components with dark/light mode, WCAG AA, and full CSS variable customization
Speak AI's design system — 45+ React + Tailwind components with dark/light mode, WCAG AA accessibility, and full CSS variable customization.
LLM NOTE: This README is the authoritative reference for every component, prop, type, and theming rule. When generating code with
@speakai/ui, consult the prop tables below — do not guess props. All color styling must use CSS variable tokens (e.g.text-foreground,bg-background). Never use hardcoded Tailwind color classes (text-gray-500,bg-white,bg-zinc-900, etc.) — these break dark mode.
npm install @speakai/ui
/* globals.css */
@import "@speakai/ui/styles.css";
// tailwind.config.ts
export default {
content: [
"./src/**/*.{ts,tsx}",
"./node_modules/@speakai/ui/dist/**/*.{js,mjs}",
],
};
The library uses class-based dark mode (.dark on <html>). Works with next-themes:
// app/layout.tsx
import { ThemeProvider } from "next-themes";
export default function RootLayout({ children }) {
return (
<html suppressHydrationWarning>
<body>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<ToastProvider>{children}</ToastProvider>
</ThemeProvider>
</body>
</html>
);
}
Each component is available as a standalone sub-path export. This avoids pulling the entire barrel and significantly improves cold compilation in bundlers like Turbopack.
// Instead of: import { Button, Card } from "@speakai/ui"
import { Button } from "@speakai/ui/button";
import { Card } from "@speakai/ui/card";
import { Dialog, DialogHeader } from "@speakai/ui/dialog";
import { cn } from "@speakai/ui/cn";
The barrel import (from "@speakai/ui") still works for backward compatibility.
| Sub-path | Components |
|---|---|
@speakai/ui/accordion | Accordion, AccordionItem |
@speakai/ui/auth-card | AuthCard |
@speakai/ui/auth-divider | AuthDivider |
@speakai/ui/avatar | Avatar |
@speakai/ui/badge | Badge, StatusBadge |
@speakai/ui/bottom-sheet | BottomSheet |
@speakai/ui/breadcrumb | Breadcrumb |
@speakai/ui/button | Button |
@speakai/ui/card | Card |
@speakai/ui/checkbox | Checkbox |
@speakai/ui/chips | Chips |
@speakai/ui/cn | cn (utility) |
@speakai/ui/color-picker | ColorPicker |
@speakai/ui/confirm-dialog | ConfirmDialog |
@speakai/ui/date-picker | DatePicker |
@speakai/ui/dialog | Dialog, DialogHeader, DialogBody, DialogFooter, DialogCloseButton |
@speakai/ui/dropdown-menu | DropdownMenu, DropdownMenuItem, DropdownMenuHeader, DropdownMenuDivider, MoreButton |
@speakai/ui/empty-state | EmptyState |
@speakai/ui/error-state | ErrorState |
@speakai/ui/file-dropzone | FileDropzone |
@speakai/ui/image-uploader | ImageUploader |
@speakai/ui/info-card | InfoCard |
@speakai/ui/input | Input, SearchInput, Select, Textarea |
@speakai/ui/language-selector | LanguageSelector |
@speakai/ui/media | MediaPlayer, useMediaSync |
@speakai/ui/otp-input | OTPInput |
@speakai/ui/page-header | PageHeader, SectionHeader |
@speakai/ui/password-input | PasswordInput |
@speakai/ui/phone-input | PhoneInput |
@speakai/ui/popover | Popover |
@speakai/ui/progress | Progress |
@speakai/ui/radio-group | RadioGroup |
@speakai/ui/side-panel | SidePanel |
@speakai/ui/sidebar | Sidebar, SidebarProvider, SidebarLayout, SidebarUser, useSidebar |
@speakai/ui/skeleton | Skeleton, SkeletonText, PageHeaderSkeleton, StatCardSkeleton, StatCardsSkeletonGrid, PageSkeleton, CardSkeleton, GridSkeleton, FormSkeleton |
@speakai/ui/slider | Slider |
@speakai/ui/sso-button | SSOButton, SSOButtons |
@speakai/ui/stat-card | StatCard, StatCardGrid |
@speakai/ui/stepper | Stepper |
@speakai/ui/switch | Switch |
@speakai/ui/table | Table, TableHeader, TableBody, TableRow, TableHead, TableCell, TableSkeleton, TableActions, TableActionButton, TableSortHead, TablePagination, TableEmpty, useSort |
@speakai/ui/tabs | Tabs, TabsList, TabsTrigger, TabsContent |
@speakai/ui/theme-toggle | ThemeToggle, ThemeSelector |
@speakai/ui/time-picker | TimePicker |
@speakai/ui/toast | ToastContainer, ToastProvider, useToast |
@speakai/ui/tooltip | Tooltip |
@speakai/ui/transcript | TranscriptView |
CRITICAL — Read before writing any JSX with this library.
All color and shape tokens are CSS variables. They flip automatically between light and dark mode.
:root {
/* Brand */
--primary: 271 91% 65%; /* purple */
--primary-foreground: 0 0% 100%;
/* Gradient */
--gradient-from: 271 91% 65%; /* purple */
--gradient-to: 330 81% 60%; /* pink */
--gradient-accent: 24 95% 53%; /* orange */
/* Status */
--danger: 0 84% 60%;
--danger-foreground: 0 0% 100%;
--success: 160 84% 39%;
--success-foreground: 0 0% 100%;
--warning: 38 92% 50%;
--warning-foreground: 0 0% 0%;
--info: 217 91% 60%;
--info-foreground: 0 0% 100%;
/* Surfaces */
--background: 0 0% 100%; /* page background */
--foreground: 222 84% 5%; /* default text */
--card: 0 0% 100%; /* card surface */
--card-foreground: 222 84% 5%;
--popover: 0 0% 100%; /* dropdown/tooltip bg */
--popover-foreground: 222 84% 5%;
--secondary: 210 40% 96%;
--secondary-foreground: 222 47% 11%;
--muted: 210 40% 96%; /* subtle backgrounds */
--muted-foreground: 215 16% 47%; /* placeholder / help text */
--accent: 210 40% 96%; /* hover highlight */
--accent-foreground: 222 47% 11%;
--border: 214 32% 91%; /* borders */
--input: 214 32% 91%; /* input borders */
--ring: 271 91% 65%; /* focus ring */
/* Skeleton */
--skeleton-bg: 220 14% 91%;
--skeleton-highlight: 220 14% 96%;
/* Typography */
--font-sans: "Inter", sans-serif;
--font-mono: "JetBrains Mono", monospace;
/* Shape */
--radius: 0.75rem; /* border-radius base */
}
| Token | Tailwind class |
|---|---|
--background | bg-background / text-background |
--foreground | text-foreground |
--primary | bg-primary / text-primary / border-primary |
--primary-foreground | text-primary-foreground |
--card | bg-card |
--card-foreground | text-card-foreground |
--popover | bg-popover |
--popover-foreground | text-popover-foreground |
--muted | bg-muted |
--muted-foreground | text-muted-foreground |
--accent | bg-accent |
--accent-foreground | text-accent-foreground |
--border | border-border |
--input | border-input |
--ring | ring-ring |
--danger | text-danger / bg-danger / border-danger |
--success | text-success / bg-success |
--warning | text-warning / bg-warning |
--info | text-info / bg-info |
// ❌ WRONG — hardcoded colors break dark mode
<div className="bg-white text-gray-900 border-gray-200">
<p className="text-gray-500">Help text</p>
<button className="bg-zinc-800 text-white">Submit</button>
// ✅ CORRECT — CSS variables flip automatically
<div className="bg-background text-foreground border-border">
<p className="text-muted-foreground">Help text</p>
<button className="bg-foreground text-background">Submit</button>
:root {
--primary: 210 80% 50%; /* your brand color (HSL) */
--gradient-from: 210 80% 50%;
--gradient-to: 170 80% 50%;
--radius: 0.5rem; /* sharper corners */
--font-sans: "Geist", sans-serif;
}
Sub-path imports (recommended) — each import pulls only that component's code:
// Auth
import { AuthCard } from "@speakai/ui/auth-card";
import { SSOButton, SSOButtons } from "@speakai/ui/sso-button";
import { PasswordInput } from "@speakai/ui/password-input";
import { OTPInput } from "@speakai/ui/otp-input";
import { AuthDivider } from "@speakai/ui/auth-divider";
// Forms
import { Button } from "@speakai/ui/button";
import { Input, SearchInput, Select, Textarea } from "@speakai/ui/input";
import { Switch } from "@speakai/ui/switch";
import { Checkbox } from "@speakai/ui/checkbox";
import { RadioGroup } from "@speakai/ui/radio-group";
import { ColorPicker } from "@speakai/ui/color-picker";
import { ImageUploader } from "@speakai/ui/image-uploader";
import { FileDropzone } from "@speakai/ui/file-dropzone";
import { Chips } from "@speakai/ui/chips";
import { Slider } from "@speakai/ui/slider";
// Layout
import { Card } from "@speakai/ui/card";
import { Sidebar, SidebarProvider, SidebarLayout, SidebarUser } from "@speakai/ui/sidebar";
import { SidePanel } from "@speakai/ui/side-panel";
import { PageHeader, SectionHeader } from "@speakai/ui/page-header";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@speakai/ui/tabs";
import { Accordion, AccordionItem } from "@speakai/ui/accordion";
import { Stepper } from "@speakai/ui/stepper";
// Data Display
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell, TableSortHead, TableActions, TableActionButton, TableEmpty, TablePagination, TableSkeleton } from "@speakai/ui/table";
import { StatCard, StatCardGrid } from "@speakai/ui/stat-card";
import { Badge, StatusBadge } from "@speakai/ui/badge";
import { InfoCard } from "@speakai/ui/info-card";
import { Breadcrumb } from "@speakai/ui/breadcrumb";
// Feedback
import { ToastProvider, useToast } from "@speakai/ui/toast";
import { Dialog, DialogHeader, DialogBody, DialogFooter, DialogCloseButton } from "@speakai/ui/dialog";
import { ConfirmDialog } from "@speakai/ui/confirm-dialog";
import { BottomSheet } from "@speakai/ui/bottom-sheet";
import { EmptyState } from "@speakai/ui/empty-state";
import { ErrorState } from "@speakai/ui/error-state";
// Skeleton
import { Skeleton, SkeletonText, PageHeaderSkeleton, StatCardSkeleton, StatCardsSkeletonGrid, PageSkeleton, CardSkeleton, GridSkeleton, FormSkeleton } from "@speakai/ui/skeleton";
// Selectors
import { LanguageSelector } from "@speakai/ui/language-selector";
import { PhoneInput } from "@speakai/ui/phone-input";
import { DatePicker } from "@speakai/ui/date-picker";
import { TimePicker } from "@speakai/ui/time-picker";
// Utilities
import { Tooltip } from "@speakai/ui/tooltip";
import { Popover } from "@speakai/ui/popover";
import { DropdownMenu, DropdownMenuItem, DropdownMenuDivider, DropdownMenuHeader, MoreButton } from "@speakai/ui/dropdown-menu";
import { Avatar } from "@speakai/ui/avatar";
import { Progress } from "@speakai/ui/progress";
import { ThemeToggle, ThemeSelector } from "@speakai/ui/theme-toggle";
import { cn } from "@speakai/ui/cn";
// Media (separate entry — avoids prosemirror dependency)
import { MediaPlayer, useMediaSync } from "@speakai/ui/media";
import {
Button, Card, Badge, Input, Table, Dialog, // ... all 75+ exports
} from "@speakai/ui";
All components support the className prop for Tailwind overrides unless noted. All accept ref via forwardRef.
AuthCardCentered card layout for login, register, and verify pages.
<AuthCard logo={<Logo />} title="Welcome back" subtitle="Sign in to continue">
<Input placeholder="Email" />
<Button variant="primary">Sign In</Button>
</AuthCard>
| Prop | Type | Default | Required |
|---|---|---|---|
title | string | — | Yes |
subtitle | string | — | No |
logo | ReactNode | — | No |
children | ReactNode | — | No |
className | string | — | No |
SSOButtonSingle SSO provider button with built-in icon.
<SSOButton provider="google" action="login" onClick={handleGoogle} />
| Prop | Type | Default | Required |
|---|---|---|---|
provider | "google" | "apple" | "microsoft" | — | Yes |
action | "login" | "register" | "login" | No |
isLoading | boolean | false | No |
disabled | boolean | — | No |
SSOButtonsRenders all three SSO buttons (Google, Apple, Microsoft) with a shared click handler.
<SSOButtons action="register" onProviderClick={(p) => handleSSO(p)} />
| Prop | Type | Default | Required |
|---|---|---|---|
action | "login" | "register" | "login" | No |
onProviderClick | (provider: "google" | "apple" | "microsoft") => void | — | No |
isLoading | boolean | false | No |
disabled | boolean | — | No |
className | string | — | No |
PasswordInputInput with show/hide password toggle.
<PasswordInput
label="Password"
placeholder="Enter password"
error="Password is too short"
/>
| Prop | Type | Default | Required |
|---|---|---|---|
error | boolean | string | — | No |
label | string | — | No |
disabled | boolean | — | No |
Accepts all native <input> props.
OTPInputMulti-digit code input with auto-focus advance and paste support.
<OTPInput length={6} value={code} onChange={setCode} autoFocus />
<OTPInput length={4} value={code} onChange={setCode} error="Invalid code" />
| Prop | Type | Default | Required |
|---|---|---|---|
value | string | — | Yes |
onChange | (value: string) => void | — | Yes |
length | number | 6 | No |
error | boolean | string | — | No |
disabled | boolean | false | No |
autoFocus | boolean | false | No |
AuthDividerHorizontal rule with centered label for separating auth options.
<AuthDivider /> {/* renders "or" */}
<AuthDivider text="continue with" />
| Prop | Type | Default | Required |
|---|---|---|---|
text | string | "or" | No |
className | string | — | No |
ButtonPrimary action element. Extend all native <button> props.
<Button variant="primary" size="md">Save Changes</Button>
<Button variant="gradient" isLoading={saving}>Submit</Button>
<Button variant="danger" size="sm">Delete</Button>
<Button variant="ghost" size="icon"><TrashIcon /></Button>
| Prop | Type | Default | Required |
|---|---|---|---|
variant | "primary" | "secondary" | "danger" | "ghost" | "outline" | "gradient" | "glass" | "solid" | "primary" | No |
size | "sm" | "md" | "lg" | "icon" | "md" | No |
isLoading | boolean | false | No |
disabled | boolean | — | No |
children | ReactNode | — | No |
Size reference: sm = h-9, md = h-10, lg = h-11, icon = h-10 w-10
InputStandard text input. Extends all native <input> props.
<Input placeholder="Email address" />
<Input error={true} /> {/* red border only */}
<Input error="Email is required" /> {/* red border + message */}
<Input label="Full name" value={name} onChange={e => setName(e.target.value)} />
| Prop | Type | Default | Required |
|---|---|---|---|
error | boolean | string | — | No |
label | string | — | No |
disabled | boolean | — | No |
SearchInputInput pre-configured with a search icon on the left.
<SearchInput
placeholder="Search agents..."
value={query}
onChange={e => setQuery(e.target.value)}
containerClassName="w-64"
/>
| Prop | Type | Default | Required |
|---|---|---|---|
containerClassName | string | — | No |
Accepts all native <input> props.
SelectDropdown selector. Supports both options array and children syntax.
{/* Options array */}
<Select
options={[{ value: "en", label: "English" }, { value: "es", label: "Spanish" }]}
placeholder="Select language"
value={lang}
onChange={e => setLang(e.target.value)}
/>
{/* Children syntax */}
<Select value={role} onChange={e => setRole(e.target.value)}>
<option value="admin">Admin</option>
<option value="member">Member</option>
</Select>
| Prop | Type | Default | Required |
|---|---|---|---|
options | Array<{ value: string; label: string }> | — | No |
placeholder | string | — | No |
error | boolean | string | — | No |
label | string | — | No |
disabled | boolean | — | No |
children | ReactNode | — | No |
TextareaMulti-line text input. Extends all native <textarea> props.
<Textarea placeholder="Describe your agent..." rows={4} />
<Textarea error="Description is required" label="Bio" />
| Prop | Type | Default | Required |
|---|---|---|---|
error | boolean | string | — | No |
label | string | — | No |
rows | number | — | No |
disabled | boolean | — | No |
SwitchToggle control.
<Switch checked={enabled} onChange={setEnabled} label="Enable notifications" />
<Switch checked={active} onChange={setActive} size="sm" />
| Prop | Type | Default | Required |
|---|---|---|---|
checked | boolean | — | Yes |
onChange | (checked: boolean) => void | — | Yes |
size | "sm" | "default" | "default" | No |
label | string | — | No |
disabled | boolean | false | No |
id | string | — | No |
CheckboxCheckbox with optional label and description.
<Checkbox
checked={agreed}
onChange={e => setAgreed(e.target.checked)}
label="I agree to the terms"
description="By checking this box you agree..."
/>
<Checkbox size="sm" error="You must agree to continue" />
| Prop | Type | Default | Required |
|---|---|---|---|
checked | boolean | — | No |
onChange | (e: ChangeEvent<HTMLInputElement>) => void | — | No |
label | string | — | No |
description | string | — | No |
size | "sm" | "default" | "lg" | "default" | No |
error | boolean | string | — | No |
disabled | boolean | — | No |
id | string | — | No |
RadioGroupGroup of radio buttons with keyboard navigation. Supports default (flat list) and card (bordered selection) variants.
{/* Default — flat list */}
<RadioGroup
value={plan}
onChange={setPlan}
options={[
{ value: "free", label: "Free", description: "For personal use" },
{ value: "pro", label: "Pro", description: "For teams" },
{ value: "enterprise", label: "Enterprise", disabled: true },
]}
/>
{/* Card variant — bordered containers with selection highlighting */}
<RadioGroup
variant="card"
value={timing}
onChange={setTiming}
options={[
{ value: "start", label: "At Start", description: "Collect before conversation begins" },
{ value: "during", label: "During", description: "Collect naturally during conversation" },
{ value: "end", label: "At End", description: "Collect before ending" },
]}
/>
| Prop | Type | Default | Required |
|---|---|---|---|
value | string | — | Yes |
onChange | (value: string) => void | — | Yes |
options | RadioOption[] | — | Yes |
orientation | "horizontal" | "vertical" | "vertical" | No |
variant | "default" | "card" | "default" | No |
name | string | — | No |
disabled | boolean | false | No |
RadioOption interface:
interface RadioOption {
value: string;
label: string;
description?: string;
disabled?: boolean;
}
Card variant: Each option renders inside a bordered container. Selected option gets
border-primary bg-primary/5. Unselected options getborder-borderwith hover effect. Use for settings/config where each option needs visual separation.
ColorPickerNative color picker with hex input and optional preset swatches.
<ColorPicker
value={color}
onChange={setColor}
label="Brand color"
presetColors={["#a855f7", "#3b82f6", "#ef4444", "#10b981"]}
showInput={true}
/>
| Prop | Type | Default | Required |
|---|---|---|---|
value | string | — | Yes |
onChange | (color: string) => void | — | Yes |
presetColors | string[] | — | No |
showInput | boolean | true | No |
label | string | — | No |
disabled | boolean | false | No |
ImageUploaderDrag-and-drop image upload with preview and remove button.
<ImageUploader
value={imageUrl}
onChange={(file) => uploadAndSetUrl(file)}
onRemove={() => setImageUrl(undefined)}
maxSize={5 * 1024 * 1024} // 5 MB
variant="dropzone"
/>
<ImageUploader variant="avatar" value={avatarUrl} onChange={handleUpload} />
| Prop | Type | Default | Required |
|---|---|---|---|
onChange | (file: File) => void | — | Yes |
value | string | — | No |
onRemove | () => void | — | No |
accept | string | "image/*" | No |
maxSize | number (bytes) | 10485760 (10 MB) | No |
placeholder | string | "Drop an image here or click to browse" | No |
disabled | boolean | false | No |
variant | "dropzone" | "avatar" | "dropzone" | No |
FileDropzoneMulti-file drag-and-drop upload zone.
<FileDropzone
onFiles={(files) => handleUpload(files)}
accept=".pdf,.doc,.docx"
multiple={true}
maxSize={20 * 1024 * 1024}
/>
| Prop | Type | Default | Required |
|---|---|---|---|
onFiles | (files: File[]) => void | — | Yes |
accept | string | — | No |
multiple | boolean | true | No |
maxSize | number (bytes) | — | No |
disabled | boolean | false | No |
children | ReactNode | — | No |
ChipsTag/chip input with autocomplete dropdown.
<Chips
value={tags}
onChange={setTags}
placeholder="Add tags..."
suggestions={["react", "typescript", "tailwind", "nextjs"]}
maxItems={10}
/>
| Prop | Type | Default | Required |
|---|---|---|---|
value | string[] | — | Yes |
onChange | (value: string[]) => void | — | Yes |
placeholder | string | "Type and press Enter" | No |
suggestions | string[] | — | No |
maxItems | number | — | No |
disabled | boolean | false | No |
Keyboard: Enter/comma to add, Backspace to remove last, Escape to close suggestions.
SliderRange input with optional label and live value display. Supports inline mode for compact layouts and track overlays for media timelines.
{/* Full-width with label */}
<Slider value={volume} onChange={setVolume} min={0} max={100} label="Volume" />
{/* Without value display */}
<Slider value={temperature} onChange={setTemperature} step={0.1} min={0} max={2} showValue={false} />
{/* Inline — compact, no header, for use in flex rows */}
<div className="flex items-center gap-2">
<span>Weight: {weight}</span>
<Slider inline value={weight} onChange={setWeight} min={1} max={10} showValue={false} className="w-20" />
</div>
{/* With track overlay content (markers, annotations) */}
<Slider
value={position}
onChange={setPosition}
min={0}
max={duration}
trackChildren={
<>
{markers.map(m => (
<div key={m.time} className="absolute top-0 h-full w-0.5 bg-primary" style={{ left: `${(m.time / duration) * 100}%` }} />
))}
</>
}
/>
| Prop | Type | Default | Required |
|---|---|---|---|
value | number | — | Yes |
onChange | (value: number) => void | — | Yes |
min | number | 0 | No |
max | number | 100 | No |
step | number | 1 | No |
label | string | — | No |
showValue | boolean | true | No |
disabled | boolean | false | No |
inline | boolean | false | No |
trackChildren | ReactNode | — | No |
Inline mode: Renders as
inline-flexwithout the label/value header row. The track fills the container width. Set a fixed width viaclassName="w-20"for compact layouts.Track children: Content rendered inside the track
<div>(positioned relative). Use absolute positioning for markers, annotations, or segment indicators.
CardSurface container with multiple visual styles.
<Card variant="default">Content</Card>
<Card variant="elevated" showGradientAccent>Featured card</Card>
<Card variant="glass">Glassmorphism effect</Card>
| Prop | Type | Default | Required |
|---|---|---|---|
variant | "default" | "outline" | "elevated" | "glass" | "default" | No |
showGradientAccent | boolean | false | No |
children | ReactNode | — | No |
SidebarProvider + Sidebar + SidebarLayoutCollapsible sidebar with mobile drawer support.
// Wrap the entire layout
<SidebarProvider>
<div className="flex h-screen">
<Sidebar
sections={[
{
id: "main",
items: [
{ id: "dashboard", label: "Dashboard", icon: <HomeIcon />, href: "/", active: true },
{ id: "agents", label: "Agents", icon: <BotIcon />, href: "/agents" },
],
},
]}
header={<Logo />}
footer={<SidebarUser name="John Doe" email="john@example.com" />}
renderLink={({ href, children, className }) => (
<Link href={href} className={className}>{children}</Link>
)}
/>
<SidebarLayout>
<main>{/* page content */}</main>
</SidebarLayout>
</div>
</SidebarProvider>
SidebarProvider props:
| Prop | Type | Default | Required |
|---|---|---|---|
children | ReactNode | — | Yes |
defaultCollapsed | boolean | false | No |
storageKey | string | "sidebar-collapsed" | No |
Sidebar props:
| Prop | Type | Default | Required |
|---|---|---|---|
sections | SidebarSection[] | — | Yes |
header | ReactNode | — | No |
footer | ReactNode | — | No |
renderLink | (props: { href: string; children: ReactNode; className?: string }) => ReactNode | — | No |
className | string | — | No |
SidebarSection interface:
interface SidebarSection {
id: string;
label?: string;
items: SidebarItem[];
}
SidebarItem interface:
interface SidebarItem {
id: string;
label: string;
icon?: ReactNode;
href?: string;
onClick?: () => void;
badge?: ReactNode;
active?: boolean;
disabled?: boolean;
children?: SidebarItem[]; // nested items
}
SidebarUser props:
| Prop | Type | Default | Required |
|---|---|---|---|
name | string | — | Yes |
email | string | — | No |
avatar | ReactNode | — | No |
actions | ReactNode | — | No |
SidebarLayout props:
| Prop | Type | Default | Required |
|---|---|---|---|
children | ReactNode | — | Yes |
className | string | — | No |
SidePanelSlide-over drawer from left or right edge.
<SidePanel open={open} onClose={() => setOpen(false)} title="Edit Agent" size="lg">
<form>...</form>
</SidePanel>
<SidePanel
open={open}
onClose={close}
title="Step 2"
showBack
onBack={goBack}
side="right"
footer={<div className="flex gap-2"><Button>Save</Button></div>}
>
...
</SidePanel>
| Prop | Type | Default | Required |
|---|---|---|---|
open | boolean | — | Yes |
onClose | () => void | — | Yes |
title | string | — | No |
side | "left" | "right" | "right" | No |
size | "sm" | "default" | "lg" | "xl" | "full" | "default" | No |
showBack | boolean | false | No |
onBack | () => void | — | No |
backdrop | boolean | true | No |
header | ReactNode | — | No |
footer | ReactNode | — | No |
children | ReactNode | — | No |
PageHeaderPage-level heading with optional gradient text accent and action slot.
<PageHeader
title="Voice Agents"
gradientText="AI-Powered"
description="Build and manage your AI voice agents."
action={<Button variant="primary">Create Agent</Button>}
/>
| Prop | Type | Default | Required |
|---|---|---|---|
title | string | — | Yes |
gradientText | string | — | No |
description | string | — | No |
action | ReactNode | — | No |
SectionHeaderSection-level heading, smaller than PageHeader.
<SectionHeader
title="Team Members"
description="Manage who has access."
action={<Button size="sm">Invite</Button>}
/>
| Prop | Type | Default | Required |
|---|---|---|---|
title | string | — | Yes |
description | string | — | No |
action | ReactNode | — | No |
Tabs + TabsList + TabsTrigger + TabsContentTabbed navigation with keyboard arrow support.
<Tabs defaultTab="overview" onTabChange={setTab}>
<TabsList variant="underline">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="settings" icon={<SettingsIcon />}>Settings</TabsTrigger>
<TabsTrigger value="logs" badge={<Badge variant="info">12</Badge>}>Logs</TabsTrigger>
</TabsList>
<TabsContent value="overview"><OverviewPanel /></TabsContent>
<TabsContent value="settings"><SettingsPanel /></TabsContent>
<TabsContent value="logs"><LogsPanel /></TabsContent>
</Tabs>
Tabs props:
| Prop | Type | Default | Required |
|---|---|---|---|
defaultTab | string | — | Yes |
onTabChange | (tab: string) => void | — | No |
children | ReactNode | — | No |
TabsList props:
| Prop | Type | Default | Required |
|---|---|---|---|
variant | "default" | "underline" | "pills" | "default" | No |
children | ReactNode | — | No |
TabsTrigger props:
| Prop | Type | Default | Required |
|---|---|---|---|
value | string | — | Yes |
variant | "default" | "underline" | "pills" | "default" | No |
icon | ReactNode | — | No |
badge | ReactNode | — | No |
disabled | boolean | false | No |
children | ReactNode | — | No |
TabsContent props:
| Prop | Type | Default | Required |
|---|---|---|---|
value | string | — | Yes |
children | ReactNode | — | No |
Accordion + AccordionItemExpandable content sections.
<Accordion type="single">
<AccordionItem title="What is a voice agent?" value="q1">
A voice agent is an AI-powered...
</AccordionItem>
<AccordionItem title="How does billing work?" value="q2">
Billing is monthly...
</AccordionItem>
</Accordion>
<Accordion type="multiple" defaultOpen={["q1"]}>
...
</Accordion>
Accordion props:
| Prop | Type | Default | Required |
|---|---|---|---|
type | "single" | "multiple" | "single" | No |
defaultOpen | string[] | [] | No |
children | ReactNode | — | No |
AccordionItem props:
| Prop | Type | Default | Required |
|---|---|---|---|
title | string | — | Yes |
children | ReactNode | — | Yes |
value | string | — | No |
defaultOpen | boolean | — | No |
disabled | boolean | false | No |
StepperStep progress indicator.
<Stepper
currentStep={1}
orientation="horizontal"
steps={[
{ label: "Account", description: "Basic info" },
{ label: "Profile", description: "Customize" },
{ label: "Done" },
]}
/>
| Prop | Type | Default | Required |
|---|---|---|---|
steps | StepperStep[] | — | Yes |
currentStep | number (0-indexed) | — | Yes |
orientation | "horizontal" | "vertical" | "horizontal" | No |
StepperStep interface:
interface StepperStep {
label: string;
description?: string;
}
Table systemFull table system with sorting, pagination, actions, and empty states.
<Table scrollable>
<TableHeader>
<TableRow>
<TableSortHead sortKey="name" activeSort={sort} direction={dir} onSort={handleSort}>Name</TableSortHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items.map(item => (
<TableRow key={item.id} clickable onClick={() => openItem(item)}>
<TableCell>{item.name}</TableCell>
<TableCell><Badge variant="success">Active</Badge></TableCell>
<TableCell>
<TableActions>
<TableActionButton label="Edit" onClick={() => edit(item)}>
<EditIcon />
</TableActionButton>
<TableActionButton label="Delete" variant="danger" onClick={() => del(item)}>
<TrashIcon />
</TableActionButton>
</TableActions>
</TableCell>
</TableRow>
))}
{items.length === 0 && (
<TableEmpty colSpan={3} title="No agents yet" description="Create your first agent." />
)}
</TableBody>
</Table>
<TablePagination page={page} pageSize={25} total={total} onPageChange={setPage} />
Table props:
| Prop | Type | Default | Required |
|---|---|---|---|
scrollable | boolean | true | No |
children | ReactNode | — | No |
TableRow props:
| Prop | Type | Default | Required |
|---|---|---|---|
clickable | boolean | false | No |
onClick | (e: MouseEvent) => void | — | No |
children | ReactNode | — | No |
TableSortHead props:
| Prop | Type | Default | Required |
|---|---|---|---|
sortKey | string | — | Yes |
activeSort | string | null | — | No |
direction | "asc" | "desc" | null | — | No |
onSort | (key: string, direction: "asc" | "desc") => void | — | No |
children | ReactNode | — | No |
TableActionButton props:
| Prop | Type | Default | Required |
|---|---|---|---|
label | string (aria-label) | — | Yes |
variant | "default" | "danger" | "default" | No |
children | ReactNode | — | No |
TableEmpty props:
| Prop | Type | Default | Required |
|---|---|---|---|
colSpan | number | — | Yes |
title | string | "No results" | No |
description | string | — | No |
icon | ReactNode | — | No |
action | ReactNode | — | No |
TablePagination props:
| Prop | Type | Default | Required |
|---|---|---|---|
page | number | — | Yes |
pageSize | number | — | Yes |
total | number | — | Yes |
onPageChange | (page: number) => void | — | Yes |
onPageSizeChange | (size: number) => void | — | No |
pageSizeOptions | number[] | [10, 25, 50, 100] | No |
TableSkeleton props:
| Prop | Type | Default | Required |
|---|---|---|---|
columns | number | 4 | No |
rows | number | 5 | No |
StatCard + StatCardGridDashboard stat tiles.
<StatCardGrid columns={4}>
<StatCard
label="Total Conversations"
value="1,234"
icon={<ChatIcon />}
iconColor="purple"
variant="gradient"
/>
<StatCard label="Active Agents" value={8} icon={<BotIcon />} iconColor="blue" />
</StatCardGrid>
StatCard props:
| Prop | Type | Default | Required |
|---|---|---|---|
label | string | — | Yes |
value | string | number | — | Yes |
icon | ReactNode | — | No |
iconColor | "purple" | "pink" | "blue" | "green" | "orange" | "red" | "purple" | No |
variant | "default" | "gradient" | "default" | No |
valueClassName | string | — | No |
StatCardGrid props:
| Prop | Type | Default | Required |
|---|---|---|---|
columns | 2 | 3 | 4 | 4 | No |
children | ReactNode | — | Yes |
BadgeInline status labels.
<Badge variant="success">Active</Badge>
<Badge variant="warning">Pending</Badge>
<Badge variant="error">Failed</Badge>
<Badge variant="info" size="sm">12 new</Badge>
<Badge color="purple">Custom</Badge>
| Prop | Type | Default | Required |
|---|---|---|---|
variant | "default" | "success" | "warning" | "error" | "info" | "outline" | "secondary" | "default" | No |
color | "green" | "yellow" | "red" | "blue" | "purple" | "pink" | "orange" | "gray" | — | No |
size | "sm" | "md" | "md" | No |
children | ReactNode | — | No |
StatusBadgeMaps a status string to a badge variant automatically.
<StatusBadge status="active" /> {/* green */}
<StatusBadge status="pending" /> {/* yellow */}
<StatusBadge status="failed" /> {/* red */}
| Prop | Type | Default | Required |
|---|---|---|---|
status | string | — | Yes |
InfoCardColored callout card for alerts, tips, or highlights.
<InfoCard color="purple" title="Pro Tip" description="Use structured outputs to extract data." />
<InfoCard color="red" title="Warning">Custom children content here.</InfoCard>
| Prop | Type | Default | Required |
|---|---|---|---|
color | "purple" | "blue" | "green" | "yellow" | "red" | "gray" | "blue" | No |
title | string | — | No |
description | string | — | No |
children | ReactNode | — | No |
BreadcrumbNavigation breadcrumbs with auto-truncation.
<Breadcrumb
items={[
{ label: "Home", href: "/" },
{ label: "Settings", href: "/settings" },
{ label: "Team" },
]}
separator="›"
maxItems={4}
/>
| Prop | Type | Default | Required |
|---|---|---|---|
items | BreadcrumbItem[] | — | Yes |
separator | ReactNode | "/" | No |
maxItems | number | 0 (no limit) | No |
BreadcrumbItem interface:
interface BreadcrumbItem {
label: string;
href?: string;
onClick?: () => void;
}
ToastProvider + useToastGlobal toast notification system. Wrap your app in <ToastProvider> once, then call useToast() anywhere.
// Setup (once, in root layout)
<ToastProvider>{children}</ToastProvider>
// Usage in any component
const { success, error, info, warning } = useToast();
success("Saved!", "Your changes have been saved.");
error("Failed", "Could not connect to server.");
info("Update available");
warning("Low credits", "You have fewer than 100 credits left.");
useToast methods:
| Method | Signature |
|---|---|
success | (title: string, message?: string) => void |
error | (title: string, message?: string) => void |
info | (title: string, message?: string) => void |
warning | (title: string, message?: string) => void |
DialogModal dialog with focus trap, backdrop, and 5 size options.
<Dialog open={open} onClose={() => setOpen(false)} size="lg">
<DialogHeader>
<h2 className="text-lg font-semibold text-foreground">Edit Agent</h2>
<DialogCloseButton onClose={() => setOpen(false)} />
</DialogHeader>
<DialogBody>
<Input label="Agent name" />
</DialogBody>
<DialogFooter>
<Button variant="ghost" onClick={() => setOpen(false)}>Cancel</Button>
<Button variant="primary" onClick={handleSave}>Save</Button>
</DialogFooter>
</Dialog>
Dialog props:
| Prop | Type | Default | Required |
|---|---|---|---|
open | boolean | — | Yes |
onClose | () => void | — | Yes |
size | "sm" | "default" | "lg" | "xl" | "full" | "default" | No |
children | ReactNode | — | No |
DialogCloseButton props:
| Prop | Type | Default | Required |
|---|---|---|---|
onClose | () => void | — | Yes |
ConfirmDialogPre-built confirmation modal for destructive or warning actions.
<ConfirmDialog
open={open}
onClose={() => setOpen(false)}
onConfirm={handleDelete}
title="Delete agent?"
description="This action cannot be undone."
variant="danger"
confirmLabel="Delete"
isLoading={deleting}
/>
Legacy prop aliases (for backward compat): isOpen = open, message = description, confirmText = confirmLabel, cancelText = cancelLabel, onCancel = onClose.
| Prop | Type | Default | Required |
|---|---|---|---|
open | boolean | — | Yes |
onClose | () => void | — | Yes |
onConfirm | () => void | — | Yes |
title | string | — | Yes |
description | string | — | No |
variant | "danger" | "warning" | "info" | "danger" | No |
confirmLabel | string | "Confirm" | No |
cancelLabel | string | "Cancel" | No |
isLoading | boolean | false | No |
icon | ReactNode | — | No |
BottomSheetMobile bottom sheet that slides up from the bottom of the screen. Supports drag-to-dismiss, snap points, backdrop overlay, focus trap, and body scroll lock.
const [open, setOpen] = useState(false);
<BottomSheet open={open} onClose={() => setOpen(false)}>
<div className="px-4 pb-6">
<h2 className="text-lg font-semibold text-foreground">Sheet Title</h2>
<p className="mt-2 text-sm text-muted-foreground">Sheet content here.</p>
</div>
</BottomSheet>
{/* With snap points (heights in px, ascending) */}
<BottomSheet
open={open}
onClose={() => setOpen(false)}
snapPoints={[200, 400, 600]}
>
<div className="px-4 pb-6">Snappable content</div>
</BottomSheet>
BottomSheet props:
| Prop | Type | Default | Required |
|---|---|---|---|
open | boolean | -- | Yes |
onClose | () => void | -- | Yes |
snapPoints | number[] | -- | No |
children | ReactNode | -- | Yes |
className | string | -- | No |
Behavior:
snapPoints, the sheet snaps to the nearest point on drag endrole="dialog", aria-modal)EmptyStateIllustrated empty state for lists and tables.
<EmptyState
icon={<BotIcon />}
title="No agents yet"
description="Create your first AI voice agent to get started."
action={<Button variant="primary">Create Agent</Button>}
height="lg"
/>
| Prop | Type | Default | Required |
|---|---|---|---|
title | string | — | Yes |
description | string | — | No |
icon | ReactNode | — | No |
action | ReactNode | — | No |
height | "sm" | "md" | "lg" | "md" | No |
ErrorStateError feedback in page, card, or inline variants.
<ErrorState
title="Failed to load agents"
message="Check your connection and try again."
onRetry={refetch}
variant="card"
/>
| Prop | Type | Default | Required |
|---|---|---|---|
variant | "page" | "card" | "inline" | "card" | No |
title | string | "Something went wrong" | No |
message | string | "An unexpected error occurred. Please try again." | No |
onRetry | () => void | — | No |
retryLabel | string | "Try again" | No |
Placeholder loading states. Use before data arrives.
// Full page loading state
<PageSkeleton showCards tableRows={8} />
// Stats grid
<StatCardsSkeletonGrid count={4} />
// Card grid
<GridSkeleton columns={3} count={6} variant="agent" />
// Form sections
<FormSkeleton sections={3} />
// Table only
<TableSkeleton columns={5} rows={10} />
// Raw skeleton block (compose your own)
<Skeleton className="h-4 w-32 rounded" />
<SkeletonText lines={3} />
PageSkeleton props:
| Prop | Type | Default | Required |
|---|---|---|---|
showCards | boolean | true | No |
cardCount | number | 4 | No |
tableRows | number | 5 | No |
GridSkeleton props:
| Prop | Type | Default | Required |
|---|---|---|---|
columns | 2 | 3 | 4 | 3 | No |
count | number | — | No |
rows | number | 1 | No |
variant | "default" | "agent" | "default" | No |
FormSkeleton props:
| Prop | Type | Default | Required |
|---|---|---|---|
fields | number | 4 | No |
sections | number | — | No |
variant | "flat" | "sections" | "flat" | No |
SkeletonText props:
| Prop | Type | Default | Required |
|---|---|---|---|
lines | number | 3 | No |
LanguageSelectorSearchable language dropdown.
<LanguageSelector
value={lang}
onChange={setLang}
searchable
label="Language"
languages={[
{ code: "en", name: "English" },
{ code: "es", name: "Spanish" },
{ code: "fr", name: "French" },
{ code: "de", name: "German" },
]}
/>
| Prop | Type | Default | Required |
|---|---|---|---|
value | string | — | Yes |
onChange | (code: string) => void | — | Yes |
languages | Array<{ code: string; name: string }> | — | Yes |
placeholder | string | "Select language" | No |
searchable | boolean | true | No |
disabled | boolean | false | No |
error | boolean | string | — | No |
label | string | — | No |
PhoneInputPhone number input with country code selector.
<PhoneInput
value={phone}
onChange={setPhone}
defaultCountry="US"
label="Phone Number"
error={phoneError}
/>
Built-in countries: US, GB, CA, AU, DE, FR, IN, JP, BR, MX (with flag emojis and dial codes).
| Prop | Type | Default | Required |
|---|---|---|---|
value | string | — | Yes |
onChange | (value: string) => void | — | Yes |
defaultCountry | string | "US" | No |
placeholder | string | "Phone number" | No |
disabled | boolean | false | No |
error | boolean | string | — | No |
label | string | — | No |
DatePickerNative date picker with Date object API.
<DatePicker
value={startDate}
onChange={setStartDate}
label="Start Date"
min={new Date()}
error={dateError}
/>
| Prop | Type | Default | Required |
|---|---|---|---|
onChange | (date: Date) => void | — | Yes |
value | Date | — | No |
min | Date | — | No |
max | Date | — | No |
label | string | — | No |
error | boolean | string | — | No |
disabled | boolean | false | No |
placeholder | string | — | No |
TimePickerNative time picker with "HH:mm" string API.
<TimePicker
value={startTime}
onChange={setStartTime}
label="Start Time"
min="09:00"
max="17:00"
step={900} // 15-minute increments
/>
| Prop | Type | Default | Required |
|---|---|---|---|
onChange | (time: string) => void | — | Yes |
value | string ("HH:mm") | "" | No |
min | string ("HH:mm") | — | No |
max | string ("HH:mm") | — | No |
step | number (seconds) | 60 | No |
label | string | — | No |
error | boolean | string | — | No |
disabled | boolean | false | No |
placeholder | string | — | No |
TooltipHover tooltip with 4 positions. Respects viewport edges.
<Tooltip content="This will permanently delete the agent." side="top">
<Button variant="danger">Delete</Button>
</Tooltip>
| Prop | Type | Default | Required |
|---|---|---|---|
content | ReactNode | — | Yes |
children | ReactNode | — | Yes |
side | "top" | "bottom" | "left" | "right" | "top" | No |
delayMs | number | 300 | No |
disabled | boolean | false | No |
PopoverPositioned content panel. Supports controlled and uncontrolled modes.
{/* Uncontrolled */}
<Popover
trigger={<Button variant="outline">Options</Button>}
side="bottom"
align="start"
>
<div className="p-4 text-sm text-foreground">Popover content</div>
</Popover>
{/* Controlled */}
<Popover
trigger={<Button>Open</Button>}
open={open}
onOpenChange={setOpen}
>
...
</Popover>
| Prop | Type | Default | Required |
|---|---|---|---|
trigger | ReactNode | — | Yes |
children | ReactNode | — | Yes |
side | "top" | "bottom" | "left" | "right" | "bottom" | No |
align | "start" | "center" | "end" | "center" | No |
open | boolean | — | No |
onOpenChange | (open: boolean) => void | — | No |
DropdownMenu + DropdownMenuItemFull keyboard-navigable dropdown with ARIA roles.
{/* With built-in trigger */}
<DropdownMenu trigger={<Button variant="ghost" size="icon"><MoreHorizontalIcon /></Button>} align="right">
<DropdownMenuItem icon={<EditIcon />} onClick={() => edit(item)}>Edit</DropdownMenuItem>
<DropdownMenuDivider />
<DropdownMenuItem icon={<TrashIcon />} variant="danger" onClick={() => del(item)}>Delete</DropdownMenuItem>
</DropdownMenu>
{/* Controlled / external trigger */}
<DropdownMenu open={open} onOpenChange={setOpen} align="left" width="w-56">
<DropdownMenuHeader>Workspace</DropdownMenuHeader>
<DropdownMenuItem>Account Settings</DropdownMenuItem>
</DropdownMenu>
DropdownMenu props:
| Prop | Type | Default | Required |
|---|---|---|---|
children | ReactNode | — | Yes |
trigger | ReactNode | — | No |
open | boolean | — | No |
onOpenChange | (open: boolean) => void | — | No |
align | "left" | "right" | "right" | No |
width | string (Tailwind class) | "w-48" | No |
DropdownMenuItem props:
| Prop | Type | Default | Required |
|---|---|---|---|
children | ReactNode | — | No |
variant | "default" | "danger" | "default" | No |
icon | ReactNode | — | No |
disabled | boolean | false | No |
onClick | (e: MouseEvent) => void | — | No |
DropdownMenuHeader props: children, className
DropdownMenuDivider props: className
MoreButtonPre-styled ··· icon button for use as a dropdown trigger.
<DropdownMenu trigger={<MoreButton />}>...</DropdownMenu>
AvatarUser avatar with image and initials fallback.
<Avatar name="John Doe" src={user.avatarUrl} size="default" />
<Avatar name="Bot Alpha" size="sm" variant="rounded" />
| Prop | Type | Default | Required |
|---|---|---|---|
name | string | — | Yes |
src | string | — | No |
size | "sm" | "default" | "lg" | "default" | No |
variant | "circle" | "rounded" | "circle" | No |
ProgressLinear progress bar.
<Progress value={75} variant="gradient" showLabel />
<Progress value={progress} size="sm" />
| Prop | Type | Default | Required |
|---|---|---|---|
value | number (0–100) | — | Yes |
variant | "default" | "gradient" | "default" | No |
size | "sm" | "default" | "default" | No |
showLabel | boolean | false | No |
ThemeToggleIcon button cycling light → dark → system. Controlled or standalone.
{/* Standalone — manages own state */}
<ThemeToggle />
{/* Controlled — pair with next-themes */}
<ThemeToggle theme={resolvedTheme as "light" | "dark" | "system"} onChange={setTheme} />
| Prop | Type | Default | Required |
|---|---|---|---|
theme | "light" | "dark" | "system" | — | No |
onChange | (theme: "light" | "dark" | "system") => void | — | No |
ThemeSelectorSegmented 3-option theme control (light / dark / system).
<ThemeSelector theme={theme} onChange={setTheme} />
| Prop | Type | Default | Required |
|---|---|---|---|
theme | "light" | "dark" | "system" | — | No |
onChange | (theme: "light" | "dark" | "system") => void | — | No |
HTML5 audio/video player with playback controls, seek bar, volume, captions, keyboard shortcuts. No Video.js dependency — uses native <audio> and <video> elements.
import { MediaPlayer, useMediaSync } from "@speakai/ui";
function PlayerExample() {
const { currentTime, setCurrentTime, seekTarget, seekTo, clearSeekTarget } = useMediaSync();
return (
<MediaPlayer
src="https://example.com/audio.mp3"
mediaType="audio"
title="Interview Recording"
seekTarget={seekTarget}
onSeekComplete={clearSeekTarget}
onTimeUpdate={setCurrentTime}
captions={[
{ src: "/captions/en.vtt", label: "English", language: "en" }
]}
/>
);
}
Video mode:
<MediaPlayer
src="https://example.com/video.mp4"
mediaType="video"
poster="/thumbnail.jpg"
mode="default"
/>
Docked mode (compact bottom bar for mobile):
<MediaPlayer src={url} mediaType="audio" title="Recording" mode="dock" />
| Prop | Type | Default | Required |
|---|---|---|---|
src | string | — | Yes |
mediaType | "audio" | "video" | — | Yes |
title | string | — | No |
poster | string | — | No |
seekTarget | number | null | null | No |
onSeekComplete | () => void | — | No |
onTimeUpdate | (time: number) => void | — | No |
onPlayStateChange | (playing: boolean) => void | — | No |
onDurationChange | (duration: number) => void | — | No |
captions | Array<{ src, label, language }> | [] | No |
mode | "default" | "dock" | "default" | No |
playbackRates | number[] | [0.5, 0.75, 1, 1.25, 1.5, 2] | No |
className | string | — | No |
Keyboard shortcuts: Space (play/pause), Left Arrow (seek -10s), Right Arrow (seek +30s)
Hook for syncing MediaPlayer with transcript components. Returns shared state that both player and transcript can read/write.
const {
currentTime, // Current playback position (seconds)
setCurrentTime, // Update from player's timeupdate
duration, // Total media duration
setDuration, // Update from player's durationchange
isPlaying, // Whether media is currently playing
setIsPlaying, // Update from player's play/pause
seekTarget, // Time to seek to (set by transcript click)
seekTo, // Trigger a seek (call from transcript)
clearSeekTarget, // Clear after seek completes
} = useMediaSync();
Usage pattern — connect player and transcript:
function MediaView() {
const sync = useMediaSync();
return (
<>
<MediaPlayer
src={mediaUrl}
mediaType="audio"
seekTarget={sync.seekTarget}
onSeekComplete={sync.clearSeekTarget}
onTimeUpdate={sync.setCurrentTime}
onDurationChange={sync.setDuration}
onPlayStateChange={sync.setIsPlaying}
/>
<TranscriptViewer
sentences={transcript}
currentTime={sync.currentTime}
onSeek={sync.seekTo}
/>
</>
);
}
Components use a consistent z-index scale. Do not override these without understanding the stack:
| Layer | z-index | Components |
|---|---|---|
| Desktop sidebar | z-30 | Sidebar rail |
| Backdrops | z-40 | Sidebar mobile backdrop, SidePanel backdrop |
| Dropdowns / Tooltips | z-50 | DropdownMenu, Popover, Tooltip, Chips suggestions |
| Drawers / Panels | z-[60] | Sidebar mobile drawer, SidePanel panel |
| Modals | z-[70] | Dialog overlay + panel, BottomSheet overlay + panel |
| Toasts | z-[9999] | Toast container |
WCAG 2.1 AA compliant:
focus-visible ring on all interactive elementsaria-busy, aria-invalid, aria-live, role="alert", role="menu", role="listbox"Dialog focus trap on open, restores focus on closeTabs / DropdownMenu arrow-key navigationRadioGroup keyboard cyclingaria-hidden="true"npm install
npm run build # compile to dist/
npm run dev # demo at localhost:5555
MIT
FAQs
Speak AI design system — React + Tailwind UI components with dark/light mode, WCAG AA, and full CSS variable customization
The npm package @speakai/ui receives a total of 505 weekly downloads. As such, @speakai/ui popularity was classified as not popular.
We found that @speakai/ui demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
The supply chain control that delays freshly published gems now covers lockfile generation and gem vendoring in Ruby projects.

Security News
During a UK cyber test, a Mythos 5 agent used sockpuppets, social engineering, and prompt injection to try to get a maintainer to merge malware.

Company News
Socket is now in the AWS Security Hub Extended plan. Adopt it through AWS, apply committed spend, and block malicious open source packages.