Steps
A shadcn-style steps component built with Ark UI primitives.
First – Contact Info
Second – Date & Time
Third – Select Rooms
export { default } from "./steps-basic";
Installation
npx shadcn@latest add @ark-cn/stepsInstall the dependency required by this primitive:
npm install @ark-ui/reactCopy the component source into your app:
"use client";
import { Steps as StepsPrimitive, useSteps } from "@ark-ui/react/steps";
import type { ComponentProps } from "react";
import { cn } from "@/lib/utils";
export type StepsProps = StepsPrimitive.RootProps;
export const Steps = ({ className, ...props }: StepsProps) => (
<StepsPrimitive.Root
data-slot="steps"
className={cn(
"flex w-full",
"data-[orientation=horizontal]:flex-col data-[orientation=horizontal]:gap-4",
"data-[orientation=vertical]:flex-row data-[orientation=vertical]:gap-6",
className,
)}
{...props}
/>
);
export const StepsList = ({
className,
...props
}: StepsPrimitive.ListProps) => (
<StepsPrimitive.List
data-slot="steps-list"
className={cn(
"flex items-center gap-2",
"data-[orientation=vertical]:flex-col data-[orientation=vertical]:items-start data-[orientation=vertical]:gap-0",
className,
)}
{...props}
/>
);
export const StepsItem = ({
className,
...props
}: StepsPrimitive.ItemProps) => (
<StepsPrimitive.Item
data-slot="steps-item"
className={cn(
"group relative flex flex-1 items-center gap-2",
"last:flex-initial last:**:data-[part=separator]:hidden",
"data-disabled:pointer-events-none data-disabled:opacity-50",
"data-[orientation=vertical]:items-start data-[orientation=vertical]:pb-8 data-[orientation=vertical]:last:pb-0",
className,
)}
{...props}
/>
);
export const StepsTrigger = ({
className,
...props
}: StepsPrimitive.TriggerProps) => (
<StepsPrimitive.Trigger
data-slot="steps-trigger"
className={cn(
"inline-flex cursor-pointer items-center gap-3 rounded-md bg-transparent p-0 text-left outline-none transition-all",
"focus-visible:ring-[3px] focus-visible:ring-ring/50",
"disabled:pointer-events-none disabled:opacity-50",
className,
)}
{...props}
/>
);
export const StepsIndicator = ({
className,
children,
...props
}: StepsPrimitive.IndicatorProps) => (
<StepsPrimitive.Indicator
data-slot="steps-indicator"
className={cn(
"inline-flex size-8 shrink-0 items-center justify-center rounded-full text-sm font-medium transition-colors",
"data-incomplete:bg-muted data-incomplete:text-muted-foreground",
"data-current:bg-primary data-current:text-primary-foreground",
"data-complete:bg-accent data-complete:text-accent-foreground",
"[&_svg]:size-4 [&_svg]:shrink-0",
className,
)}
{...props}
>
{children}
</StepsPrimitive.Indicator>
);
export const StepsSeparator = ({
className,
...props
}: StepsPrimitive.SeparatorProps) => (
<StepsPrimitive.Separator
data-slot="steps-separator"
className={cn(
"shrink-0 rounded-full bg-muted transition-colors",
"data-complete:bg-primary",
"data-[orientation=horizontal]:mx-2 data-[orientation=horizontal]:h-0.5 data-[orientation=horizontal]:flex-1",
"data-[orientation=vertical]:absolute data-[orientation=vertical]:left-3.75 data-[orientation=vertical]:top-10 data-[orientation=vertical]:bottom-2 data-[orientation=vertical]:w-0.5",
className,
)}
{...props}
/>
);
export const StepsContent = ({
className,
...props
}: StepsPrimitive.ContentProps) => (
<StepsPrimitive.Content
data-slot="steps-content"
className={cn(
"rounded-lg border border-border p-4 text-sm",
"data-[orientation=vertical]:flex-1",
className,
)}
{...props}
/>
);
export const StepsCompletedContent = ({
className,
...props
}: StepsPrimitive.CompletedContentProps) => (
<StepsPrimitive.CompletedContent
data-slot="steps-completed-content"
className={cn(
"flex items-center justify-center rounded-lg border border-border p-6 text-center text-sm font-medium text-primary",
"data-[orientation=vertical]:flex-1",
className,
)}
{...props}
/>
);
export const StepsTitle = ({ className, ...props }: ComponentProps<"span">) => (
<span
data-slot="steps-title"
className={cn("text-sm font-semibold whitespace-nowrap", className)}
{...props}
/>
);
export const StepsDescription = ({
className,
...props
}: ComponentProps<"span">) => (
<span
data-slot="steps-description"
className={cn("text-xs text-muted-foreground", className)}
{...props}
/>
);
export const StepsPrevTrigger = ({
className,
...props
}: StepsPrimitive.PrevTriggerProps) => (
<StepsPrimitive.PrevTrigger
data-slot="steps-prev-trigger"
className={className}
{...props}
/>
);
export const StepsNextTrigger = ({
className,
...props
}: StepsPrimitive.NextTriggerProps) => (
<StepsPrimitive.NextTrigger
data-slot="steps-next-trigger"
className={className}
{...props}
/>
);
export const StepsProgress = ({
className,
...props
}: StepsPrimitive.ProgressProps) => (
<StepsPrimitive.Progress
data-slot="steps-progress"
className={cn(
"h-1 w-full overflow-hidden rounded-full bg-muted",
className,
)}
{...props}
/>
);
export const StepsContext = StepsPrimitive.Context;
export const StepsRootProvider = (props: StepsPrimitive.RootProviderProps) => (
<StepsPrimitive.RootProvider {...props} />
);
export type {
UseStepsProps,
UseStepsReturn,
} from "@ark-ui/react/steps";
export { useSteps };
Update import aliases to match your project setup.
Usage
import * as Steps from "@/components/ui/steps"Read exported parts in src/components/ui/steps.tsx and compose the primitive according to the Ark UI pattern for this component.
Examples
Basic
First – Contact Info
Second – Date & Time
Third – Select Rooms
import { Check } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Steps,
StepsCompletedContent,
StepsContent,
StepsContext,
StepsDescription,
StepsIndicator,
StepsItem,
StepsList,
StepsNextTrigger,
StepsPrevTrigger,
StepsSeparator,
StepsTitle,
StepsTrigger,
} from "@/components/ui/steps";
const STEPS_ITEMS = [
{ title: "First", description: "Contact Info" },
{ title: "Second", description: "Date & Time" },
{ title: "Third", description: "Select Rooms" },
] as const;
const StepsBasicDemo = () => (
<Steps count={STEPS_ITEMS.length}>
<StepsContext>
{(api) => (
<>
<StepsList>
{STEPS_ITEMS.map((item, index) => {
const state = api.getItemState({ index });
return (
<StepsItem index={index} key={item.title}>
<StepsTrigger>
<StepsIndicator>
{state.completed ? (
<Check className="size-4" />
) : (
index + 1
)}
</StepsIndicator>
<div className="flex min-w-0 flex-col">
<StepsTitle>{item.title}</StepsTitle>
<StepsDescription>{item.description}</StepsDescription>
</div>
</StepsTrigger>
<StepsSeparator />
</StepsItem>
);
})}
</StepsList>
{STEPS_ITEMS.map((item, index) => (
<StepsContent index={index} key={item.title}>
<p className="text-muted-foreground">
{item.title} – {item.description}
</p>
</StepsContent>
))}
<StepsCompletedContent>
Steps Complete — Thank you for filling out the form!
</StepsCompletedContent>
<div className="mt-4 flex w-full items-center justify-between">
<StepsPrevTrigger asChild>
<Button variant="outline" size="sm">
Previous
</Button>
</StepsPrevTrigger>
<span className="text-muted-foreground text-sm">
Step {api.value + 1} of {api.count}
</span>
<StepsNextTrigger asChild>
<Button size="sm">Next</Button>
</StepsNextTrigger>
</div>
</>
)}
</StepsContext>
</Steps>
);
export default StepsBasicDemo;
Controlled
First – Contact Info
Second – Date & Time
Third – Select Rooms
import { Check } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Steps,
StepsCompletedContent,
StepsContent,
StepsContext,
StepsDescription,
StepsIndicator,
StepsItem,
StepsList,
StepsNextTrigger,
StepsPrevTrigger,
StepsSeparator,
StepsTitle,
StepsTrigger,
} from "@/components/ui/steps";
const STEPS_ITEMS = [
{ title: "First", description: "Contact Info" },
{ title: "Second", description: "Date & Time" },
{ title: "Third", description: "Select Rooms" },
] as const;
const StepsControlledDemo = () => {
const [step, setStep] = useState(0);
return (
<Steps
count={STEPS_ITEMS.length}
onStepChange={(details) => setStep(details.step)}
step={step}
>
<StepsContext>
{(api) => (
<>
<StepsList>
{STEPS_ITEMS.map((item, index) => {
const state = api.getItemState({ index });
return (
<StepsItem index={index} key={item.title}>
<StepsTrigger>
<StepsIndicator>
{state.completed ? (
<Check className="size-4" />
) : (
index + 1
)}
</StepsIndicator>
<div className="flex min-w-0 flex-col">
<StepsTitle>{item.title}</StepsTitle>
<StepsDescription>{item.description}</StepsDescription>
</div>
</StepsTrigger>
<StepsSeparator />
</StepsItem>
);
})}
</StepsList>
{STEPS_ITEMS.map((item, index) => (
<StepsContent index={index} key={item.title}>
<p className="text-muted-foreground">
{item.title} – {item.description}
</p>
</StepsContent>
))}
<StepsCompletedContent>
Steps Complete — Thank you!
</StepsCompletedContent>
<div className="mt-4 flex w-full items-center justify-between">
<StepsPrevTrigger asChild>
<Button variant="outline" size="sm">
Previous
</Button>
</StepsPrevTrigger>
<span className="text-muted-foreground text-sm">
Step {api.value + 1} of {api.count}
</span>
<StepsNextTrigger asChild>
<Button size="sm">Next</Button>
</StepsNextTrigger>
</div>
</>
)}
</StepsContext>
</Steps>
);
};
export default StepsControlledDemo;
Root provider
First – Contact Info
Second – Date & Time
Third – Select Rooms
import { Check } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
StepsCompletedContent,
StepsContent,
StepsContext,
StepsIndicator,
StepsItem,
StepsList,
StepsNextTrigger,
StepsPrevTrigger,
StepsRootProvider,
StepsSeparator,
StepsTitle,
StepsTrigger,
useSteps,
} from "@/components/ui/steps";
const STEPS_ITEMS = [
{ title: "First", description: "Contact Info" },
{ title: "Second", description: "Date & Time" },
{ title: "Third", description: "Select Rooms" },
] as const;
const StepsRootProviderDemo = () => {
const store = useSteps({ count: STEPS_ITEMS.length });
return (
<div className="flex w-full flex-col gap-3">
<div className="flex flex-wrap items-center gap-2">
<Button
onClick={() => store.goToPrevStep()}
size="sm"
variant="outline"
>
Prev
</Button>
<Button
onClick={() => store.goToNextStep()}
size="sm"
variant="outline"
>
Next
</Button>
<Button onClick={() => store.resetStep()} size="sm" variant="outline">
Reset
</Button>
<span className="text-muted-foreground text-xs">
step:{" "}
<span className="font-medium text-foreground">{store.value + 1}</span>
</span>
</div>
<StepsRootProvider value={store}>
<StepsContext>
{(api) => (
<>
<StepsList>
{STEPS_ITEMS.map((item, index) => {
const state = api.getItemState({ index });
return (
<StepsItem index={index} key={item.title}>
<StepsTrigger>
<StepsIndicator>
{state.completed ? (
<Check className="size-4" />
) : (
index + 1
)}
</StepsIndicator>
<StepsTitle>{item.title}</StepsTitle>
</StepsTrigger>
<StepsSeparator />
</StepsItem>
);
})}
</StepsList>
{STEPS_ITEMS.map((item, index) => (
<StepsContent index={index} key={item.title}>
<p className="text-muted-foreground">
{item.title} – {item.description}
</p>
</StepsContent>
))}
<StepsCompletedContent>All done!</StepsCompletedContent>
<div className="flex items-center justify-between">
<StepsPrevTrigger asChild>
<Button variant="outline" size="sm">
Previous
</Button>
</StepsPrevTrigger>
<span className="text-muted-foreground text-sm">
Step {api.value + 1} of {api.count}
</span>
<StepsNextTrigger asChild>
<Button size="sm">Next</Button>
</StepsNextTrigger>
</div>
</>
)}
</StepsContext>
</StepsRootProvider>
</div>
);
};
export default StepsRootProviderDemo;
Vertical
Order Placed
Your order has been successfully placed
This is the content for Order Placed. You can add forms, information, or any other content here.
Processing
We're preparing your items for shipment
This is the content for Processing. You can add forms, information, or any other content here.
Shipped
Your order is on its way to you
This is the content for Shipped. You can add forms, information, or any other content here.
Delivered
Order delivered to your address
This is the content for Delivered. You can add forms, information, or any other content here.
import { Check } from "lucide-react";
import {
Steps,
StepsCompletedContent,
StepsContent,
StepsContext,
StepsDescription,
StepsIndicator,
StepsItem,
StepsList,
StepsSeparator,
StepsTitle,
StepsTrigger,
} from "@/components/ui/steps";
const TIMELINE_ITEMS = [
{
title: "Order Placed",
description: "Your order has been successfully placed",
},
{
title: "Processing",
description: "We're preparing your items for shipment",
},
{ title: "Shipped", description: "Your order is on its way to you" },
{ title: "Delivered", description: "Order delivered to your address" },
] as const;
const StepsVerticalDemo = () => (
<Steps count={TIMELINE_ITEMS.length} defaultStep={2} orientation="vertical">
<StepsContext>
{(api) => (
<>
<StepsList>
{TIMELINE_ITEMS.map((item, index) => {
const state = api.getItemState({ index });
return (
<StepsItem index={index} key={item.title}>
<StepsTrigger>
<StepsIndicator>
{state.completed ? (
<Check className="size-4" />
) : (
index + 1
)}
</StepsIndicator>
<div className="flex min-w-0 flex-col">
<StepsTitle>{item.title}</StepsTitle>
<StepsDescription>{item.description}</StepsDescription>
</div>
</StepsTrigger>
<StepsSeparator />
</StepsItem>
);
})}
</StepsList>
{TIMELINE_ITEMS.map((item, index) => (
<StepsContent index={index} key={item.title}>
<div className="flex flex-col gap-2">
<h4 className="font-semibold text-foreground">{item.title}</h4>
<p className="text-muted-foreground text-sm">
{item.description}
</p>
<p className="text-muted-foreground text-sm">
This is the content for {item.title}. You can add forms,
information, or any other content here.
</p>
</div>
</StepsContent>
))}
<StepsCompletedContent>All steps complete!</StepsCompletedContent>
</>
)}
</StepsContext>
</Steps>
);
export default StepsVerticalDemo;
Form wizard
Review your information before submitting.
import { Check } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Steps,
StepsCompletedContent,
StepsContent,
StepsContext,
StepsDescription,
StepsIndicator,
StepsItem,
StepsList,
StepsNextTrigger,
StepsPrevTrigger,
StepsSeparator,
StepsTitle,
StepsTrigger,
} from "@/components/ui/steps";
const FORM_STEPS = [
{ title: "Account Setup", description: "Create your account" },
{ title: "Profile Info", description: "Complete your profile" },
{ title: "Review", description: "Review your information" },
] as const;
const StepsFormWizardDemo = () => (
<Steps count={FORM_STEPS.length}>
<StepsContext>
{(api) => (
<>
<StepsList>
{FORM_STEPS.map((item, index) => {
const state = api.getItemState({ index });
return (
<StepsItem index={index} key={item.title}>
<StepsTrigger>
<StepsIndicator>
{state.completed ? (
<Check className="size-4" />
) : (
index + 1
)}
</StepsIndicator>
<div className="flex min-w-0 flex-col">
<StepsTitle>{item.title}</StepsTitle>
<StepsDescription>{item.description}</StepsDescription>
</div>
</StepsTrigger>
<StepsSeparator />
</StepsItem>
);
})}
</StepsList>
<StepsContent index={0}>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1.5">
<label className="font-medium text-sm">Username</label>
<input
className="h-9 rounded-md border border-input bg-transparent px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
placeholder="Enter username"
/>
</div>
<div className="flex flex-col gap-1.5">
<label className="font-medium text-sm">Email</label>
<input
className="h-9 rounded-md border border-input bg-transparent px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
placeholder="Enter email"
type="email"
/>
</div>
</div>
</StepsContent>
<StepsContent index={1}>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1.5">
<label className="font-medium text-sm">Bio</label>
<textarea
className="min-h-20 rounded-md border border-input bg-transparent px-3 py-2 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
placeholder="Tell us about yourself"
/>
</div>
</div>
</StepsContent>
<StepsContent index={2}>
<p className="text-muted-foreground">
Review your information before submitting.
</p>
</StepsContent>
<StepsCompletedContent>
Account created successfully!
</StepsCompletedContent>
<div className="flex items-center justify-between">
<StepsPrevTrigger asChild>
<Button variant="outline" size="sm">
Previous
</Button>
</StepsPrevTrigger>
<span className="text-muted-foreground text-sm">
Step {api.value + 1} of {api.count}
</span>
<StepsNextTrigger asChild>
<Button size="sm">Next</Button>
</StepsNextTrigger>
</div>
</>
)}
</StepsContext>
</Steps>
);
export default StepsFormWizardDemo;
Personal form
import { Check } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Steps,
StepsCompletedContent,
StepsContent,
StepsContext,
StepsDescription,
StepsIndicator,
StepsItem,
StepsList,
StepsNextTrigger,
StepsPrevTrigger,
StepsSeparator,
StepsTitle,
StepsTrigger,
} from "@/components/ui/steps";
const PERSONAL_STEPS = [
{ title: "Personal Details", description: "Enter your basic information" },
{ title: "About You", description: "Tell us more about yourself" },
{ title: "Professional Info", description: "Add your professional details" },
] as const;
const StepsPersonalFormDemo = () => (
<Steps count={PERSONAL_STEPS.length}>
<StepsContext>
{(api) => (
<>
<StepsList>
{PERSONAL_STEPS.map((item, index) => {
const state = api.getItemState({ index });
return (
<StepsItem index={index} key={item.title}>
<StepsTrigger>
<StepsIndicator>
{state.completed ? (
<Check className="size-4" />
) : (
index + 1
)}
</StepsIndicator>
<div className="flex min-w-0 flex-col">
<StepsTitle>{item.title}</StepsTitle>
<StepsDescription>{item.description}</StepsDescription>
</div>
</StepsTrigger>
<StepsSeparator />
</StepsItem>
);
})}
</StepsList>
<StepsContent index={0}>
<div className="flex flex-col gap-4">
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col gap-1.5">
<label className="font-medium text-sm">First Name</label>
<input
className="h-9 rounded-md border border-input bg-transparent px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
defaultValue="John"
/>
</div>
<div className="flex flex-col gap-1.5">
<label className="font-medium text-sm">Last Name</label>
<input
className="h-9 rounded-md border border-input bg-transparent px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
defaultValue="Doe"
/>
</div>
</div>
<div className="flex flex-col gap-1.5">
<label className="font-medium text-sm">Email</label>
<input
className="h-9 rounded-md border border-input bg-transparent px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
defaultValue="john.doe@example.com"
type="email"
/>
</div>
</div>
</StepsContent>
<StepsContent index={1}>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1.5">
<label className="font-medium text-sm">Bio</label>
<textarea
className="min-h-20 rounded-md border border-input bg-transparent px-3 py-2 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
placeholder="Tell us more about yourself"
/>
</div>
</div>
</StepsContent>
<StepsContent index={2}>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1.5">
<label className="font-medium text-sm">Specialization</label>
<input
className="h-9 rounded-md border border-input bg-transparent px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
placeholder="e.g. Frontend, Backend"
/>
</div>
</div>
</StepsContent>
<StepsCompletedContent>
Profile complete — Welcome aboard!
</StepsCompletedContent>
<div className="flex items-center justify-between">
<StepsPrevTrigger asChild>
<Button variant="outline" size="sm">
Previous
</Button>
</StepsPrevTrigger>
<span className="text-muted-foreground text-sm">
Step {api.value + 1} of {api.count}
</span>
<StepsNextTrigger asChild>
<Button size="sm">Next</Button>
</StepsNextTrigger>
</div>
</>
)}
</StepsContext>
</Steps>
);
export default StepsPersonalFormDemo;
Context
First – Contact Info
Second – Date & Time
Third – Select Rooms
import { Check } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Steps,
StepsCompletedContent,
StepsContent,
StepsContext,
StepsIndicator,
StepsItem,
StepsList,
StepsNextTrigger,
StepsPrevTrigger,
StepsSeparator,
StepsTitle,
StepsTrigger,
} from "@/components/ui/steps";
const STEPS_ITEMS = [
{ title: "First", description: "Contact Info" },
{ title: "Second", description: "Date & Time" },
{ title: "Third", description: "Select Rooms" },
] as const;
const StepsContextDemo = () => (
<Steps count={STEPS_ITEMS.length}>
<StepsContext>
{(api) => (
<>
<StepsList>
{STEPS_ITEMS.map((item, index) => {
const state = api.getItemState({ index });
return (
<StepsItem index={index} key={item.title}>
<StepsTrigger>
<StepsIndicator>
{state.completed ? (
<Check className="size-4" />
) : (
index + 1
)}
</StepsIndicator>
<StepsTitle>{item.title}</StepsTitle>
</StepsTrigger>
<StepsSeparator />
</StepsItem>
);
})}
</StepsList>
{STEPS_ITEMS.map((item, index) => (
<StepsContent index={index} key={item.title}>
<p className="text-muted-foreground">
{item.title} – {item.description}
</p>
</StepsContent>
))}
<StepsCompletedContent>All done!</StepsCompletedContent>
<div className="flex items-center justify-between gap-2">
<span className="text-muted-foreground text-xs">
Progress:{" "}
<span className="font-medium text-foreground">
{api.percent.toFixed(0)}%
</span>{" "}
· completed:{" "}
<span className="font-medium text-foreground">
{String(api.isCompleted)}
</span>
</span>
<div className="flex gap-2">
<StepsPrevTrigger asChild>
<Button variant="outline" size="sm">
Back
</Button>
</StepsPrevTrigger>
<StepsNextTrigger asChild>
<Button size="sm">Next</Button>
</StepsNextTrigger>
</div>
</div>
</>
)}
</StepsContext>
</Steps>
);
export default StepsContextDemo;
Linear
First – Contact Info
Second – Date & Time
Third – Select Rooms
import { Check } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Steps,
StepsCompletedContent,
StepsContent,
StepsContext,
StepsDescription,
StepsIndicator,
StepsItem,
StepsList,
StepsNextTrigger,
StepsPrevTrigger,
StepsSeparator,
StepsTitle,
StepsTrigger,
} from "@/components/ui/steps";
const STEPS_ITEMS = [
{ title: "First", description: "Contact Info" },
{ title: "Second", description: "Date & Time" },
{ title: "Third", description: "Select Rooms" },
] as const;
const StepsLinearDemo = () => {
const [invalidStep, setInvalidStep] = useState<number | null>(null);
return (
<div className="flex w-full flex-col gap-3">
{invalidStep !== null ? (
<div className="rounded-md border border-destructive/50 bg-destructive/10 px-3 py-2 text-destructive text-xs">
Step {invalidStep + 1} is invalid — complete it before moving
forward.
</div>
) : null}
<Steps
count={STEPS_ITEMS.length}
isStepValid={(index) => index !== 1}
linear
onStepInvalid={(details) => setInvalidStep(details.step)}
onStepChange={() => setInvalidStep(null)}
>
<StepsContext>
{(api) => (
<>
<StepsList>
{STEPS_ITEMS.map((item, index) => {
const state = api.getItemState({ index });
return (
<StepsItem index={index} key={item.title}>
<StepsTrigger>
<StepsIndicator>
{state.completed ? (
<Check className="size-4" />
) : (
index + 1
)}
</StepsIndicator>
<div className="flex min-w-0 flex-col">
<StepsTitle>{item.title}</StepsTitle>
<StepsDescription>
{item.description}
</StepsDescription>
</div>
</StepsTrigger>
<StepsSeparator />
</StepsItem>
);
})}
</StepsList>
{STEPS_ITEMS.map((item, index) => (
<StepsContent index={index} key={item.title}>
<p className="text-muted-foreground">
{item.title} – {item.description}
</p>
</StepsContent>
))}
<StepsCompletedContent>All done!</StepsCompletedContent>
<div className="flex items-center justify-between">
<StepsPrevTrigger asChild>
<Button variant="outline" size="sm">
Previous
</Button>
</StepsPrevTrigger>
<span className="text-muted-foreground text-sm">
Step {api.value + 1} of {api.count}
</span>
<StepsNextTrigger asChild>
<Button size="sm">Next</Button>
</StepsNextTrigger>
</div>
</>
)}
</StepsContext>
</Steps>
</div>
);
};
export default StepsLinearDemo;
Nested
Nested steps inside step content:
Inner content 1
Inner content 2
Outer step B content
import { Check } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Steps,
StepsCompletedContent,
StepsContent,
StepsContext,
StepsIndicator,
StepsItem,
StepsList,
StepsNextTrigger,
StepsPrevTrigger,
StepsSeparator,
StepsTitle,
StepsTrigger,
} from "@/components/ui/steps";
const StepsNestedDemo = () => (
<Steps count={2}>
<StepsContext>
{(api) => (
<>
<StepsList>
{["Outer A", "Outer B"].map((title, index) => {
const state = api.getItemState({ index });
return (
<StepsItem index={index} key={title}>
<StepsTrigger>
<StepsIndicator>
{state.completed ? (
<Check className="size-4" />
) : (
index + 1
)}
</StepsIndicator>
<StepsTitle>{title}</StepsTitle>
</StepsTrigger>
<StepsSeparator />
</StepsItem>
);
})}
</StepsList>
<StepsContent index={0}>
<div className="flex w-full flex-col gap-3">
<p className="text-muted-foreground text-sm">
Nested steps inside step content:
</p>
<Steps count={2} className="w-full">
<StepsContext>
{(innerApi) => (
<>
<StepsList>
{["Inner 1", "Inner 2"].map((title, index) => {
const state = innerApi.getItemState({ index });
return (
<StepsItem index={index} key={title}>
<StepsTrigger>
<StepsIndicator>
{state.completed ? (
<Check className="size-4" />
) : (
index + 1
)}
</StepsIndicator>
<StepsTitle>{title}</StepsTitle>
</StepsTrigger>
<StepsSeparator />
</StepsItem>
);
})}
</StepsList>
<StepsContent index={0}>
<p className="text-muted-foreground">Inner content 1</p>
</StepsContent>
<StepsContent index={1}>
<p className="text-muted-foreground">Inner content 2</p>
</StepsContent>
<StepsCompletedContent>
Inner complete!
</StepsCompletedContent>
<div className="flex justify-end gap-2">
<StepsPrevTrigger asChild>
<Button variant="outline" size="sm">
Back
</Button>
</StepsPrevTrigger>
<StepsNextTrigger asChild>
<Button size="sm">Next</Button>
</StepsNextTrigger>
</div>
</>
)}
</StepsContext>
</Steps>
</div>
</StepsContent>
<StepsContent index={1}>
<p className="text-muted-foreground">Outer step B content</p>
</StepsContent>
<StepsCompletedContent>
All outer steps complete!
</StepsCompletedContent>
<div className="flex justify-end gap-2">
<StepsPrevTrigger asChild>
<Button variant="outline" size="sm">
Back
</Button>
</StepsPrevTrigger>
<StepsNextTrigger asChild>
<Button size="sm">Next</Button>
</StepsNextTrigger>
</div>
</>
)}
</StepsContext>
</Steps>
);
export default StepsNestedDemo;
Horizontal centered
import { BookUser, Check, CreditCard, Truck } from "lucide-react";
import {
Steps,
StepsContext,
StepsDescription,
StepsIndicator,
StepsItem,
StepsList,
StepsSeparator,
StepsTitle,
StepsTrigger,
} from "@/components/ui/steps";
import { cn } from "@/lib/utils";
const CENTERED_STEPS: {
title: string;
description: string;
icon?: typeof BookUser;
}[] = [
{ title: "Address", description: "Add your address", icon: BookUser },
{ title: "Shipping", description: "Set your preferred", icon: Truck },
{ title: "Payment", description: "Add any payment", icon: CreditCard },
{ title: "Checkout", description: "Confirm your order" },
];
const StepsHorizontalCenteredDemo = () => (
<Steps count={CENTERED_STEPS.length}>
<StepsContext>
{(api) => (
<StepsList>
{CENTERED_STEPS.map((item, index) => {
const state = api.getItemState({ index });
const Icon = item.icon;
return (
<StepsItem
index={index}
key={item.title}
className="flex-col items-center justify-center gap-0"
>
<StepsTrigger className="static z-1">
<StepsIndicator>
{state.completed ? (
<Check className="size-4" />
) : Icon ? (
<Icon className="size-4" />
) : (
index + 1
)}
</StepsIndicator>
</StepsTrigger>
<StepsSeparator
className={cn(
"z-0 absolute left-1/2 -right-1/2 top-4 data-[orientation=horizontal]:mx-0",
index === CENTERED_STEPS.length - 2 && "-right-1/4",
)}
/>
<div className="mt-2 flex flex-col items-center text-center">
<StepsTitle>{item.title}</StepsTitle>
<StepsDescription>{item.description}</StepsDescription>
</div>
</StepsItem>
);
})}
</StepsList>
)}
</StepsContext>
</Steps>
);
export default StepsHorizontalCenteredDemo;
Vertical buttons
import { Check, Circle, Dot } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Steps,
StepsContext,
StepsDescription,
StepsItem,
StepsList,
StepsSeparator,
StepsTitle,
StepsTrigger,
} from "@/components/ui/steps";
import { cn } from "@/lib/utils";
const DETAILS_STEPS = [
{
title: "Your details",
description:
"Provide your name and email address. We will use this information to create your account",
},
{
title: "Company details",
description:
"A few details about your company will help us personalize your experience",
},
{
title: "Invite your team",
description:
"Start collaborating with your team by inviting them to join your account. You can skip this step and invite them later",
},
] as const;
const StepsVerticalButtonsDemo = () => (
<Steps
count={DETAILS_STEPS.length}
orientation="vertical"
className="mx-auto flex-col! gap-0! max-w-md"
>
<StepsContext>
{(api) => (
<StepsList className="w-full">
{DETAILS_STEPS.map((item, index) => {
const state = api.getItemState({ index });
return (
<StepsItem
index={index}
key={item.title}
className="flex w-full items-start gap-6"
>
<StepsSeparator className="data-[orientation=vertical]:left-3.75 data-[orientation=vertical]:top-8 data-[orientation=vertical]:bottom-auto data-[orientation=vertical]:h-[calc(100%-32px)]" />
<StepsTrigger asChild>
<Button
variant={
state.completed || state.current ? "default" : "outline"
}
size="icon"
className={cn(
"z-10 shrink-0 rounded-full text-foreground before:content-[''] before:absolute before:inset-0 before:rounded-full",
state.current &&
"ring-2 ring-ring ring-offset-2 ring-offset-background",
state.completed && "bg-primary text-background",
)}
>
{state.completed ? (
<Check className="size-5" />
) : state.current ? (
<Circle />
) : (
<Dot />
)}
</Button>
</StepsTrigger>
<div className="flex flex-col gap-1">
<StepsTitle
className={cn(
"transition lg:text-base",
state.current && "text-primary",
)}
>
{item.title}
</StepsTitle>
<StepsDescription
className={cn(
"whitespace-normal text-sm transition",
state.current && "text-primary",
)}
>
{item.description}
</StepsDescription>
</div>
</StepsItem>
);
})}
</StepsList>
)}
</StepsContext>
</Steps>
);
export default StepsVerticalButtonsDemo;
Form centered
import { Check, Circle, Dot } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Steps,
StepsCompletedContent,
StepsContent,
StepsContext,
StepsDescription,
StepsItem,
StepsList,
StepsNextTrigger,
StepsPrevTrigger,
StepsSeparator,
StepsTitle,
StepsTrigger,
} from "@/components/ui/steps";
import { cn } from "@/lib/utils";
const CENTERED_FORM_STEPS = [
{ title: "Your details", description: "Provide your name and email" },
{ title: "Your password", description: "Choose a password" },
{ title: "Your Favorite Drink", description: "Choose a drink" },
] as const;
const StepsFormCenteredDemo = () => {
const [step, setStep] = useState(0);
return (
<Steps
count={CENTERED_FORM_STEPS.length}
onStepChange={(details) => setStep(details.step)}
step={step}
>
<StepsContext>
{(api) => (
<>
<StepsList>
{CENTERED_FORM_STEPS.map((item, index) => {
const state = api.getItemState({ index });
return (
<StepsItem
index={index}
key={item.title}
className="flex-col items-center justify-center gap-0"
>
<StepsSeparator
className={cn(
"z-0 absolute left-[calc(50%+16px)] -right-[calc(50%-8px)] top-3.75 data-[orientation=horizontal]:mx-0",
index === CENTERED_FORM_STEPS.length - 2 &&
"-right-[calc(25%-16px)]",
)}
/>
<StepsTrigger asChild>
<Button
variant={
state.completed || state.current
? "default"
: "outline"
}
size="icon"
className={cn(
"z-10 shrink-0 rounded-full text-foreground before:content-[''] before:absolute before:inset-0 before:rounded-full",
state.current &&
"ring-2 ring-ring ring-offset-2 ring-offset-background",
state.completed && "bg-primary text-background",
)}
>
{state.completed ? (
<Check className="size-5" />
) : state.current ? (
<Circle />
) : (
<Dot />
)}
</Button>
</StepsTrigger>
<div className="mt-5 flex flex-col items-center text-center">
<StepsTitle
className={cn(
"transition",
state.current && "text-primary",
)}
>
{item.title}
</StepsTitle>
<StepsDescription
className={cn(
"transition",
state.current && "text-primary",
)}
>
{item.description}
</StepsDescription>
</div>
</StepsItem>
);
})}
</StepsList>
<StepsContent index={0}>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1.5">
<label className="font-medium text-sm">Full Name</label>
<input
className="h-9 rounded-md border border-input bg-transparent px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
placeholder="John Doe"
type="text"
/>
</div>
</div>
</StepsContent>
<StepsContent index={1}>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1.5">
<label className="font-medium text-sm">Password</label>
<input
className="h-9 rounded-md border border-input bg-transparent px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
placeholder="********"
type="password"
/>
</div>
</div>
</StepsContent>
<StepsContent index={2}>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1.5">
<label className="font-medium text-sm">Favorite Drink</label>
<select className="h-9 rounded-md border border-input bg-transparent px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring">
<option value="">Select a drink</option>
<option value="coffee">Coffee</option>
<option value="tea">Tea</option>
<option value="soda">Soda</option>
</select>
</div>
</div>
</StepsContent>
<StepsCompletedContent>
Form submitted successfully!
</StepsCompletedContent>
<div className="flex items-center justify-between">
<StepsPrevTrigger asChild>
<Button variant="outline" size="sm">
Back
</Button>
</StepsPrevTrigger>
<div className="flex items-center gap-3">
{api.value < api.count - 1 ? (
<StepsNextTrigger asChild>
<Button size="sm">Next</Button>
</StepsNextTrigger>
) : !api.isCompleted ? (
<Button size="sm" onClick={() => api.goToNextStep()}>
Submit
</Button>
) : null}
</div>
</div>
</>
)}
</StepsContext>
</Steps>
);
};
export default StepsFormCenteredDemo;
API reference
This component mirrors the upstream Ark UI primitive.
See the ARK UI documentation for the full API.