control/uialpha
Guides
  • Overview
  • Get started
  • Shadcn compatibility
  • Architecture
  • Agent surface
  • Theme accessibility
  • Theme AI builder
Skins
  • Skinning Control UI
Layout
  • Aspect ratioBetaBeta — the props contract is close to final, but small breaking changes can still land.
  • Dockable PanelExpExperimental — the contract and the rendering can change without notice. Own the installed copy before shipping it.
  • Infinite CanvasExpExperimental — the contract and the rendering can change without notice. Own the installed copy before shipping it.
  • Morphing PanelExpExperimental — the contract and the rendering can change without notice. Own the installed copy before shipping it.
  • ResizableBetaBeta — the props contract is close to final, but small breaking changes can still land.
  • Scroll area
  • SidebarBetaBeta — the props contract is close to final, but small breaking changes can still land.
Actions
  • Button
  • Button group
  • Command
  • Context Menu
  • Dropdown Menu
  • Menubar
  • Toggle
  • Toolbar
  • Trigger MenuBetaBeta — the props contract is close to final, but small breaking changes can still land.
Forms
  • Autocomplete
  • CalendarBetaBeta — the props contract is close to final, but small breaking changes can still land.
  • Checkbox
  • Checkbox GroupBetaBeta — the props contract is close to final, but small breaking changes can still land.
  • Color PickerBetaBeta — the props contract is close to final, but small breaking changes can still land.
  • Combobox
  • DropzoneBetaBeta — the props contract is close to final, but small breaking changes can still land.
  • Field
  • Form
  • Gradient EditorBetaBeta — the props contract is close to final, but small breaking changes can still land.
  • Input
  • Input group
  • Input OTP
  • Native select
  • Number Field
  • Phone inputBetaBeta — the props contract is close to final, but small breaking changes can still land.
  • Radio group
  • Select
  • Slider
  • Switch
  • Textarea
Navigation
  • Navigation menu
  • Pagination
  • Stepper
  • Table of contents
  • Tabs
Overlays
  • Alert dialog
  • Dialog
  • Drawer
  • Hover card
  • Popover
  • Responsive dialog
  • Tooltip
Feedback
  • AlertBetaBeta — the props contract is close to final, but small breaking changes can still land.
  • EmptyBetaBeta — the props contract is close to final, but small breaking changes can still land.
  • Progress
  • SkeletonBetaBeta — the props contract is close to final, but small breaking changes can still land.
  • Spinner
  • Toast
Display
  • Accordion
  • Avatar
  • BadgeBetaBeta — the props contract is close to final, but small breaking changes can still land.
  • CardBetaBeta — the props contract is close to final, but small breaking changes can still land.
  • Code
  • Code Diff
  • Collapsible
  • ItemBetaBeta — the props contract is close to final, but small breaking changes can still land.
  • Kbd
  • Markdown
  • Meter
  • Table
  • Timeline
  • Tree
  • Typography
Hooks
  • Use Chat Message
  • Use Chat Composer
  • Use User Ask
  • Use Audio Recorder
  • Use Environment Variables
  • Use Copy To Clipboard
Utils
  • cn
  • Skin
  • Contracts
  • Serialize
Extensions
  • Control Effects
  • View Transition
  • Send Aurora
Create app

An opinionated, customizable superset of shadcn/ui

  • GitHub9
