---
title: Stepper
description: Guide users through a sequence of panels and completion states.
package: moraine
version: 0.6.0
repository: https://github.com/subframe7536/moraine
---

# Stepper

> Guide users through a sequence of panels and completion states.

Use Stepper to show progress through a fixed sequence of steps. Decide whether users may jump ahead before enabling clickable navigation.

## Basic usage

```tsx
import { Stepper } from 'moraine'

export function Example() {
  return (
    <Stepper
      items={[
        { value: 'details', title: 'Details' },
        { value: 'review', title: 'Review' },
      ]}
    />
  )
}
```

## Anatomy

```text
Stepper [component; slot=root]
├── header [slot]
│   └── item [slot]
│       ├── trigger [slot]
│       │   ├── indicator [slot]
│       │   │   └── icon [slot]
│       │   └── wrapper [slot]
│       │       ├── title [slot]
│       │       └── description [slot]
│       └── separator [slot]
└── content [slot]
```

The indicator, title, and description share one trigger; separators appear only between items.

## Usage

### Linear and clickable steps

The component's linear and clickable behavior determines which steps can be activated. Use controlled state with `value` and `onChange` when completion or navigation decisions come from application state.

```tsx
import { Button, Stepper } from 'moraine'
import { createSignal } from 'solid-js'

const STEPS = [
  { value: 'account', title: 'Account', description: 'User credentials' },
  { value: 'profile', title: 'Profile', description: 'Personal details' },
  { value: 'review', title: 'Review', description: 'Confirmation' },
]

export function LinearUsage() {
  const [step, setStep] = createSignal('account')
  const stepKeys = ['account', 'profile', 'review']

  const currentIndex = () => stepKeys.indexOf(step())

  const goPrev = () => {
    const prevKey = stepKeys[Math.max(0, currentIndex() - 1)]
    if (prevKey) {
      setStep(prevKey)
    }
  }

  const goNext = () => {
    const nextKey = stepKeys[Math.min(stepKeys.length - 1, currentIndex() + 1)]
    if (nextKey) {
      setStep(nextKey)
    }
  }

  return (
    <div class="max-w-xl w-full space-y-4">
      <Stepper items={STEPS} value={step()} onChange={setStep} linear />
      <div class="flex gap-2">
        <Button size="xs" variant="outline" disabled={currentIndex() === 0} onClick={goPrev}>
          Previous
        </Button>
        <Button size="xs" disabled={currentIndex() === stepKeys.length - 1} onClick={goNext}>
          Next step
        </Button>
      </div>
    </div>
  )
}
```

### Panels and orientation

Step labels and panels use tab-like relationships. Vertical orientation changes the layout and directional keyboard navigation; completed state and step icons remain data you supply.

```tsx
import { Stepper } from 'moraine'

export function PanelsUsage() {
  return (
    <div class="max-w-md w-full">
      <Stepper
        defaultValue="details"
        items={[
          {
            value: 'details',
            title: 'Project details',
            content: (
              <div class="text-xs text-muted-foreground p-4">
                Configure project name and workspace root.
              </div>
            ),
          },
          {
            value: 'target',
            title: 'Deploy target',
            content: (
              <div class="text-xs text-muted-foreground p-4">
                Select cloud provider and cluster region.
              </div>
            ),
          },
        ]}
      />
    </div>
  )
}
```

### Keyboard interaction

`activationMode` controls whether keyboard focus selects a step immediately (`automatic`, the default navigation mode) or selection waits for activation (`manual`). Disabled or linear-locked steps are skipped.

| Key                                          | Description                                              |
| -------------------------------------------- | -------------------------------------------------------- |
| <kbd>ArrowRight</kbd> / <kbd>ArrowDown</kbd> | Moves toward the next enabled step where applicable.     |
| <kbd>ArrowLeft</kbd> / <kbd>ArrowUp</kbd>    | Moves toward the previous enabled step where applicable. |
| <kbd>Enter</kbd> / <kbd>Space</kbd>          | Activates the focused step in manual activation mode.    |

## Examples

### Controlled step

