🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@speakai/ui

Package Overview
Dependencies
Maintainers
1
Versions
64
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@speakai/ui

Speak AI design system — React + Tailwind UI components with dark/light mode, WCAG AA, and full CSS variable customization

latest
Source
npmnpm
Version
0.25.0
Version published
Weekly downloads
517
-14.26%
Maintainers
1
Weekly downloads
 
Created
Source

@speakai/ui

npm version npm downloads license

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.

Table of Contents

Install

npm install @speakai/ui

Setup

1. Import styles

/* globals.css */
@import "@speakai/ui/styles.css";

2. Configure Tailwind

// tailwind.config.ts
export default {
  content: [
    "./src/**/*.{ts,tsx}",
    "./node_modules/@speakai/ui/dist/**/*.{js,mjs}",
  ],
};

3. Dark mode

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.

All available sub-paths
Sub-pathComponents
@speakai/ui/accordionAccordion, AccordionItem
@speakai/ui/auth-cardAuthCard
@speakai/ui/auth-dividerAuthDivider
@speakai/ui/avatarAvatar
@speakai/ui/badgeBadge, StatusBadge
@speakai/ui/bottom-sheetBottomSheet
@speakai/ui/breadcrumbBreadcrumb
@speakai/ui/buttonButton
@speakai/ui/cardCard
@speakai/ui/checkboxCheckbox
@speakai/ui/chipsChips
@speakai/ui/cncn (utility)
@speakai/ui/color-pickerColorPicker
@speakai/ui/confirm-dialogConfirmDialog
@speakai/ui/date-pickerDatePicker
@speakai/ui/dialogDialog, DialogHeader, DialogBody, DialogFooter, DialogCloseButton
@speakai/ui/dropdown-menuDropdownMenu, DropdownMenuItem, DropdownMenuHeader, DropdownMenuDivider, MoreButton
@speakai/ui/empty-stateEmptyState
@speakai/ui/error-stateErrorState
@speakai/ui/file-dropzoneFileDropzone
@speakai/ui/image-uploaderImageUploader
@speakai/ui/info-cardInfoCard
@speakai/ui/inputInput, SearchInput, Select, Textarea
@speakai/ui/language-selectorLanguageSelector
@speakai/ui/mediaMediaPlayer, useMediaSync
@speakai/ui/otp-inputOTPInput
@speakai/ui/page-headerPageHeader, SectionHeader
@speakai/ui/password-inputPasswordInput
@speakai/ui/phone-inputPhoneInput
@speakai/ui/popoverPopover
@speakai/ui/progressProgress
@speakai/ui/radio-groupRadioGroup
@speakai/ui/side-panelSidePanel
@speakai/ui/sidebarSidebar, SidebarProvider, SidebarLayout, SidebarUser, useSidebar
@speakai/ui/skeletonSkeleton, SkeletonText, PageHeaderSkeleton, StatCardSkeleton, StatCardsSkeletonGrid, PageSkeleton, CardSkeleton, GridSkeleton, FormSkeleton
@speakai/ui/sliderSlider
@speakai/ui/sso-buttonSSOButton, SSOButtons
@speakai/ui/stat-cardStatCard, StatCardGrid
@speakai/ui/stepperStepper
@speakai/ui/switchSwitch
@speakai/ui/tableTable, TableHeader, TableBody, TableRow, TableHead, TableCell, TableSkeleton, TableActions, TableActionButton, TableSortHead, TablePagination, TableEmpty, useSort
@speakai/ui/tabsTabs, TabsList, TabsTrigger, TabsContent
@speakai/ui/theme-toggleThemeToggle, ThemeSelector
@speakai/ui/time-pickerTimePicker
@speakai/ui/toastToastContainer, ToastProvider, useToast
@speakai/ui/tooltipTooltip
@speakai/ui/transcriptTranscriptView

Theming Rules

CRITICAL — Read before writing any JSX with this library.

CSS Variables — Full Reference

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 */
}

Tailwind Token → Class Mapping

TokenTailwind class
--backgroundbg-background / text-background
--foregroundtext-foreground
--primarybg-primary / text-primary / border-primary
--primary-foregroundtext-primary-foreground
--cardbg-card
--card-foregroundtext-card-foreground
--popoverbg-popover
--popover-foregroundtext-popover-foreground
--mutedbg-muted
--muted-foregroundtext-muted-foreground
--accentbg-accent
--accent-foregroundtext-accent-foreground
--borderborder-border
--inputborder-input
--ringring-ring
--dangertext-danger / bg-danger / border-danger
--successtext-success / bg-success
--warningtext-warning / bg-warning
--infotext-info / bg-info