No results found.
Guides
Overview
An owned-source registry of primitives, agent surfaces, complete blocks, and swappable skins.
Guide
Get started
Choose a skin, install a component or complete block, wire its CSS, and compose your application runtime.
Guide
shadcn compatibility
shadcn registry, token, and ownership conventions without writing to components/ui.
Guide
Architecture
Runtime ownership, skin layering, customization paths, and registry derivation.
Guide
Agent surface
Inspect and install registry items through HTTP, shadcn manifests, static metadata, and machine-readable docs.
Guide
Theme accessibility
Audit canonical theme colors plus rendered popup, badge, and active-tab states, then run the same checks from the CLI.
Guide
Theme AI builder
Create a Control UI theme with Claude Code, Codex, or Mastra Code, then import and test it live.
Guide
Skins
Skinning Control UI
Author complete token-driven Control UI skins with slots, adornments, motion controls, and one shared component source.
Skin
Refined
Compact, calm starting skin with a complete Control UI token contract.
Skin
Windows XP
Windows XP-inspired Luna tokens, bevels, and titlebar details.
Skin
Liquid metal
Polished metal skin with a WebGL shader control surface.
Skin
Rig
Brutalist skin with coral accents, squared corners, and dense typography.
Skin
Flat
Neutral reset skin with square corners, no shadows, and instant motion.
Skin
Modern Apple
Apple-inspired Liquid Glass skin: WebGL-refraction on floating surfaces, precise directional rims, transparent inputs, and continuous corners.
Skin
Cuicui
Cuicui-inspired shell skin with fixed grain, a docked w-80 sidebar, a neutral main container, and the send-aurora anchored extension on ChatComposer (skin.config fills the chat-composer:send-layer anchor).
Skin
Linear
Linear-inspired skin: indigo brand on a cool neutral ramp, a flat 13px chrome band, 4px radius, hairline borders instead of elevation, and pill-shaped filled actions.
Skin
Skills
CSS-first interactivity
Model UI reactions with relational selectors, container/style queries, and native platform elements before adding React state, effects, or event handlers.
Skill
CSS-first motion & sizing
Drive sizing, enter/exit motion, and scroll effects from CSS and native attributes — token-driven and progressively enhanced — instead of measuring and animating values in JavaScript.
Skill
Derive, do not duplicate
Avoid second sources of truth in component props, hook parameters, and local state.
Skill
Remount state boundaries
Reset state by changing component identity or loading boundaries, not by syncing with effects.
Skill
Explicit names instead of comments
Encode intent in identifiers and extracted units so the code needs no explanatory comments, and keep the rare justified one telegraphic.
Skill
One responsibility per file
Split domain components and hooks before fetching, filtering, selection, and form state become entangled.
Skill
Keep input and output APIs narrow
Split oversized prop, argument, and return-value APIs into cohesive responsibilities.
Skill
Keep context necessary, narrow, and stable
Use context only across real composition boundaries, with the smallest stable semantic value consumers need.
Skill
Do not query the DOM in React
Keep element identity and collections in React instead of searching rendered markup.
Skill
Real stack tests
Prefer tests that drive production hooks, clients, routing, and cache behavior with only the network mocked.
Skill
Test rendered output, not class names
Prove visual behavior through computed styles, geometry, or browser output instead of Tailwind class strings.
Skill
Cohesive folders
Keep every folder a short, readable table of contents by grouping loose files into named responsibilities.
Skill
Single source of truth
Prevent shared facts from drifting by assigning each one a canonical owner.
Skill
Use existing components first
Start from the local design system or registry primitive before creating a new styled element.
Skill
Token discipline
Use established design tokens and local CSS variables before reaching for one-off values.
Skill
ClassName boundaries
Use component APIs for intended variation instead of overriding design-system internals with className.
Skill
Tailwind v4 CSS configuration
Configure Tailwind through CSS directives and tokens instead of adding new JavaScript config.
Skill
Tailwind v4 migration syntax
Use v4 names and modifiers when replacing or reviewing v3-era Tailwind classes.
Skill
First-class utilities before custom CSS
Prefer Tailwind v4's first-class utility families before arbitrary properties or handwritten CSS.
Skill
Variant-first styling
Check Tailwind variants before adding handwritten selectors, style props, or React state for styling.
Skill
Tailwind v4 capabilities
Reach for new v4 utilities before layout hacks, user-agent checks, or JavaScript measurement.
Skill
Tailwind v4 behavior & motion
Account for v4 behavior changes around transforms, hover, default colors, variant order, spacing, and motion.
Skill
State continuity
Show loading, empty, partial, success, and failure states in the same product language.
Skill
Provenance without noise
Surface source, runtime, and ownership cues only where they help the user trust or act.
Skill
Dense but scannable
Favor compact surfaces that support repeated work without flattening hierarchy.
Skill
Say it once
Cut redundant AI-slop copy so labels, controls, and state carry the obvious meaning.
Skill
Skin authoring
Author a skin as three files with strict roles — theme.css owns the complete token contract, skin.config restyles slots and declares DS choices, skin.css says only what classes cannot.
Skill
Extension authoring
Author an extension as an optional installable layered on the library — root-mounted for cross-cutting anatomy-attached behavior, anchored for component-scoped fx activated from skin.config.
Skill
Registry-first DX
Keep the registry manifest and installed source as the contract users can inspect.
Skill
Skin completeness
Treat a skin as a complete source recipe, not a partial class override layer.
Skill
Control UI composition
Keep blocks readable by composing public agent components, hooks, and slots explicitly.
Skill
Runtime-agnostic UI
Keep core visual surfaces independent from model runners, stores, and transport lifecycles.
Skill
Compound components DX
Expose named anatomy parts when users need to customize layout or actions.
Skill
Context vs props
Reach for a compound-component context only when props cannot reach the part, and store intent in it, not styling.
Skill
AI
AI components
Explore composable surfaces for messages, input, activity, media, and agent workflows.
Agent
ChatMessage
Composable chat message with typed role, density, and tone state.
Agent
ChatComposer
Prompt composer with controlled text, submit state, and trigger-menu support.
Agent
ChatComposerAttachment
Composer attachment rail with file previews, upload progress, and removal.
Agent
UserAsk
Keyboard-first agent question panel that temporarily replaces the chat composer inside its container.
Agent
TaskList
Floating agent task progress pill above the composer that expands into the full task list.
Agent
AudioRecorder
Voice recorder with realtime waveform, duration, cancel, and submit controls.
Agent
AudioVisualizer
Levels-driven realtime audio visualizer offered in two usage versions - bars and line - sharing one export and one props contract.
BetaBeta — the props contract is close to final, but small breaking changes can still land.Agent
DynamicNotification
Dynamic Island-style AI notification pill with a thinking state that morphs into a reply bubble — token-driven surface, WebGL-enhanced backdrop blur, or real refractive liquid glass.
ExpExperimental — the contract and the rendering can change without notice. Own the installed copy before shipping it.Agent
EnvironmentVariables
Composable environment variable editor with .env upload, bulk paste, reveal controls, and submit helpers.
ExpExperimental — the contract and the rendering can change without notice. Own the installed copy before shipping it.Agent
Activity
Shared static and collapsible activity anatomy with bounded, scrollable detail content.
Agent
Context
Compact context-window usage with an automatically derived token graph and anchored detail inspector.
BetaBeta — the props contract is close to final, but small breaking changes can still land.Agent
InlineCitation
Inline multi-source citation with a keyboard-accessible preview and source navigation.
BetaBeta — the props contract is close to final, but small breaking changes can still land.Agent
SourceBadge
Linked source badge with an automatic same-origin favicon, derived hostname, and resilient fallback.
Agent
ActionBar
Reusable hover actions for message and response controls.
Agent
InlineAttachment
Inline file and media previews for chat turns.
Agent
MarkdownBlock
Assistant markdown output rendered to prose, with a header and copy-source action.
Agent
CodeBlockEditor
Editable code surface with Shiki highlighting and token-based light/dark themes.
Agent
ChatLayout
Layout primitives for full chat threads, turns, and thoughts.
Agent
ThreadRail
Conversation minimap for scanning and jumping between chat turns.
Agent
Primitives
Primitives
Browse every Control UI primitive through the same live examples used in its documentation.
Primitive
Button
Accessible action button with size, variant, and semantic tone support.
Primitive
Collapsible
Accessible disclosure primitive with measured open and close motion.
Primitive
Tabs
Segmented and browser-style navigation with a stable active indicator.
Primitive
Sidebar
Responsive app sidebar with collapse, mobile sheet, and keyboard toggle support.
BetaBeta — the props contract is close to final, but small breaking changes can still land.Primitive
Scroll area
Scroll container with overlay scrollbars and edge fades.
Primitive
Table of contents
Sticky in-page navigation with scroll-spy range highlighting.
Primitive
Timeline
Static chronological events with independent status, connectors, descriptions, and metadata.
Primitive
Stepper
Static and interactive workflow steps with horizontal and vertical layouts.
Primitive
Skeleton
Loading placeholder with a token-driven shimmer.
BetaBeta — the props contract is close to final, but small breaking changes can still land.Primitive
Slider
Single-value range control with branded and plain treatments.
Primitive
Select
Single-choice picker with a token-matched trigger and floating list.
Primitive
DropdownMenu
Dropdown menu for actions, resources, labels, separators, and submenus.
Primitive
Context Menu
Pointer-positioned right-click and long-press menu with nested actions and selection controls.
Primitive
Toggle
Pressed-state button and toggle group built on the Button surface.
Primitive
Switch
On/off control with token-driven track, thumb, and press motion.
Primitive
Dialog
Modal dialog for focused tasks, confirmations, and custom panels.
Primitive
Popover
Anchored floating panel for inline settings and contextual content.
Primitive
Tooltip
Hover or focus hint popup with Base UI positioning and Control UI tokens.
Primitive
Drawer
Swipeable edge panel for mobile sheets and off-canvas surfaces.
Primitive
Responsive dialog
Modal dialog on desktop that becomes a swipeable bottom drawer on mobile.
Primitive
Toast
Transient notifications with a callable toast API and single Toaster mount.
Primitive
Input
Text field primitive sized and styled to match other controls.
Primitive
Input group
Joined input wrapper for addons, icons, and focus-within rings.
Primitive
Dropzone
Composable file intake with validation, managed selection, and drag-activated overlays.
BetaBeta — the props contract is close to final, but small breaking changes can still land.Primitive
Phone input
International phone field with country search, E.164 values, and Zod validation helpers.
BetaBeta — the props contract is close to final, but small breaking changes can still land.Primitive
Command
Command palette with token-matched dialog, input, and result rows.
Primitive
Kbd
Keyboard shortcut chip and chord group.
Primitive
Checkbox
Single checkbox control with checked and indeterminate states.
Primitive
Radio group
Single-choice radio set for plans, filters, and option lists.
Primitive
Accordion
Stacked disclosure rows with measured panel animation.
Primitive
Avatar
Profile image with initials fallback and composable overlapping groups.
Primitive
Progress
Determinate task progress with optional label and value rows.
Primitive
Hover card
Hover or focus preview panel for profiles, links, and contextual details.
Primitive
Alert dialog
Modal confirmation dialog for destructive or blocking decisions.
Primitive
Menubar
Desktop command bar with nested menus, shortcuts, and separators.
Primitive
Navigation menu
Site navigation menu with a shared animated viewport.
Primitive
Field
Form field wrapper for labels, descriptions, errors, and validity state.
Primitive
Form
Form wrapper that coordinates field validation and returned errors.
Primitive
Native select
Native select control styled to match the Control UI control family.
Primitive
Textarea
Multiline text field with CSS-first auto-growth.
Primitive
Input OTP
One-time-code field with grouped, focus-aware slots.
Primitive
Combobox
Searchable single-select with input and floating option list.
Primitive
Alert
Inline status panel for callouts, errors, and notices.
BetaBeta — the props contract is close to final, but small breaking changes can still land.Primitive
Badge
Compact status, label, or count chip.
BetaBeta — the props contract is close to final, but small breaking changes can still land.Primitive
Card
Content surface for panels, tiles, and settings groups.
BetaBeta — the props contract is close to final, but small breaking changes can still land.Primitive
Table
Responsive data table for lists, comparisons, and structured records.
Primitive
Aspect ratio
CSS aspect-ratio wrapper for media and previews.
BetaBeta — the props contract is close to final, but small breaking changes can still land.Primitive
Button group
Joined button group for toolbars, split actions, and segmented controls.
Primitive
Empty
Empty-state layout for blank lists, zero results, and new workspaces.
BetaBeta — the props contract is close to final, but small breaking changes can still land.Primitive
Item
List row with media, content, and trailing actions.
BetaBeta — the props contract is close to final, but small breaking changes can still land.Primitive
Pagination
Page navigation for long lists and result sets.
Primitive
Spinner
Accessible loading indicator for pending buttons, panels, and inline waits.
Primitive
Meter
Static range meter for quota, storage, score, or usage values.
Primitive
Tree
Accessible tree view with roving keyboard navigation, single/multi selection, and animated disclosure.
Primitive
Checkbox Group
Multi-select checkbox set with shared state and select-all support.
BetaBeta — the props contract is close to final, but small breaking changes can still land.Primitive
Autocomplete
Free-text input with search-as-you-type suggestions.
Primitive
Number Field
Numeric input with stepper buttons and optional drag-to-change behavior.
Primitive
Trigger Menu
Caret-anchored command or mention menu for text editors.
BetaBeta — the props contract is close to final, but small breaking changes can still land.Primitive
Toolbar
Roving-focus toolbar for editor controls and compact actions.
Primitive
Dockable Panel
Non-modal workspace panel that moves between two explicit edge slots with a mobile Drawer fallback.
ExpExperimental — the contract and the rendering can change without notice. Own the installed copy before shipping it.Primitive
Infinite Canvas
Pan-and-zoom spatial workspace for arranging content without fixed bounds.
ExpExperimental — the contract and the rendering can change without notice. Own the installed copy before shipping it.Primitive
Morphing Panel
Accessible disclosure surface that morphs between explicit collapsed and expanded dimensions.
ExpExperimental — the contract and the rendering can change without notice. Own the installed copy before shipping it.Primitive
Color Picker
Color input with picker UI, formats, presets, and contrast helpers.
BetaBeta — the props contract is close to final, but small breaking changes can still land.Primitive
Gradient Editor
CSS gradient editor with draggable stops and live preview.
BetaBeta — the props contract is close to final, but small breaking changes can still land.Primitive
Resizable
Accessible resizable panel groups and split layouts with keyboard support.
BetaBeta — the props contract is close to final, but small breaking changes can still land.Primitive
Calendar
Date selection grid built on react-day-picker and themed through tokens.
BetaBeta — the props contract is close to final, but small breaking changes can still land.Primitive
Typography
The token-driven type scale — one --text-* rung per size, named by role. Publish the utilities, not a component.
Primitive
Code
Shared code surface: Shiki-highlighted lines, gutter, clean copy, and virtualization for large files.
Primitive
Code Diff
Unified or split diff from a git patch or a before/after pair, with word-level intra-line highlighting.
Primitive
Markdown
Rendered agent markdown (GFM) whose code fences compose Code, and diff fences compose CodeDiff.
Primitive
Support files
useChatMessage
Typed role, density, and tone state for ChatMessage.
Hook
useChatComposer
Controlled composer text and submit state.
Hook
useUserAsk
Question/option registration, selection, freeform text, and keyboard flow for UserAsk.
Hook
useAudioRecorder
Browser microphone recording state, waveform levels, and Blob completion.
Hook
useEnvironmentVariables
Editable environment variable rows with .env parsing, upload, duplicate detection, and submit helpers.
Hook
useCopyToClipboard
Copy-to-clipboard behavior with success state and fallback support.
Hook
cn
Tailwind class-name merge helper.
Util
skin
Skin slot and adornment resolvers for the Control UI library.
Util
contracts
Shared prop and slot contracts for swappable skins.
Util
serialize
Bridge a rich editor doc to plain text (and structured mentions) and back — independent of any one component.
Util
Use cases
Use cases
Start from complete workspace templates or focused interaction patterns, then own and adapt the installed source.
Use case
Chat
Controlled chat shell that composes rendered turns and a provider-owned composer.
Use case
Theme toggle
Controlled theme controls with a three-value switch, binary switch, cycle button, and dropdown.
Use case
Coding agent
Desktop coding workspace with project tasks, a focused conversation, and a persistent controlled composer.
BetaBeta — the props contract is close to final, but small breaking changes can still land.Use case
Settings
Searchable multi-page settings shell with responsive navigation and accessible control groups.
Use case
File explorer
Finder-inspired file browser with locations, search, resizable columns, breadcrumbs, and an item preview.
BetaBeta — the props contract is close to final, but small breaking changes can still land.Use case
PrimitivesAIUse casesSkills
Primitives

