Use steppers to visualize a group of connected actions or the order of a workflow.

import {Stepper} from "@qualcomm-ui/react/stepper"

Examples

Linear Mode

The stepper is linear by default (linear is true). Steps must be completed in order.

  • Backward navigation is always allowed
  • Previously visited steps can be re-clicked without restriction
  • Forward navigation is restricted to the immediate next step
  • canGoToStep can override forward navigation decisions
First - Contact Info
import {ChevronLeft, ChevronRight} from "lucide-react"

import {Button} from "@qualcomm-ui/react/button"
import {Stepper} from "@qualcomm-ui/react/stepper"

const items = [
  {description: "Contact Info", title: "First", value: "first"},
  {description: "Date & Time", title: "Second", value: "second"},
  {description: "Select Rooms", title: "Third", value: "third"},
]

export function StepperLinearDemo() {
  return (
    <Stepper.Root count={items.length}>
      <Stepper.List>
        {items.map((item, index) => (
          <Stepper.Item key={item.value} index={index}>
            <Stepper.Trigger>
              <Stepper.Indicator>{index + 1}</Stepper.Indicator>
              <Stepper.Label>{item.title}</Stepper.Label>
            </Stepper.Trigger>
            <Stepper.Separator />
          </Stepper.Item>
        ))}
      </Stepper.List>

      {items.map((item, index) => (
        <Stepper.Content key={item.value} index={index}>
          {item.title} - {item.description}
        </Stepper.Content>
      ))}

      <Stepper.CompletedContent>All steps completed.</Stepper.CompletedContent>

      <div className="mt-6 flex justify-between">
        <Stepper.PrevTrigger>
          <Button size="sm" startIcon={ChevronLeft} variant="outline">
            Back
          </Button>
        </Stepper.PrevTrigger>
        <Stepper.NextTrigger>
          <Button endIcon={ChevronRight} size="sm" variant="outline">
            Next
          </Button>
        </Stepper.NextTrigger>
      </div>
    </Stepper.Root>
  )
}

Non-linear Mode

Set linear to false to allow accessing any step at any time. Use the completed prop to manually track which steps are done.

canGoToStep applies to all navigation if provided. Otherwise, all navigation is allowed.

First - Contact Info

Icon

Use the <Stepper.IndicatorIcon> component to use an icon for each step's indicator.

Contact details
import type {ReactElement} from "react"

import {
  ChevronLeft,
  ChevronRight,
  ChevronsUp,
  DollarSign,
  User,
} from "lucide-react"

import {Button} from "@qualcomm-ui/react/button"
import {Stepper} from "@qualcomm-ui/react/stepper"

const items = [
  {content: "Contact details", icon: User, title: "Step 1", value: "step-1"},
  {content: "Payment info", icon: DollarSign, title: "Step 2", value: "step-2"},
  {content: "Confirmation", icon: ChevronsUp, title: "Step 3", value: "step-3"},
]

// TODO: icon API is still being finalized
export function StepperIconDemo(): ReactElement {
  return (
    <Stepper.Root count={items.length}>
      <Stepper.List>
        {items.map((item, index) => (
          <Stepper.Item key={item.value} index={index}>
            <Stepper.Trigger>
              <Stepper.Indicator>
                <Stepper.IndicatorIcon icon={item.icon} />
              </Stepper.Indicator>
              <Stepper.Label>{item.title}</Stepper.Label>
            </Stepper.Trigger>
            <Stepper.Separator />
          </Stepper.Item>
        ))}
      </Stepper.List>

      {items.map((item, index) => (
        <Stepper.Content key={item.value} index={index}>
          {item.content}
        </Stepper.Content>
      ))}

      <Stepper.CompletedContent>All steps completed.</Stepper.CompletedContent>

      <div className="mt-6 flex justify-between">
        <Stepper.PrevTrigger>
          <Button size="sm" startIcon={ChevronLeft} variant="outline">
            Back
          </Button>
        </Stepper.PrevTrigger>
        <Stepper.NextTrigger>
          <Button endIcon={ChevronRight} size="sm" variant="outline">
            Next
          </Button>
        </Stepper.NextTrigger>
      </div>
    </Stepper.Root>
  )
}

Hint

Add a hint to provide additional context below the step label.

Provide your payment details
import type {ReactElement} from "react"

import {ChevronLeft, ChevronRight} from "lucide-react"

import {Button} from "@qualcomm-ui/react/button"
import {Stepper} from "@qualcomm-ui/react/stepper"

const items = [
  {
    content: "Enter your contact information",
    hint: "Contact details",
    title: "Step 1",
    value: "step-1",
  },
  {
    content: "Provide your payment details",
    hint: "Payment info",
    title: "Step 2",
    value: "step-2",
  },
  {
    content: "Review and confirm your order",
    hint: "Confirmation",
    title: "Step 3",
    value: "step-3",
  },
]

export function StepperHintDemo(): ReactElement {
  return (
    <Stepper.Root count={items.length} defaultStep={1}>
      <Stepper.List>
        {items.map((item, index) => (
          <Stepper.Item key={item.value} index={index}>
            <Stepper.Trigger>
              <Stepper.Indicator>{index + 1}</Stepper.Indicator>
              <Stepper.Label>{item.title}</Stepper.Label>
              <Stepper.Hint>{item.hint}</Stepper.Hint>
            </Stepper.Trigger>
            <Stepper.Separator />
          </Stepper.Item>
        ))}
      </Stepper.List>

      {items.map((item, index) => (
        <Stepper.Content key={item.value} index={index}>
          {item.content}
        </Stepper.Content>
      ))}

      <Stepper.CompletedContent>All steps completed.</Stepper.CompletedContent>

      <div className="mt-6 flex justify-between">
        <Stepper.PrevTrigger>
          <Button size="sm" startIcon={ChevronLeft} variant="outline">
            Back
          </Button>
        </Stepper.PrevTrigger>
        <Stepper.NextTrigger>
          <Button endIcon={ChevronRight} size="sm" variant="outline">
            Next
          </Button>
        </Stepper.NextTrigger>
      </div>
    </Stepper.Root>
  )
}