Correct vs Incorrect Color Usage

// ❌ 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>

Override Your Brand

: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;
}

Quick Reference — All Exports

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";
Barrel import (legacy — pulls entire bundle)
import {
  Button, Card, Badge, Input, Table, Dialog, // ... all 75+ exports
} from "@speakai/ui";

Component Reference

All components support the className prop for Tailwind overrides unless noted. All accept ref via forwardRef.

Auth Components

AuthCard

Centered 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>
PropTypeDefaultRequired
titlestringYes
subtitlestringNo
logoReactNodeNo
childrenReactNodeNo
classNamestringNo

SSOButton

Single SSO provider button with built-in icon.

<SSOButton provider="google" action="login" onClick={handleGoogle} />
PropTypeDefaultRequired
provider"google" | "apple" | "microsoft"Yes
action"login" | "register""login"No
isLoadingbooleanfalseNo
disabledbooleanNo

SSOButtons

Renders all three SSO buttons (Google, Apple, Microsoft) with a shared click handler.

<SSOButtons action="register" onProviderClick={(p) => handleSSO(p)} />
PropTypeDefaultRequired
action"login" | "register""login"No
onProviderClick(provider: "google" | "apple" | "microsoft") => voidNo
isLoadingbooleanfalseNo
disabledbooleanNo
classNamestringNo

PasswordInput

Input with show/hide password toggle.

<PasswordInput
  label="Password"
  placeholder="Enter password"
  error="Password is too short"
/>
PropTypeDefaultRequired
errorboolean | stringNo
labelstringNo
disabledbooleanNo

Accepts all native <input> props.

OTPInput

Multi-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" />
PropTypeDefaultRequired
valuestringYes
onChange(value: string) => voidYes
lengthnumber6No
errorboolean | stringNo
disabledbooleanfalseNo
autoFocusbooleanfalseNo

AuthDivider

Horizontal rule with centered label for separating auth options.

<AuthDivider />                  {/* renders "or" */}
<AuthDivider text="continue with" />
PropTypeDefaultRequired
textstring"or"No
classNamestringNo

Form Components

Button

Primary 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>
PropTypeDefaultRequired
variant"primary" | "secondary" | "danger" | "ghost" | "outline" | "gradient" | "glass" | "solid""primary"No
size"sm" | "md" | "lg" | "icon""md"No
isLoadingbooleanfalseNo
disabledbooleanNo
childrenReactNodeNo

Size reference: sm = h-9, md = h-10, lg = h-11, icon = h-10 w-10

Input

Standard 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)} />
PropTypeDefaultRequired
errorboolean | stringNo
labelstringNo
disabledbooleanNo

SearchInput

Input pre-configured with a search icon on the left.

<SearchInput
  placeholder="Search agents..."
  value={query}
  onChange={e => setQuery(e.target.value)}
  containerClassName="w-64"
/>
PropTypeDefaultRequired
containerClassNamestringNo

Accepts all native <input> props.

Select

Dropdown 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>
PropTypeDefaultRequired
optionsArray<{ value: string; label: string }>No
placeholderstringNo
errorboolean | stringNo
labelstringNo
disabledbooleanNo
childrenReactNodeNo

Textarea

Multi-line text input. Extends all native <textarea> props.

<Textarea placeholder="Describe your agent..." rows={4} />
<Textarea error="Description is required" label="Bio" />
PropTypeDefaultRequired
errorboolean | stringNo
labelstringNo
rowsnumberNo
disabledbooleanNo

Switch

Toggle control.

<Switch checked={enabled} onChange={setEnabled} label="Enable notifications" />
<Switch checked={active} onChange={setActive} size="sm" />
PropTypeDefaultRequired
checkedbooleanYes
onChange(checked: boolean) => voidYes
size"sm" | "default""default"No
labelstringNo
disabledbooleanfalseNo
idstringNo

Checkbox

Checkbox 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" />
PropTypeDefaultRequired
checkedbooleanNo
onChange(e: ChangeEvent<HTMLInputElement>) => voidNo
labelstringNo
descriptionstringNo
size"sm" | "default" | "lg""default"No
errorboolean | stringNo
disabledbooleanNo
idstringNo

RadioGroup

Group 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" },
  ]}