Stepper

Static and interactive workflow steps with horizontal and vertical layouts.

Examples

Controlled vertical workflow

Application state owns navigation while Stepper derives progress and keeps inactive panels mounted.

Composition

The preferred shape for composing the installed primitive from its exported parts.

Parts

Exported compound parts from the installed source.

Stepper
├── StepperList
├── StepperItem
├── StepperTrigger
├── StepperIndicator
├── StepperSeparator
├── StepperTitle
├── StepperDescription
└── StepperContent

First install and activate one skin. Core deliberately contains no visual token defaults.

The Control UI source installs this primitive from src/registry/sources/control-ui/ui/stepper.tsx. Install it on its own with the command above, or inspect the source below.

Registry command
npx shadcn@latest add https://control-ui.dev/r/stepper.json
See registry manifest

Raw code

This primitive's owned source and private support files

src/registry/sources/control-ui/ui/stepper.tsx
"use client";

import { CheckIcon, CircleAlertIcon } from "lucide-react";
import { createContext, useContext, useId, useState } from "react";
import type {
  StepperContentMode,
  StepperContentProps,
  StepperDescriptionProps,
  StepperIndicatorProps,
  StepperItemProps,
  StepperListProps,
  StepperOrientation,
  StepperProps,
  StepperSeparatorProps,
  StepperState,
  StepperTitleProps,
  StepperTriggerProps,
} from "@/components/control-ui/contracts";
import { cn } from "@/components/control-ui/lib/cn";
import { skinSlot } from "@/components/control-ui/skin";