Layout & Orientation

Stepper orientation and label placement are controlled through the orientation prop.

Horizontal

horizontal (default): The step list is centered horizontally, and the labels are stacked vertically and centered below the step indicator.

Provide your payment details
import type {ReactElement} from "react"

import {ChevronLeft, ChevronRight} from "lucide-react"

import {Button} from "@qualcomm-ui/react/button"
import {Stepper} from "@qualcomm-ui/react/stepper"

const items = [
  {
    content: "Enter your contact information",
    hint: "Contact details",
    title: "Step 1",
    value: "step-1",
  },
  {
    content: "Provide your payment details",
    hint: "Payment info",
    title: "Step 2",
    value: "step-2",
  },
  {
    content: "Review and confirm your order",
    hint: "Confirmation",
    title: "Step 3",
    value: "step-3",
  },
]

export function StepperHorizontalDemo(): ReactElement {
  return (
    <Stepper.Root count={items.length} defaultStep={1}>
      <Stepper.List>
        {items.map((item, index) => (
          <Stepper.Item key={item.value} index={index}>
            <Stepper.Trigger>
              <Stepper.Indicator>{index + 1}</Stepper.Indicator>
              <Stepper.Label>{item.title}</Stepper.Label>
              <Stepper.Hint>{item.hint}</Stepper.Hint>
            </Stepper.Trigger>
            <Stepper.Separator />
          </Stepper.Item>
        ))}
      </Stepper.List>

      {items.map((item, index) => (
        <Stepper.Content key={item.value} index={index}>
          {item.content}
        </Stepper.Content>
      ))}

      <Stepper.CompletedContent>All steps completed.</Stepper.CompletedContent>

      <div className="mt-6 flex justify-between">
        <Stepper.PrevTrigger>
          <Button size="sm" startIcon={ChevronLeft} variant="outline">
            Back
          </Button>
        </Stepper.PrevTrigger>
        <Stepper.NextTrigger>
          <Button endIcon={ChevronRight} size="sm" variant="outline">
            Next
          </Button>
        </Stepper.NextTrigger>
      </div>
    </Stepper.Root>
  )
}

Horizontal Inline

horizontal-inline is a variant of horizontal that places the step indicator and labels inline. The step items are left-aligned and take the full width of their parent container

First - Contact Info
import {ChevronLeft, ChevronRight} from "lucide-react"

import {Button} from "@qualcomm-ui/react/button"
import {Stepper} from "@qualcomm-ui/react/stepper"

const items = [
  {description: "Contact Info", title: "First", value: "first"},
  {description: "Date & Time", title: "Second", value: "second"},
  {description: "Select Rooms", title: "Third", value: "third"},
]

export function StepperHorizontalInlineDemo() {
  return (
    <Stepper.Root count={items.length} orientation="horizontal-inline">
      <Stepper.List>
        {items.map((item, index) => (
          <Stepper.Item key={item.value} index={index}>
            <Stepper.Trigger>
              <Stepper.Indicator>{index + 1}</Stepper.Indicator>
              <Stepper.Label>{item.title}</Stepper.Label>
              <Stepper.Hint>{item.description}</Stepper.Hint>
            </Stepper.Trigger>
            <Stepper.Separator />
          </Stepper.Item>
        ))}
      </Stepper.List>

      {items.map((item, index) => (
        <Stepper.Content key={item.value} index={index}>
          {item.title} - {item.description}
        </Stepper.Content>
      ))}

      <Stepper.CompletedContent>All steps completed.</Stepper.CompletedContent>

      <div className="mt-6 flex justify-between">
        <Stepper.PrevTrigger>
          <Button size="sm" startIcon={ChevronLeft} variant="outline">
            Back
          </Button>
        </Stepper.PrevTrigger>
        <Stepper.NextTrigger>
          <Button endIcon={ChevronRight} size="sm" variant="outline">
            Next
          </Button>
        </Stepper.NextTrigger>
      </div>
    </Stepper.Root>
  )
}

Horizontal Bottom Start

horizontal-bottom-start is a variant of horizontal that places the labels below each step, aligned to the start. The items are left-aligned and take the full width of their parent container.

First - Contact Info
import {ChevronLeft, ChevronRight} from "lucide-react"

import {Button} from "@qualcomm-ui/react/button"
import {Stepper} from "@qualcomm-ui/react/stepper"

const items = [
  {description: "Contact Info", title: "First", value: "first"},
  {description: "Date & Time", title: "Second", value: "second"},
  {description: "Select Rooms", title: "Third", value: "third"},
]

export function StepperHorizontalBottomStartDemo() {
  return (
    <Stepper.Root count={items.length} orientation="horizontal-bottom-start">
      <Stepper.List>
        {items.map((item, index) => (
          <Stepper.Item key={item.value} index={index}>
            <Stepper.Trigger>
              <Stepper.Indicator>{index + 1}</Stepper.Indicator>
              <Stepper.Label>{item.title}</Stepper.Label>
              <Stepper.Hint>{item.description}</Stepper.Hint>
            </Stepper.Trigger>
            <Stepper.Separator />
          </Stepper.Item>
        ))}
      </Stepper.List>

      {items.map((item, index) => (
        <Stepper.Content key={item.value} index={index}>
          {item.title} - {item.description}
        </Stepper.Content>
      ))}

      <Stepper.CompletedContent>All steps completed.</Stepper.CompletedContent>

      <div className="mt-6 flex justify-between">
        <Stepper.PrevTrigger>
          <Button size="sm" startIcon={ChevronLeft} variant="outline">
            Back
          </Button>
        </Stepper.PrevTrigger>
        <Stepper.NextTrigger>
          <Button endIcon={ChevronRight} size="sm" variant="outline">
            Next
          </Button>
        </Stepper.NextTrigger>
      </div>
    </Stepper.Root>
  )
}