/>
PropTypeDefaultRequired
valuestringYes
onChange(value: string) => voidYes
optionsRadioOption[]Yes
orientation"horizontal" | "vertical""vertical"No
variant"default" | "card""default"No
namestringNo
disabledbooleanfalseNo

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 get border-border with hover effect. Use for settings/config where each option needs visual separation.

ColorPicker

Native color picker with hex input and optional preset swatches.

<ColorPicker
  value={color}
  onChange={setColor}
  label="Brand color"
  presetColors={["#a855f7", "#3b82f6", "#ef4444", "#10b981"]}
  showInput={true}
/>
PropTypeDefaultRequired
valuestringYes
onChange(color: string) => voidYes
presetColorsstring[]No
showInputbooleantrueNo
labelstringNo
disabledbooleanfalseNo

ImageUploader

Drag-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} />
PropTypeDefaultRequired
onChange(file: File) => voidYes
valuestringNo
onRemove() => voidNo
acceptstring"image/*"No
maxSizenumber (bytes)10485760 (10 MB)No
placeholderstring"Drop an image here or click to browse"No
disabledbooleanfalseNo
variant"dropzone" | "avatar""dropzone"No

FileDropzone

Multi-file drag-and-drop upload zone.

<FileDropzone
  onFiles={(files) => handleUpload(files)}
  accept=".pdf,.doc,.docx"
  multiple={true}
  maxSize={20 * 1024 * 1024}
/>
PropTypeDefaultRequired
onFiles(files: File[]) => voidYes
acceptstringNo
multiplebooleantrueNo
maxSizenumber (bytes)No
disabledbooleanfalseNo
childrenReactNodeNo

Chips

Tag/chip input with autocomplete dropdown.

<Chips
  value={tags}
  onChange={setTags}
  placeholder="Add tags..."
  suggestions={["react", "typescript", "tailwind", "nextjs"]}
  maxItems={10}
/>
PropTypeDefaultRequired
valuestring[]Yes
onChange(value: string[]) => voidYes
placeholderstring"Type and press Enter"No
suggestionsstring[]No
maxItemsnumberNo
disabledbooleanfalseNo

Keyboard: Enter/comma to add, Backspace to remove last, Escape to close suggestions.

Slider