type StepperContextValue = {
  value: number | null;
  orientation: StepperOrientation;
  contentMode: StepperContentMode;
  responsive: boolean;
  baseId: string;
  selectStep: (step: number) => void;
};

const StepperContext = createContext<StepperContextValue | null>(null);

function useStepper() {
  const context = useContext(StepperContext);
  if (!context) throw new Error("Stepper parts must be used within <Stepper>.");
  return context;
}

type StepperItemContextValue = {
  step: number;
  state: StepperState;
  disabled: boolean;
  invalid: boolean;
  titleId: string;
  contentId: string;
};

const StepperItemContext = createContext<StepperItemContextValue | null>(null);

function useStepperItem() {
  const context = useContext(StepperItemContext);
  if (!context) throw new Error("Stepper item parts must be used within <StepperItem>.");
  return context;
}

function stateForStep(value: number | null, step: number): StepperState {
  if (value === null) return "neutral";
  if (step < value) return "complete";
  if (step === value) return "current";
  return "upcoming";
}

function indicatorStateLabel(state: StepperState, invalid: boolean): string | null {
  if (invalid) return "invalid";
  if (state === "neutral") return null;
  return state;
}

function defaultIndicatorContent(step: number, state: StepperState, invalid: boolean) {
  if (invalid) return <CircleAlertIcon className="size-4" />;
  if (state === "complete") return <CheckIcon className="size-4" />;
  return step + 1;
}