Vertical

Set orientation to vertical to stack the steps vertically. The labels are centered relative to the step indicator.

Contact details
import type {ReactElement} from "react"

import {ChevronLeft, ChevronRight} from "lucide-react"

import {Button} from "@qualcomm-ui/react/button"
import {Stepper} from "@qualcomm-ui/react/stepper"

const items = [
  {content: "Contact details", title: "Step 1", value: "step-1"},
  {content: "Payment info", title: "Step 2", value: "step-2"},
  {content: "Confirmation", title: "Step 3", value: "step-3"},
]

export function StepperVerticalDemo(): ReactElement {
  return (
    <Stepper.Root
      className="min-h-96"
      count={items.length}
      orientation="vertical"
    >
      <Stepper.List>
        {items.map((item, index) => (
          <Stepper.Item key={item.value} index={index}>
            <Stepper.Trigger>
              <Stepper.Indicator>{index + 1}</Stepper.Indicator>
              <Stepper.Label>{item.title}</Stepper.Label>
              <Stepper.Hint>{item.content}</Stepper.Hint>
            </Stepper.Trigger>
            <Stepper.Separator />
          </Stepper.Item>
        ))}
      </Stepper.List>

      <div className="flex flex-col gap-4">
        {items.map((item, index) => (
          <Stepper.Content key={item.value} index={index}>
            {item.content}
          </Stepper.Content>
        ))}

        <Stepper.CompletedContent>
          All steps completed.
        </Stepper.CompletedContent>

        <div className="mt-6 flex gap-2">
          <Stepper.PrevTrigger>
            <Button size="sm" startIcon={ChevronLeft} variant="outline">
              Back
            </Button>
          </Stepper.PrevTrigger>
          <Stepper.NextTrigger>
            <Button endIcon={ChevronRight} size="sm" variant="outline">
              Next
            </Button>
          </Stepper.NextTrigger>
        </div>
      </div>
    </Stepper.Root>
  )
}

Vertical Inline

Set orientation to vertical-inline to stack the steps vertically while aligning the labels inline with the step indicator.

Contact details
import type {ReactElement} from "react"

import {ChevronLeft, ChevronRight} from "lucide-react"

import {Button} from "@qualcomm-ui/react/button"
import {Stepper} from "@qualcomm-ui/react/stepper"

const items = [
  {content: "Contact details", title: "Step 1", value: "step-1"},
  {content: "Payment info", title: "Step 2", value: "step-2"},
  {content: "Confirmation", title: "Step 3", value: "step-3"},
]

export function StepperVerticalInlineDemo(): ReactElement {
  return (
    <Stepper.Root
      className="min-h-96"
      count={items.length}
      orientation="vertical-inline"
    >
      <Stepper.List>
        {items.map((item, index) => (
          <Stepper.Item key={item.value} index={index}>
            <Stepper.Trigger>
              <Stepper.Indicator>{index + 1}</Stepper.Indicator>
              <Stepper.Label>{item.title}</Stepper.Label>
              <Stepper.Hint>{item.content}</Stepper.Hint>
            </Stepper.Trigger>
            <Stepper.Separator />
          </Stepper.Item>
        ))}
      </Stepper.List>

      <div className="flex flex-col gap-4">
        {items.map((item, index) => (
          <Stepper.Content key={item.value} index={index}>
            {item.content}
          </Stepper.Content>
        ))}

        <Stepper.CompletedContent>
          All steps completed.
        </Stepper.CompletedContent>

        <div className="mt-6 flex gap-2">
          <Stepper.PrevTrigger>
            <Button size="sm" startIcon={ChevronLeft} variant="outline">
              Back
            </Button>
          </Stepper.PrevTrigger>
          <Stepper.NextTrigger>
            <Button endIcon={ChevronRight} size="sm" variant="outline">
              Next
            </Button>
          </Stepper.NextTrigger>
        </div>
      </div>
    </Stepper.Root>
  )
}

Controlled State

Use step and onStepChange to control the active step externally.

Contact details
import {type ReactElement, useState} from "react"

import {ChevronLeft, ChevronRight} from "lucide-react"

import {Button} from "@qualcomm-ui/react/button"
import {Stepper} from "@qualcomm-ui/react/stepper"

const items = [
  {content: "Contact details", title: "Step 1", value: "step-1"},
  {content: "Payment info", title: "Step 2", value: "step-2"},
  {content: "Confirmation", title: "Step 3", value: "step-3"},
]

export function StepperControlledDemo(): ReactElement {
  const [step, setStep] = useState(0)

  return (
    <Stepper.Root count={items.length} onStepChange={setStep} step={step}>
      <Stepper.List>
        {items.map((item, index) => (
          <Stepper.Item key={item.value} index={index}>
            <Stepper.Trigger>
              <Stepper.Indicator>{index + 1}</Stepper.Indicator>
              <Stepper.Label>{item.title}</Stepper.Label>
            </Stepper.Trigger>
            <Stepper.Separator />
          </Stepper.Item>
        ))}
      </Stepper.List>

      {items.map((item, index) => (
        <Stepper.Content key={item.value} index={index}>
          {item.content}
        </Stepper.Content>
      ))}

      <Stepper.CompletedContent>All steps completed.</Stepper.CompletedContent>

      <div className="mt-6 flex justify-between">
        <Stepper.PrevTrigger>
          <Button size="sm" startIcon={ChevronLeft} variant="outline">
            Back
          </Button>
        </Stepper.PrevTrigger>
        <Stepper.NextTrigger>
          <Button endIcon={ChevronRight} size="sm" variant="outline">
            Next
          </Button>
        </Stepper.NextTrigger>
      </div>
    </Stepper.Root>
  )
}