```tsx
import { Button, Stepper } from 'moraine'
import { createSignal } from 'solid-js'

export function ControlledNonLinear() {
  const RELEASE_STEPS = () => [
    {
      title: 'Draft',
      value: 'draft',
      content: <p class="text-sm text-foreground">Prepare release notes.</p>,
    },
    {
      title: 'Review',
      value: 'review',
      content: <p class="text-sm text-foreground">Collect team approvals.</p>,
    },
    {
      title: 'Ship',
      value: 'ship',
      content: <p class="text-sm text-foreground">Deploy to production.</p>,
    },
  ]

  const [releaseStep, setReleaseStep] = createSignal('review')

  return (
    <div class="space-y-4">
      <Stepper
        items={RELEASE_STEPS()}
        value={releaseStep()}
        onChange={setReleaseStep}
        linear={false}
      />
      <div class="flex flex-wrap gap-2 items-center">
        <Button size="sm" variant="outline" onClick={() => setReleaseStep('draft')}>
          Go to draft
        </Button>
        <Button size="sm" variant="outline" onClick={() => setReleaseStep('review')}>
          Go to review
        </Button>
        <Button size="sm" variant="outline" onClick={() => setReleaseStep('ship')}>
          Go to ship
        </Button>
        <p class="text-xs text-muted-foreground">Current step: {releaseStep()}</p>
      </div>
    </div>
  )
}
```

### Clickable and linear steps

```tsx
import { Stepper, Switch } from 'moraine'
import { createSignal } from 'solid-js'

export function ClickableVsReadOnly() {
  const [clickable, setClickable] = createSignal(false)
  const [linear, setLinear] = createSignal(true)
  const checkoutSteps = [
    {
      title: 'Address',
      description: 'Where should we send the order?',
      icon: 'i-lucide:map-pinned',
      value: 'address',
      content: <p class="text-sm text-foreground">Collect shipping address details.</p>,
    },
    {
      title: 'Shipping',
      description: 'Choose a delivery method.',
      icon: 'i-lucide:truck',
      value: 'shipping',
      content: <p class="text-sm text-foreground">Pick standard, express, or local pickup.</p>,
    },
    {
      title: 'Payment',
      description: 'Confirm billing and payment.',
      icon: 'i-lucide:credit-card',
      value: 'payment',
      content: <p class="text-sm text-foreground">Review billing details and submit payment.</p>,
    },
  ]

  return (
    <div class="space-y-4">
      <div class="flex flex-wrap gap-4">
        <Switch checked={clickable()} label="Clickable" onCheckedChange={setClickable} />
        <Switch checked={linear()} label="Linear" onCheckedChange={setLinear} />
      </div>

      <Stepper
        items={checkoutSteps}
        defaultValue="address"
        clickable={clickable()}
        linear={linear()}
      />
    </div>
  )
}
```

## Attributes

| Attributes | Slot | Description |
| --- | --- | --- |
| `data-disabled` | `stepper-item`, `stepper-separator` | Present when the component, slot, or item is disabled. |
| `data-state` | `stepper-item`, `stepper-trigger`, `stepper-indicator`, `stepper-separator` | Stores the component state used by styling hooks (e.g. open, closed, active). |
| `data-clickable` | `stepper-trigger` | Present when the step or item accepts direct activation. |
| `data-selected` | `stepper-trigger`, `stepper-content` | Present when the item or tab is selected. |

## Props

Props for the Stepper component.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| activationMode | 'automatic' \| 'manual' \| undefined | 'automatic' | Whether keyboard activation happens immediately or only after confirmation. |
| clickable | boolean \| undefined | false | Whether steps are clickable for navigation. |
| defaultValue | Value \| undefined | — | Default active step value for uncontrolled usage. |
| disabled | boolean \| undefined | false | Whether the entire stepper is disabled. |
| id | string \| undefined | — | Unique identifier for the stepper root element. |
| items | ({<br>  /** Unique value for the step. */<br>  value?: Value;<br>  /** Title of the step. */<br>  title?: JSX.Element;<br>  /** Secondary description of the step. */<br>  description?: JSX.Element;<br>  /** Icon to display in the step indicator. */<br>  icon?: IconT.Name;<br>  /** Content to display when the step is active. */<br>  content?: JSX.Element;<br>  /** Whether the step is disabled. */<br>  disabled?: boolean;<br>  /** Additional class name for the step item. */<br>  class?: string;<br>})[] \| undefined | — | Array of steps to display. |
| linear | boolean \| undefined | true | Whether to enforce linear navigation (must complete steps in order). |
| loop | boolean \| undefined | false | Whether keyboard navigation loops around when reaching the ends. |
| onChange | ((value: Value) => void) \| undefined | — | Callback when the active step changes. |
| orientation | 'horizontal' \| 'vertical' \| undefined | 'horizontal' | The orientation of the stepper. |
| size | 'sm' \| 'md' \| 'lg' \| undefined | 'md' | Visual size of the component. |
| value | Value \| undefined | — | Controlled active step value. |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| classes | Classes \| undefined | — | Family slot class defaults for this instance. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |
| styles | Styles \| undefined | — | Family slot style defaults for this instance. |