function itemLayout(orientation: StepperOrientation, responsive: boolean) {
  if (orientation === "vertical") {
    return "grid min-w-0 grid-cols-[2rem_minmax(0,1fr)] pb-6 text-left last:pb-0";
  }
  return cn(
    "relative grid min-w-0 flex-1 grid-cols-1 justify-items-center text-center",
    responsive &&
      "@max-md/stepper:w-full @max-md/stepper:flex-none @max-md/stepper:grid-cols-[2rem_minmax(0,1fr)] @max-md/stepper:justify-items-stretch @max-md/stepper:pb-6 @max-md/stepper:text-left @max-md/stepper:last:pb-0",
  );
}

function partLayout(orientation: StepperOrientation, responsive: boolean, part: "indicator" | "title" | "description") {
  const vertical = {
    indicator: "col-start-1 row-span-2 row-start-1",
    title: "col-start-2 row-start-1 ml-3",
    description: "col-start-2 row-start-2 ml-3",
  }[part];
  if (orientation === "vertical") return vertical;

  const horizontal = {
    indicator: "col-start-1 row-start-1",
    title: "col-start-1 row-start-2 mt-2",
    description: "col-start-1 row-start-3 mt-0.5",
  }[part];
  if (!responsive) return horizontal;

  const narrow = {
    indicator: "@max-md/stepper:col-start-1 @max-md/stepper:row-span-2 @max-md/stepper:row-start-1",
    title: "@max-md/stepper:col-start-2 @max-md/stepper:row-start-1 @max-md/stepper:mt-0 @max-md/stepper:ml-3",
    description: "@max-md/stepper:col-start-2 @max-md/stepper:row-start-2 @max-md/stepper:mt-0 @max-md/stepper:ml-3",
  }[part];
  return cn(horizontal, narrow);
}