Manual Completion

Set linear to false and use the completed prop to manually control which steps are marked as complete.

Account content
import {type ReactElement, useState} from "react"

import {ChevronLeft, ChevronRight} from "lucide-react"

import {Button} from "@qualcomm-ui/react/button"
import {Stepper} from "@qualcomm-ui/react/stepper"

const items = [
  {title: "Account", value: "account"},
  {title: "Profile", value: "profile"},
  {title: "Review", value: "review"},
]

export function StepperCompletedDemo(): ReactElement {
  const [completed, setCompleted] = useState<Record<number, boolean>>({})

  function toggleCompleted(index: number) {
    setCompleted((prev) => ({...prev, [index]: !prev[index]}))
  }

  const allCompleted = items.every((_, i) => completed[i])

  return (
    <Stepper.Root completed={completed} count={items.length} linear={false}>
      <Stepper.List>
        {items.map((item, index) => (
          <Stepper.Item key={item.value} index={index}>
            <Stepper.Trigger>
              <Stepper.Indicator>{index + 1}</Stepper.Indicator>
              <Stepper.Label>{item.title}</Stepper.Label>
            </Stepper.Trigger>
            <Stepper.Separator />
          </Stepper.Item>
        ))}
      </Stepper.List>

      {items.map((item, index) => (
        <Stepper.Content key={item.value} index={index}>
          <div className="flex items-center gap-4">
            <span>{item.title} content</span>
            <Button
              onClick={() => toggleCompleted(index)}
              size="sm"
              variant={completed[index] ? "outline" : "fill"}
            >
              {completed[index] ? "Mark Incomplete" : "Mark Complete"}
            </Button>
          </div>
        </Stepper.Content>
      ))}

      <Stepper.CompletedContent>All steps completed.</Stepper.CompletedContent>

      <div className="mt-6 flex justify-between">
        <Stepper.PrevTrigger>
          <Button size="sm" startIcon={ChevronLeft} variant="outline">
            Back
          </Button>
        </Stepper.PrevTrigger>
        <Stepper.Context>
          {({count, hasNextStep, step}) => (
            <Stepper.NextTrigger>
              <Button
                disabled={!hasNextStep || (step === count - 1 && !allCompleted)}
                endIcon={ChevronRight}
                size="sm"
                variant="outline"
              >
                Next
              </Button>
            </Stepper.NextTrigger>
          )}
        </Stepper.Context>
      </div>
    </Stepper.Root>
  )
}

Pending Steps

Use the pending prop to mark steps as pending. This is typically used to indicate that a step has been visited but not completed.

Account content
import {type ReactElement, useState} from "react"

import {ChevronLeft, ChevronRight} from "lucide-react"

import {Button} from "@qualcomm-ui/react/button"
import {Stepper} from "@qualcomm-ui/react/stepper"

const items = [
  {title: "Account", value: "account"},
  {title: "Profile", value: "profile"},
  {title: "Review", value: "review"},
]

export function StepperPendingDemo(): ReactElement {
  const [pendingStep, setPendingStep] = useState<number>(1)

  return (
    <Stepper.Root count={items.length} pending={{[pendingStep]: true}}>
      <Stepper.List>
        {items.map((item, index) => (
          <Stepper.Item key={item.value} index={index}>
            <Stepper.Trigger>
              <Stepper.Indicator>{index + 1}</Stepper.Indicator>
              <Stepper.Label>{item.title}</Stepper.Label>
            </Stepper.Trigger>
            <Stepper.Separator />
          </Stepper.Item>
        ))}
      </Stepper.List>

      {items.map((item, index) => (
        <Stepper.Content key={item.value} index={index}>
          <div className="flex items-center gap-4">
            <span>{item.title} content</span>
          </div>
        </Stepper.Content>
      ))}

      <Stepper.CompletedContent>All steps completed.</Stepper.CompletedContent>

      <Stepper.Context>
        {({step}) => (
          <div className="mt-6 flex justify-between">
            <Stepper.PrevTrigger>
              <Button size="sm" startIcon={ChevronLeft} variant="outline">
                Back
              </Button>
            </Stepper.PrevTrigger>
            <Stepper.NextTrigger>
              <Button
                endIcon={ChevronRight}
                onClick={() => setPendingStep(step + 1)}
                size="sm"
                variant="outline"
              >
                Next
              </Button>
            </Stepper.NextTrigger>
          </div>
        )}
      </Stepper.Context>
    </Stepper.Root>
  )
}

Sizes

The stepper comes in two sizes: sm and lg. The default size is lg.

Contact details
Contact details
import type {ReactElement} from "react"

import {Stepper} from "@qualcomm-ui/react/stepper"

const items = [
  {content: "Contact details", title: "Step 1", value: "step-1"},
  {content: "Payment info", title: "Step 2", value: "step-2"},
  {content: "Confirmation", title: "Step 3", value: "step-3"},
]

const sizes = ["sm", "lg"] as const

export function StepperSizesDemo(): ReactElement {
  return (
    <div className="flex w-full flex-col gap-16">
      {sizes.map((size) => (
        <Stepper.Root key={size} count={items.length} size={size}>
          <Stepper.List>
            {items.map((item, index) => (
              <Stepper.Item key={item.value} index={index}>
                <Stepper.Trigger>
                  <Stepper.Indicator>{index + 1}</Stepper.Indicator>
                  <Stepper.Label>{item.title}</Stepper.Label>
                </Stepper.Trigger>
                <Stepper.Separator />
              </Stepper.Item>
            ))}
          </Stepper.List>

          {items.map((item, index) => (
            <Stepper.Content key={item.value} index={index}>
              {item.content}
            </Stepper.Content>
          ))}
        </Stepper.Root>
      ))}
    </div>
  )
}