Range 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}%` }} />
      ))}
    </>
  }
/>
PropTypeDefaultRequired
valuenumberYes
onChange(value: number) => voidYes
minnumber0No
maxnumber100No
stepnumber1No
labelstringNo
showValuebooleantrueNo
disabledbooleanfalseNo
inlinebooleanfalseNo
trackChildrenReactNodeNo

Inline mode: Renders as inline-flex without the label/value header row. The track fills the container width. Set a fixed width via className="w-20" for compact layouts.

Track children: Content rendered inside the track <div> (positioned relative). Use absolute positioning for markers, annotations, or segment indicators.

Layout Components

Card

Surface container with multiple visual styles.

<Card variant="default">Content</Card>
<Card variant="elevated" showGradientAccent>Featured card</Card>
<Card variant="glass">Glassmorphism effect</Card>
PropTypeDefaultRequired
variant"default" | "outline" | "elevated" | "glass""default"No
showGradientAccentbooleanfalseNo
childrenReactNodeNo

SidebarProvider + Sidebar + SidebarLayout

Collapsible 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:

PropTypeDefaultRequired
childrenReactNodeYes
defaultCollapsedbooleanfalseNo
storageKeystring"sidebar-collapsed"No

Sidebar props:

PropTypeDefaultRequired
sectionsSidebarSection[]Yes
headerReactNodeNo
footerReactNodeNo
renderLink(props: { href: string; children: ReactNode; className?: string }) => ReactNodeNo
classNamestringNo

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:

PropTypeDefaultRequired
namestringYes
emailstringNo
avatarReactNodeNo
actionsReactNodeNo

SidebarLayout props:

PropTypeDefaultRequired
childrenReactNodeYes
classNamestringNo

SidePanel

Slide-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>
PropTypeDefaultRequired
openbooleanYes
onClose() => voidYes
titlestringNo
side"left" | "right""right"No
size"sm" | "default" | "lg" | "xl" | "full""default"No
showBackbooleanfalseNo
onBack() => voidNo
backdropbooleantrueNo
headerReactNodeNo
footerReactNodeNo
childrenReactNodeNo

PageHeader

Page-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>}
/>
PropTypeDefaultRequired
titlestringYes
gradientTextstringNo
descriptionstringNo
actionReactNodeNo

SectionHeader

Section-level heading, smaller than PageHeader.

<SectionHeader
  title="Team Members"
  description="Manage who has access."
  action={<Button size="sm">Invite</Button>}
/>
PropTypeDefaultRequired
titlestringYes
descriptionstringNo
actionReactNodeNo

Tabs + TabsList + TabsTrigger + TabsContent

Tabbed 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:

PropTypeDefaultRequired
defaultTabstringYes
onTabChange(tab: string) => voidNo
childrenReactNodeNo

TabsList props:

PropTypeDefaultRequired
variant"default" | "underline" | "pills""default"No
childrenReactNodeNo

TabsTrigger props:

PropTypeDefaultRequired
valuestringYes
variant"default" | "underline" | "pills""default"No
iconReactNodeNo
badgeReactNodeNo
disabledbooleanfalseNo
childrenReactNodeNo

TabsContent props:

PropTypeDefaultRequired
valuestringYes
childrenReactNodeNo

Accordion + AccordionItem

Expandable 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:

PropTypeDefaultRequired
type"single" | "multiple""single"No
defaultOpenstring[][]No
childrenReactNodeNo

AccordionItem props:

PropTypeDefaultRequired
titlestringYes
childrenReactNodeYes
valuestringNo
defaultOpenbooleanNo
disabledbooleanfalseNo

Stepper

Step progress indicator.

<Stepper
  currentStep={1}
  orientation="horizontal"
  steps={[
    { label: "Account", description: "Basic info" },
    { label: "Profile", description: "Customize" },
    { label: "Done" },
  ]}
/>
PropTypeDefaultRequired
stepsStepperStep[]Yes
currentStepnumber (0-indexed)Yes
orientation"horizontal" | "vertical""horizontal"No

StepperStep interface:

interface StepperStep {
  label: string;
  description?: string;
}

Data Display Components

Table system

Full 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:

PropTypeDefaultRequired
scrollablebooleantrueNo
childrenReactNodeNo

TableRow props:

PropTypeDefaultRequired
clickablebooleanfalseNo
onClick(e: MouseEvent) => voidNo
childrenReactNodeNo

TableSortHead props:

PropTypeDefaultRequired
sortKeystringYes
activeSortstring | nullNo
direction"asc" | "desc" | nullNo
onSort(key: string, direction: "asc" | "desc") => voidNo
childrenReactNodeNo

TableActionButton props:

PropTypeDefaultRequired
labelstring (aria-label)Yes
variant"default" | "danger""default"No
childrenReactNodeNo

TableEmpty props:

PropTypeDefaultRequired
colSpannumberYes
titlestring"No results"No
descriptionstringNo
iconReactNodeNo
actionReactNodeNo

TablePagination props:

PropTypeDefaultRequired
pagenumberYes
pageSizenumberYes
totalnumberYes
onPageChange(page: number) => voidYes
onPageSizeChange(size: number) => voidNo
pageSizeOptionsnumber[][10, 25, 50, 100]No

TableSkeleton props:

PropTypeDefaultRequired
columnsnumber4No
rowsnumber5No

StatCard + StatCardGrid

Dashboard 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:

PropTypeDefaultRequired
labelstringYes
valuestring | numberYes
iconReactNodeNo
iconColor"purple" | "pink" | "blue" | "green" | "orange" | "red""purple"No
variant"default" | "gradient""default"No
valueClassNamestringNo

StatCardGrid props:

PropTypeDefaultRequired
columns2 | 3 | 44No
childrenReactNodeYes

Badge

Inline 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>
PropTypeDefaultRequired
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
childrenReactNodeNo

StatusBadge

Maps a status string to a badge variant automatically.

<StatusBadge status="active" />     {/* green */}
<StatusBadge status="pending" />    {/* yellow */}
<StatusBadge status="failed" />     {/* red */}
PropTypeDefaultRequired
statusstringYes

InfoCard

Colored 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>
PropTypeDefaultRequired
color"purple" | "blue" | "green" | "yellow" | "red" | "gray""blue"No
titlestringNo
descriptionstringNo
childrenReactNodeNo

Breadcrumb

Navigation breadcrumbs with auto-truncation.

<Breadcrumb
  items={[
    { label: "Home", href: "/" },
    { label: "Settings", href: "/settings" },
    { label: "Team" },
  ]}
  separator="›"
  maxItems={4}
/>
PropTypeDefaultRequired
itemsBreadcrumbItem[]Yes
separatorReactNode"/"No
maxItemsnumber0 (no limit)No

BreadcrumbItem interface:

interface BreadcrumbItem {
  label: string;
  href?: string;
  onClick?: () => void;
}

Feedback Components

ToastProvider + useToast

Global 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:

MethodSignature
success(title: string, message?: string) => void
error(title: string, message?: string) => void
info(title: string, message?: string) => void
warning(title: string, message?: string) => void

Dialog

Modal 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:

PropTypeDefaultRequired
openbooleanYes
onClose() => voidYes
size"sm" | "default" | "lg" | "xl" | "full""default"No
childrenReactNodeNo

DialogCloseButton props:

PropTypeDefaultRequired
onClose() => voidYes

ConfirmDialog

Pre-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.

PropTypeDefaultRequired
openbooleanYes
onClose() => voidYes
onConfirm() => voidYes
titlestringYes
descriptionstringNo
variant"danger" | "warning" | "info""danger"No
confirmLabelstring"Confirm"No
cancelLabelstring"Cancel"No
isLoadingbooleanfalseNo
iconReactNodeNo

BottomSheet

Mobile 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:

PropTypeDefaultRequired
openboolean--Yes
onClose() => void--Yes
snapPointsnumber[]--No
childrenReactNode--Yes
classNamestring--No

Behavior:

  • Drag the handle down past 30% of sheet height to dismiss
  • With snapPoints, the sheet snaps to the nearest point on drag end
  • Escape key closes the sheet
  • Clicking the backdrop closes the sheet
  • Body scroll is locked while open
  • Focus is trapped inside the sheet (role="dialog", aria-modal)

EmptyState

Illustrated 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"
/>
PropTypeDefaultRequired
titlestringYes
descriptionstringNo
iconReactNodeNo
actionReactNodeNo
height"sm" | "md" | "lg""md"No

ErrorState

Error feedback in page, card, or inline variants.

<ErrorState
  title="Failed to load agents"
  message="Check your connection and try again."
  onRetry={refetch}
  variant="card"
/>
PropTypeDefaultRequired
variant"page" | "card" | "inline""card"No
titlestring"Something went wrong"No
messagestring"An unexpected error occurred. Please try again."No
onRetry() => voidNo
retryLabelstring"Try again"No

Skeleton Components

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:

PropTypeDefaultRequired
showCardsbooleantrueNo
cardCountnumber4No
tableRowsnumber5No

GridSkeleton props:

PropTypeDefaultRequired
columns2 | 3 | 43No
countnumberNo
rowsnumber1No
variant"default" | "agent""default"No

FormSkeleton props:

PropTypeDefaultRequired
fieldsnumber4No
sectionsnumberNo
variant"flat" | "sections""flat"No

SkeletonText props:

PropTypeDefaultRequired
linesnumber3No

Selector Components

LanguageSelector

Searchable 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" },
  ]}
/>
PropTypeDefaultRequired
valuestringYes
onChange(code: string) => voidYes
languagesArray<{ code: string; name: string }>Yes
placeholderstring"Select language"No
searchablebooleantrueNo
disabledbooleanfalseNo
errorboolean | stringNo
labelstringNo

PhoneInput

Phone 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).

PropTypeDefaultRequired
valuestringYes
onChange(value: string) => voidYes
defaultCountrystring"US"No
placeholderstring"Phone number"No
disabledbooleanfalseNo
errorboolean | stringNo
labelstringNo

DatePicker

Native date picker with Date object API.

<DatePicker
  value={startDate}
  onChange={setStartDate}
  label="Start Date"
  min={new Date()}
  error={dateError}
/>
PropTypeDefaultRequired
onChange(date: Date) => voidYes
valueDateNo
minDateNo
maxDateNo
labelstringNo
errorboolean | stringNo
disabledbooleanfalseNo
placeholderstringNo

TimePicker

Native 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
/>
PropTypeDefaultRequired
onChange(time: string) => voidYes
valuestring ("HH:mm")""No
minstring ("HH:mm")No
maxstring ("HH:mm")No
stepnumber (seconds)60No
labelstringNo
errorboolean | stringNo
disabledbooleanfalseNo
placeholderstringNo

Utility Components

Tooltip

Hover tooltip with 4 positions. Respects viewport edges.

<Tooltip content="This will permanently delete the agent." side="top">
  <Button variant="danger">Delete</Button>
</Tooltip>
PropTypeDefaultRequired
contentReactNodeYes
childrenReactNodeYes
side"top" | "bottom" | "left" | "right""top"No
delayMsnumber300No
disabledbooleanfalseNo

Popover

Positioned 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>
PropTypeDefaultRequired
triggerReactNodeYes
childrenReactNodeYes
side"top" | "bottom" | "left" | "right""bottom"No
align"start" | "center" | "end""center"No
openbooleanNo
onOpenChange(open: boolean) => voidNo

DropdownMenu + DropdownMenuItem

Full 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:

PropTypeDefaultRequired
childrenReactNodeYes
triggerReactNodeNo
openbooleanNo
onOpenChange(open: boolean) => voidNo
align"left" | "right""right"No
widthstring (Tailwind class)"w-48"No

DropdownMenuItem props:

PropTypeDefaultRequired
childrenReactNodeNo
variant"default" | "danger""default"No
iconReactNodeNo
disabledbooleanfalseNo
onClick(e: MouseEvent) => voidNo

DropdownMenuHeader props: children, className

DropdownMenuDivider props: className

MoreButton

Pre-styled ··· icon button for use as a dropdown trigger.

<DropdownMenu trigger={<MoreButton />}>...</DropdownMenu>

Avatar

User avatar with image and initials fallback.

<Avatar name="John Doe" src={user.avatarUrl} size="default" />
<Avatar name="Bot Alpha" size="sm" variant="rounded" />
PropTypeDefaultRequired
namestringYes
srcstringNo
size"sm" | "default" | "lg""default"No
variant"circle" | "rounded""circle"No

Progress

Linear progress bar.

<Progress value={75} variant="gradient" showLabel />
<Progress value={progress} size="sm" />
PropTypeDefaultRequired
valuenumber (0–100)Yes
variant"default" | "gradient""default"No
size"sm" | "default""default"No
showLabelbooleanfalseNo

ThemeToggle

Icon 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} />
PropTypeDefaultRequired
theme"light" | "dark" | "system"No
onChange(theme: "light" | "dark" | "system") => voidNo

ThemeSelector

Segmented 3-option theme control (light / dark / system).

<ThemeSelector theme={theme} onChange={setTheme} />
PropTypeDefaultRequired
theme"light" | "dark" | "system"No
onChange(theme: "light" | "dark" | "system") => voidNo

Media Components

MediaPlayer

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" />
PropTypeDefaultRequired
srcstringYes
mediaType"audio" | "video"Yes
titlestringNo
posterstringNo
seekTargetnumber | nullnullNo
onSeekComplete() => voidNo
onTimeUpdate(time: number) => voidNo
onPlayStateChange(playing: boolean) => voidNo
onDurationChange(duration: number) => voidNo
captionsArray<{ src, label, language }>[]No
mode"default" | "dock""default"No
playbackRatesnumber[][0.5, 0.75, 1, 1.25, 1.5, 2]No
classNamestringNo

Keyboard shortcuts: Space (play/pause), Left Arrow (seek -10s), Right Arrow (seek +30s)

useMediaSync

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}
      />
    </>
  );
}

Z-Index Layering

Components use a consistent z-index scale. Do not override these without understanding the stack:

Layerz-indexComponents
Desktop sidebarz-30Sidebar rail
Backdropsz-40Sidebar mobile backdrop, SidePanel backdrop
Dropdowns / Tooltipsz-50DropdownMenu, Popover, Tooltip, Chips suggestions
Drawers / Panelsz-[60]Sidebar mobile drawer, SidePanel panel
Modalsz-[70]Dialog overlay + panel, BottomSheet overlay + panel
Toastsz-[9999]Toast container

Accessibility

WCAG 2.1 AA compliant:

  • focus-visible ring on all interactive elements
  • aria-busy, aria-invalid, aria-live, role="alert", role="menu", role="listbox"
  • Dialog focus trap on open, restores focus on close
  • Tabs / DropdownMenu arrow-key navigation
  • RadioGroup keyboard cycling
  • 40px minimum touch targets on all interactive elements
  • 16px minimum input font size (prevents iOS zoom)
  • Decorative SVGs use aria-hidden="true"

Development

npm install
npm run build   # compile to dist/
npm run dev     # demo at localhost:5555

License

MIT

Keywords

react

FAQs

Package last updated on 04 Aug 2026

Did you know?

Socket

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.

Install

Related posts