export function Stepper({
  value,
  defaultValue = null,
  onValueChange,
  orientation = "horizontal",
  contentMode = "current",
  responsive = true,
  id,
  className,
  children,
  ...props
}: StepperProps) {
  const generatedId = useId();
  const [internalValue, setInternalValue] = useState<number | null>(defaultValue);
  const controlled = value !== undefined;
  const currentValue = value !== undefined ? value : internalValue;

  const selectStep = (step: number) => {
    if (!controlled) setInternalValue(step);
    onValueChange?.(step);
  };

  return (
    <StepperContext.Provider
      value={{
        value: currentValue,
        orientation,
        contentMode,
        responsive,
        baseId: id ?? generatedId,
        selectStep,
      }}
    >
      <div
        {...props}
        id={id}
        data-control-ui="stepper"
        data-slot="root"
        data-orientation={orientation}
        data-content-mode={contentMode}
        data-responsive={responsive ? "true" : undefined}
        className={cn(
          responsive && "@container/stepper",
          "w-full",
          skinSlot("stepper", "root", { orientation, contentMode, responsive }),
          className,
        )}
      >
        {children}
      </div>
    </StepperContext.Provider>
  );
}

export function StepperList({ className, ...props }: StepperListProps) {
  const { orientation, responsive } = useStepper();
  return (
    <ol
      {...props}
      data-control-ui="stepper"
      data-slot="list"
      data-orientation={orientation}
      className={cn(
        orientation === "horizontal" ? "flex w-full items-start" : "flex w-full flex-col",
        orientation === "horizontal" && responsive && "@max-md/stepper:flex-col @max-md/stepper:items-stretch",
        skinSlot("stepper", "list", { orientation, responsive }),
        className,
      )}
    />
  );
}