Validation Examples

Non-linear Form

Combine linear={false} with per-step validation to let users complete steps in any order. Use the invalid and completed props to track each step's status, and Stepper.Context to conditionally render a submit action once all steps pass.

import {type ReactElement, useState} from "react"

import {type} from "arktype"
import {ChevronLeft, ChevronRight} from "lucide-react"
import {Controller, useForm} from "react-hook-form"

import {Button} from "@qualcomm-ui/react/button"
import {Stepper} from "@qualcomm-ui/react/stepper"
import {TextInput} from "@qualcomm-ui/react/text-input"

const items = [
  {
    label: "Age range",
    name: "age" as const,
    placeholder: "25-34",
    title: "Demographics",
  },
  {
    label: "Preferred contact method",
    name: "contact" as const,
    placeholder: "Email",
    title: "Preferences",
  },
  {
    label: "Comments",
    name: "comments" as const,
    placeholder: "Tell us what you think",
    title: "Feedback",
  },
]

const schema = type({
  age: "string > 0",
  comments: "string > 0",
  contact: "string > 0",
})

type FormData = typeof schema.infer

export function StepperNonlinearFormDemo(): ReactElement {
  const [completed, setCompleted] = useState<Record<number, boolean>>({})
  const {clearErrors, control, getValues, setError} = useForm<FormData>({
    defaultValues: {age: "", comments: "", contact: ""},
    mode: "onChange",
    reValidateMode: "onChange",
  })
  const [invalid, setInvalid] = useState<Record<string, boolean>>({})

  function saveStep(index: number) {
    const field = items[index]?.name
    if (!field) {
      console.debug("no field")
      return
    }

    const result = schema(getValues())
    if (result instanceof type.errors) {
      const fieldError = result.find((e) => e.path?.[0] === field)
      if (fieldError) {
        setError(field, {message: fieldError.message})
        setInvalid((prev) => ({...prev, [index]: true}))
        setCompleted((prev) => ({...prev, [index]: false}))
        return
      }
    }

    clearErrors(field)
    setInvalid((prev) => ({...prev, [index]: false}))
    setCompleted((prev) => ({...prev, [index]: true}))
  }

  const allCompleted = items.every((_, i) => completed[i])

  return (
    <Stepper.Root
      completed={completed}
      count={items.length}
      invalid={invalid}
      linear={false}
    >
      <Stepper.List>
        {items.map((item, index) => (
          <Stepper.Item key={item.name} index={index}>
            <Stepper.Trigger>
              <Stepper.Indicator>{index + 1}</Stepper.Indicator>
              <Stepper.Label>{item.title}</Stepper.Label>
            </Stepper.Trigger>
            <Stepper.Separator />
          </Stepper.Item>
        ))}
      </Stepper.List>

      {items.map((item, index) => (
        <Stepper.Content key={item.name} index={index}>
          <div className="flex">
            <Controller
              control={control}
              name={item.name}
              render={({
                field: {onChange, ref, ...fieldProps},
                fieldState: {error},
              }) => (
                <TextInput
                  className="max-w-56"
                  errorText={error?.message}
                  inputProps={{ref}}
                  invalid={!!error}
                  label={item.label}
                  onValueChange={onChange}
                  placeholder={item.placeholder}
                  required
                  {...fieldProps}
                />
              )}
            />
          </div>
        </Stepper.Content>
      ))}

      <Stepper.CompletedContent>
        Survey submitted. Thank you for your feedback!
      </Stepper.CompletedContent>

      <Stepper.Context>
        {(api) => (
          <div className="mt-6 flex justify-between">
            <Stepper.PrevTrigger>
              <Button
                onClick={() => saveStep(api.step)}
                size="sm"
                startIcon={ChevronLeft}
                variant="outline"
              >
                Back
              </Button>
            </Stepper.PrevTrigger>

            {allCompleted ? (
              <Button
                endIcon={ChevronRight}
                onClick={() => {
                  saveStep(api.step)
                  api.goToNextStep()
                }}
                size="sm"
              >
                Submit
              </Button>
            ) : (
              <Stepper.NextTrigger>
                <Button
                  disabled={
                    !api.hasNextStep ||
                    (api.step === items.length - 1 && !allCompleted)
                  }
                  endIcon={ChevronRight}
                  onClick={() => {
                    saveStep(api.step)
                  }}
                  size="sm"
                >
                  Next
                </Button>
              </Stepper.NextTrigger>
            )}
          </div>
        )}
      </Stepper.Context>
    </Stepper.Root>
  )
}

Skippable Steps

Use isStepSkippable to mark optional steps that can be bypassed during navigation.

import type {ReactElement} from "react"

import {type} from "arktype"
import {ChevronLeft, ChevronRight} from "lucide-react"
import {Controller, useForm} from "react-hook-form"

import {Button} from "@qualcomm-ui/react/button"
import {Stepper} from "@qualcomm-ui/react/stepper"
import {TextInput} from "@qualcomm-ui/react/text-input"

const PROMO_STEP = 1

const steps = [
  {title: "Shipping", value: "shipping"},
  {title: "Promo Code", value: "promo"},
  {title: "Payment", value: "payment"},
]

const shippingSchema = type({address: "string > 0"})
const paymentSchema = type({card: "string > 0"})

type FormData = typeof shippingSchema.infer & {card: string; promo: string}

const stepSchemas: Record<number, (data: FormData) => type.errors | undefined> =
  {
    0: (data) => {
      const result = shippingSchema(data)
      return result instanceof type.errors ? result : undefined
    },
    2: (data) => {
      const result = paymentSchema(data)
      return result instanceof type.errors ? result : undefined
    },
  }

export function StepperSkippableStepsDemo(): ReactElement {
  const {clearErrors, control, getValues, setError} = useForm<FormData>({
    defaultValues: {address: "", card: "", promo: ""},
    mode: "onChange",
  })

  function validateStep(index: number): boolean {
    const validate = stepSchemas[index]
    if (!validate) {
      return true
    }
    return !validate(getValues())
  }

  function computeStepErrors(index: number) {
    clearErrors()

    const validate = stepSchemas[index]
    if (!validate) {
      return
    }

    const errors = validate(getValues())
    if (!errors) {
      return
    }

    for (const error of errors) {
      const field = error.path?.[0] as keyof FormData | undefined
      if (field) {
        setError(field, {message: error.message})
      }
    }
  }

  return (
    <Stepper.Root
      canGoToStep={({current, target}) => {
        if (target <= current) {
          // default to the built-in validation logic
          return undefined
        }
        return validateStep(current)
      }}
      count={steps.length}
      // this will bypass canGoToStep for PROMO_STEP only
      isStepSkippable={(index) => index === PROMO_STEP}
      onStepInvalid={({step: invalidStep}) => computeStepErrors(invalidStep)}
    >
      <Stepper.List>
        {steps.map((s, index) => (
          <Stepper.Item key={s.value} index={index}>
            <Stepper.Trigger>
              <Stepper.Indicator>{index + 1}</Stepper.Indicator>
              <Stepper.Label>
                {s.title}
                {index === PROMO_STEP && (
                  <Stepper.Hint>(optional)</Stepper.Hint>
                )}
              </Stepper.Label>
            </Stepper.Trigger>
            <Stepper.Separator />
          </Stepper.Item>
        ))}
      </Stepper.List>

      <Stepper.Content index={0}>
        <Controller
          control={control}
          name="address"
          render={({field: {onChange, ...fieldProps}, fieldState: {error}}) => (
            <TextInput
              className="max-w-56"
              errorText={error?.message}
              invalid={!!error}
              label="Shipping address"
              onValueChange={onChange}
              placeholder="123 Main St"
              required
              {...fieldProps}
            />
          )}
        />
      </Stepper.Content>

      <Stepper.Content index={1}>
        <Controller
          control={control}
          name="promo"
          render={({field: {onChange, ...fieldProps}}) => (
            <TextInput
              className="max-w-56"
              label="Promo code"
              onValueChange={onChange}
              placeholder="SAVE20"
              {...fieldProps}
            />
          )}
        />
      </Stepper.Content>

      <Stepper.Content index={2}>
        <Controller
          control={control}
          name="card"
          render={({field: {onChange, ...fieldProps}, fieldState: {error}}) => (
            <TextInput
              className="max-w-56"
              errorText={error?.message}
              invalid={!!error}
              label="Card number"
              onValueChange={onChange}
              placeholder="4242 4242 4242 4242"
              required
              {...fieldProps}
            />
          )}
        />
      </Stepper.Content>

      <Stepper.CompletedContent>
        Order confirmed. Thank you for your purchase!
      </Stepper.CompletedContent>

      <div className="mt-6 flex justify-between">
        <Stepper.PrevTrigger>
          <Button size="sm" startIcon={ChevronLeft} variant="outline">
            Back
          </Button>
        </Stepper.PrevTrigger>
        <Stepper.NextTrigger>
          <Button endIcon={ChevronRight} size="sm" variant="outline">
            Next
          </Button>
        </Stepper.NextTrigger>
      </div>
    </Stepper.Root>
  )
}

Explorer

Enter your contact information

API

<Stepper.Root>

Groups all parts of the stepper. Renders a <div> element by default.
PropTypeDefault
The total number of steps
number
Whether navigation to a step should be allowed. Receives the current step, target step, and whether the target has been previously visited.