export function StepperItem({ step, disabled = false, invalid = false, className, children, ...props }: StepperItemProps) {
  const { value, orientation, responsive, baseId } = useStepper();
  const state = stateForStep(value, step);
  const contextValue = {
    step,
    state,
    disabled,
    invalid,
    titleId: `${baseId}-step-${step}-title`,
    contentId: `${baseId}-step-${step}-content`,
  } satisfies StepperItemContextValue;

  return (
    <StepperItemContext.Provider value={contextValue}>
      <li
        {...props}
        data-control-ui="stepper"
        data-slot="item"
        data-step={step}
        data-state={state}
        data-disabled={disabled ? "true" : undefined}
        data-invalid={invalid ? "true" : undefined}
        aria-current={state === "current" ? "step" : undefined}
        className={cn(
          "relative",
          itemLayout(orientation, responsive),
          skinSlot("stepper", "item", { state, disabled, invalid }),
          className,
        )}
      >
        {children}
      </li>
    </StepperItemContext.Provider>
  );
}

export function StepperTrigger({ disabled: disabledProp, className, onClick, type = "button", ...props }: StepperTriggerProps) {
  const { orientation, responsive, selectStep } = useStepper();
  const { step, state, disabled, invalid, contentId } = useStepperItem();
  const isDisabled = disabled || Boolean(disabledProp);

  return (
    <button
      {...props}
      data-control-ui="stepper"
      data-slot="trigger"
      data-state={state}
      data-invalid={invalid ? "true" : undefined}
      type={type}
      disabled={isDisabled}
      aria-controls={contentId}
      className={cn(
        "group/stepper-trigger col-span-full row-span-3 row-start-1 grid w-full min-w-0 grid-cols-1 justify-items-center rounded-sm text-center outline-none transition-colors duration-[var(--duration-fast)] ease-[var(--ease-standard)] focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
        orientation === "vertical" && "row-span-2 grid-cols-[2rem_minmax(0,1fr)] justify-items-stretch text-left",
        orientation === "horizontal" &&
          responsive &&
          "@max-md/stepper:row-span-2 @max-md/stepper:grid-cols-[2rem_minmax(0,1fr)] @max-md/stepper:justify-items-stretch @max-md/stepper:text-left",
        skinSlot("stepper", "trigger", { state, disabled: isDisabled, invalid }),
        className,
      )}
      onClick={(event) => {
        onClick?.(event);
        if (!event.defaultPrevented && !isDisabled) selectStep(step);
      }}
    />
  );
}

export function StepperIndicator({ className, children, ...props }: StepperIndicatorProps) {
  const { orientation, responsive } = useStepper();
  const { step, state, disabled, invalid } = useStepperItem();
  const stateLabel = indicatorStateLabel(state, invalid);
  const status = [`Step ${step + 1}`, stateLabel, disabled ? "disabled" : null].filter(Boolean).join(", ");
  const indicatorContent = children ?? defaultIndicatorContent(step, state, invalid);

  return (
    <span
      {...props}
      data-control-ui="stepper"
      data-slot="indicator"
      data-state={state}
      data-invalid={invalid ? "true" : undefined}
      className={cn(
        "relative z-10 flex size-8 shrink-0 items-center justify-center rounded-full border bg-background text-xs font-semibold tabular-nums text-muted-foreground transition-[color,background-color,border-color,box-shadow] duration-[var(--duration-base)] ease-[var(--ease-standard)]",
        state === "complete" && !invalid && "border-primary bg-primary text-primary-foreground",
        state === "current" && !invalid && "border-primary text-primary-text ring-4 ring-primary/10",
        invalid && "border-dashed border-destructive bg-destructive/10 text-destructive-text",
        partLayout(orientation, responsive, "indicator"),
        skinSlot("stepper", "indicator", { state, invalid }),
        className,
      )}
    >
      <span aria-hidden="true">{indicatorContent}</span>
      <span className="sr-only">{status}</span>
    </span>
  );
}

export function StepperSeparator({ className, ...props }: StepperSeparatorProps) {
  const { orientation, responsive } = useStepper();
  const { state, invalid } = useStepperItem();
  return (
    <span
      {...props}
      data-control-ui="stepper"
      data-slot="separator"
      data-state={state}
      aria-hidden="true"
      className={cn(
        "absolute bg-border transition-colors duration-[var(--duration-base)] ease-[var(--ease-standard)]",
        state === "complete" && !invalid && "bg-primary",
        orientation === "horizontal" && "top-4 right-[calc(-50%+1rem)] left-[calc(50%+1rem)] h-px",
        orientation === "vertical" && "top-8 bottom-0 left-4 w-px",
        orientation === "horizontal" &&
          responsive &&
          "@max-md/stepper:top-8 @max-md/stepper:right-auto @max-md/stepper:bottom-0 @max-md/stepper:left-4 @max-md/stepper:h-auto @max-md/stepper:w-px",
        skinSlot("stepper", "separator", { state, invalid }),
        className,
      )}
    />
  );
}

export function StepperTitle({ className, ...props }: StepperTitleProps) {
  const { orientation, responsive } = useStepper();
  const { titleId } = useStepperItem();
  return (
    <span
      {...props}
      id={titleId}
      data-control-ui="stepper"
      data-slot="title"
      className={cn(
        "min-w-0 text-sm font-medium leading-tight text-foreground",
        partLayout(orientation, responsive, "title"),
        skinSlot("stepper", "title", {}),
        className,
      )}
    />
  );
}

export function StepperDescription({ className, ...props }: StepperDescriptionProps) {
  const { orientation, responsive } = useStepper();
  return (
    <span
      {...props}
      data-control-ui="stepper"
      data-slot="description"
      className={cn(
        "min-w-0 text-sm leading-relaxed text-muted-foreground",
        partLayout(orientation, responsive, "description"),
        skinSlot("stepper", "description", {}),
        className,
      )}
    />
  );
}

export function StepperContent({ step, keepMounted = true, className, ...props }: StepperContentProps) {
  const { value, contentMode, baseId } = useStepper();
  const active = value === step;
  const hidden = contentMode === "current" && !active;
  if (hidden && !keepMounted) return null;

  return (
    <section
      {...props}
      id={`${baseId}-step-${step}-content`}
      data-control-ui="stepper"
      data-slot="content"
      data-state={active ? "active" : "inactive"}
      hidden={hidden}
      aria-labelledby={`${baseId}-step-${step}-title`}
      className={cn("mt-6 outline-none", skinSlot("stepper", "content", { active }), className)}
    />
  );
}

On this page

  • Preview
  • Examples
  • Composition
  • Installation
  • Source
  1. 1Step 1ConnectLink the repository.
  2. 2Step 2ConfigureSet the build options.
  3. 3Step 3PublishDeploy the project.

Add the public details used to identify this project across the workspace.

Invite collaborators and decide which team members can edit the project.

Review the project configuration and publish when everything looks right.