Return
false to block navigation, true to allow it, or undefined to defer to the built-in navigation rules.
    (details: {
    current: number
    target: number
    visited: boolean
    }) => boolean
    A map of step indices to their completion status. In linear mode, steps before the current step are automatically completed. Use this to override or set completion status for individual steps. Note that this property is purely visual and does not impact navigation logic.
    Record<
    number,
    boolean
    >
    The initial value of the stepper when rendered. Use when you don't need to control the value of the stepper.
    number
    The document's text/writing direction.
    'ltr' | 'rtl'
    'ltr'
    
    A root node to correctly resolve document in custom environments. i.e., Iframes, Electron.
      () =>
      | Node
      | ShadowRoot
      | Document
      A map of step indices to their invalid status. Note that this property is purely visual and does not impact navigation logic.
      Record<
      number,
      boolean
      >
      Whether a step can be skipped during navigation in linear mode. Skippable steps are bypassed when using next/prev.
        (
        index: number,
        ) => boolean
        () => false
        
        If true, the stepper requires the user to complete the steps in order.
        boolean
        true
        
        Callback to be called when the value changes
          (
          step: number,
          ) => void
          Called when navigation is blocked due to an invalid step.
            (details: {
            action: 'set' | 'next'
            step: number
            targetStep?: number
            }) => void
            The orientation of the stepper
            | 'horizontal'
            | 'horizontal-inline'
            | 'horizontal-bottom-start'
            | 'vertical'
            | 'vertical-inline'
            "horizontal"
            
            A map of step indices to their pending status.
            Record<
            number,
            boolean
            >
            Allows you to replace the component's HTML element with a different tag or component. Learn more
            | ReactElement
            | ((
            props: object,
            ) => ReactElement)
            The size of the stepper and its elements.
            'sm' | 'lg'
            'lg'
            
            The controlled value of the stepper
            number
            Type
            number
            Description
            The total number of steps
            Type
            (details: {
            current: number
            target: number
            visited: boolean
            }) => boolean
            Description
            Whether navigation to a step should be allowed. Receives the current step, target step, and whether the target has been previously visited.

            Return
            false to block navigation, true to allow it, or undefined to defer to the built-in navigation rules.
              Type
              Record<
              number,
              boolean
              >
              Description
              A map of step indices to their completion status. In linear mode, steps before the current step are automatically completed. Use this to override or set completion status for individual steps. Note that this property is purely visual and does not impact navigation logic.
              Type
              number
              Description
              The initial value of the stepper when rendered. Use when you don't need to control the value of the stepper.
              Type
              'ltr' | 'rtl'
              Description
              The document's text/writing direction.
              Type
              () =>
              | Node
              | ShadowRoot
              | Document
              Description
              A root node to correctly resolve document in custom environments. i.e., Iframes, Electron.
                Type
                Record<
                number,
                boolean
                >
                Description
                A map of step indices to their invalid status. Note that this property is purely visual and does not impact navigation logic.
                Type
                (
                index: number,
                ) => boolean
                Description
                Whether a step can be skipped during navigation in linear mode. Skippable steps are bypassed when using next/prev.
                  Type
                  boolean
                  Description
                  If true, the stepper requires the user to complete the steps in order.
                  Type
                  (
                  step: number,
                  ) => void
                  Description
                  Callback to be called when the value changes
                    Type
                    (details: {
                    action: 'set' | 'next'
                    step: number
                    targetStep?: number
                    }) => void
                    Description
                    Called when navigation is blocked due to an invalid step.
                      Type
                      | 'horizontal'
                      | 'horizontal-inline'
                      | 'horizontal-bottom-start'
                      | 'vertical'
                      | 'vertical-inline'
                      Description
                      The orientation of the stepper
                      Type
                      Record<
                      number,
                      boolean
                      >
                      Description
                      A map of step indices to their pending status.
                      Type
                      | ReactElement
                      | ((
                      props: object,
                      ) => ReactElement)
                      Description
                      Allows you to replace the component's HTML element with a different tag or component. Learn more
                      Type
                      'sm' | 'lg'
                      Description
                      The size of the stepper and its elements.
                      Type
                      number
                      Description
                      The controlled value of the stepper

                      <Stepper.List>

                      Container for the step items. Renders a <div> element by default.
                      PropType
                      React children prop.
                      id attribute. If omitted, a unique identifier will be automatically generated for accessibility.
                      string
                      Allows you to replace the component's HTML element with a different tag or component. Learn more
                      | ReactElement
                      | ((
                      props: object,
                      ) => ReactElement)
                      Description
                      React children prop.
                      Type
                      string
                      Description
                      id attribute. If omitted, a unique identifier will be automatically generated for accessibility.
                      Type
                      | ReactElement
                      | ((
                      props: object,
                      ) => ReactElement)
                      Description
                      Allows you to replace the component's HTML element with a different tag or component. Learn more

                      <Stepper.Item>

                      Wrapper for a single step. Renders a <div> element by default.
                      PropType
                      The index of the step
                      number
                      Allows you to replace the component's HTML element with a different tag or component. Learn more
                      | ReactElement
                      | ((
                      props: object,
                      ) => ReactElement)
                      Type
                      number
                      Description
                      The index of the step
                      Type
                      | ReactElement
                      | ((
                      props: object,
                      ) => ReactElement)
                      Description
                      Allows you to replace the component's HTML element with a different tag or component. Learn more

                      <Stepper.Trigger>

                      Used to make each step item clickable. Renders a <button> element by default.
                      PropType
                      id attribute. If omitted, a unique identifier will be automatically generated for accessibility.
                      string
                      Allows you to replace the component's HTML element with a different tag or component. Learn more
                      | ReactElement
                      | ((
                      props: object,
                      ) => ReactElement)
                      Type
                      string
                      Description
                      id attribute. If omitted, a unique identifier will be automatically generated for accessibility.
                      Type
                      | ReactElement
                      | ((
                      props: object,
                      ) => ReactElement)
                      Description
                      Allows you to replace the component's HTML element with a different tag or component. Learn more

                      <Stepper.Indicator>

                      Visual indicator for the step state. Renders a <div> element by default.

                      By default, displays the step number or icon. When a step is completed, it shows a checkmark icon. When a step is invalid, it shows an error icon.

                      Rendering order:

                      - errorIcon (when invalid)
                      - children (current step)
                      - completedIcon (when complete)
                      - children (not current step)
                      PropTypeDefault
                      Icon to display when the step is completed.
                      | LucideIcon
                      | ReactNode
                      Check
                      
                      Icon to display when the step is in an error state.
                      | LucideIcon
                      | ReactNode
                      StepperIndicatorAlert
                      
                      Allows you to replace the component's HTML element with a different tag or component. Learn more
                      | ReactElement
                      | ((
                      props: object,
                      ) => ReactElement)
                      Type
                      | LucideIcon
                      | ReactNode
                      Description
                      Icon to display when the step is completed.
                      Type
                      | LucideIcon
                      | ReactNode
                      Description
                      Icon to display when the step is in an error state.
                      Type
                      | ReactElement
                      | ((
                      props: object,
                      ) => ReactElement)
                      Description
                      Allows you to replace the component's HTML element with a different tag or component. Learn more

                      <Stepper.Separator>

                      Visual connector between steps. Renders a <div> element by default.
                      PropType
                      Allows you to replace the component's HTML element with a different tag or component. Learn more
                      | ReactElement
                      | ((
                      props: object,
                      ) => ReactElement)
                      Type
                      | ReactElement
                      | ((
                      props: object,
                      ) => ReactElement)
                      Description
                      Allows you to replace the component's HTML element with a different tag or component. Learn more

                      <Stepper.CompletedContent>

                      Content displayed when all steps are completed.

                      Content area displayed when all steps are completed. Renders a <div> element by default.
                      PropType
                      Allows you to replace the component's HTML element with a different tag or component. Learn more
                      | ReactElement
                      | ((
                      props: object,
                      ) => ReactElement)
                      Type
                      | ReactElement
                      | ((
                      props: object,
                      ) => ReactElement)
                      Description
                      Allows you to replace the component's HTML element with a different tag or component. Learn more

                      <Stepper.Content>

                      Content area for a step. Renders a <div> element by default.
                      PropType
                      The index of the step
                      number
                      id attribute. If omitted, a unique identifier will be automatically generated for accessibility.
                      string
                      Allows you to replace the component's HTML element with a different tag or component. Learn more
                      | ReactElement
                      | ((
                      props: object,
                      ) => ReactElement)
                      Type
                      number
                      Description
                      The index of the step
                      Type
                      string
                      Description
                      id attribute. If omitted, a unique identifier will be automatically generated for accessibility.
                      Type
                      | ReactElement
                      | ((
                      props: object,
                      ) => ReactElement)
                      Description
                      Allows you to replace the component's HTML element with a different tag or component. Learn more

                      <Stepper.Label>

                      Displays the step title. Renders a <span> element by default.
                      PropType
                      Allows you to replace the component's HTML element with a different tag or component. Learn more
                      | ReactElement
                      | ((
                      props: object,
                      ) => ReactElement)
                      Type
                      | ReactElement
                      | ((
                      props: object,
                      ) => ReactElement)
                      Description
                      Allows you to replace the component's HTML element with a different tag or component. Learn more

                      <Stepper.Hint>

                      Displays a step subtitle or hint. Renders a <span> element by default.
                      PropType
                      Allows you to replace the component's HTML element with a different tag or component. Learn more
                      | ReactElement
                      | ((
                      props: object,
                      ) => ReactElement)
                      Type
                      | ReactElement
                      | ((
                      props: object,
                      ) => ReactElement)
                      Description
                      Allows you to replace the component's HTML element with a different tag or component. Learn more

                      <Stepper.NextTrigger>

                      Navigates to the next step. Renders a <button> element by default.
                      PropType
                      Element | ((props: {data-disabled: ''; data-part: 'next-trigger'; dir: 'ltr' | 'rtl'; disabled: boolean; onClick: MouseEventHandler; scope: 'stepper'; type: 'button';}) => Element)
                      Type
                      Element | ((props: {data-disabled: ''; data-part: 'next-trigger'; dir: 'ltr' | 'rtl'; disabled: boolean; onClick: MouseEventHandler; scope: 'stepper'; type: 'button';}) => Element)

                      <Stepper.PrevTrigger>

                      Navigates to the previous step. Renders a <button> element by default.
                      PropType
                      Element | ((props: {data-disabled: ''; data-part: 'prev-trigger'; dir: 'ltr' | 'rtl'; disabled: boolean; onClick: MouseEventHandler; scope: 'stepper'; type: 'button';}) => Element)
                      Type
                      Element | ((props: {data-disabled: ''; data-part: 'prev-trigger'; dir: 'ltr' | 'rtl'; disabled: boolean; onClick: MouseEventHandler; scope: 'stepper'; type: 'button';}) => Element)

                      <Stepper.Context>

                      Render prop that provides the current stepper API context.
                      PropType
                      Render Prop that provides the current StepperApi context.
                      Description
                      Render Prop that provides the current StepperApi context.

                      StepperApi

                      PropType
                      The total number of steps.
                      number
                      Returns the resolved state for a step at the given index.
                        (props: {
                        index: number
                        render?:
                        | Element
                        | ((
                        props: Props,
                        ) => Element)
                        }) => {
                        completed: boolean
                        contentId: string
                        current: boolean
                        first: boolean
                        incomplete: boolean
                        index: number
                        invalid: boolean
                        last: boolean
                        pending?: boolean
                        previous?: boolean
                        skippable: boolean
                        triggerId: string
                        visited: boolean
                        }
                        Function to go to the next step.
                          () => void
                          Function to go to the previous step.
                            () => void
                            Whether the stepper has a next step.
                            boolean
                            Whether the stepper has a previous step.
                            boolean
                            Check if a specific step can be skipped
                              (
                              index: number,
                              ) => boolean
                              Function to go to reset the stepper.
                                () => void
                                Function to set the value of the stepper.
                                  (
                                  step: number,
                                  ) => void
                                  The value of the stepper.
                                  number
                                  Type
                                  number
                                  Description
                                  The total number of steps.
                                  Type
                                  (props: {
                                  index: number
                                  render?:
                                  | Element
                                  | ((
                                  props: Props,
                                  ) => Element)
                                  }) => {
                                  completed: boolean
                                  contentId: string
                                  current: boolean
                                  first: boolean
                                  incomplete: boolean
                                  index: number
                                  invalid: boolean
                                  last: boolean
                                  pending?: boolean
                                  previous?: boolean
                                  skippable: boolean
                                  triggerId: string
                                  visited: boolean
                                  }
                                  Description
                                  Returns the resolved state for a step at the given index.
                                    Type
                                    () => void
                                    Description
                                    Function to go to the next step.
                                      Type
                                      () => void
                                      Description
                                      Function to go to the previous step.
                                        Type
                                        boolean
                                        Description
                                        Whether the stepper has a next step.
                                        Type
                                        boolean
                                        Description
                                        Whether the stepper has a previous step.
                                        Type
                                        (
                                        index: number,
                                        ) => boolean
                                        Description
                                        Check if a specific step can be skipped
                                          Type
                                          () => void
                                          Description
                                          Function to go to reset the stepper.
                                            Type
                                            (
                                            step: number,
                                            ) => void
                                            Description
                                            Function to set the value of the stepper.
                                              Type
                                              number
                                              Description
                                              The value of the stepper.
                                              Last updated on by Ryan Bower