Skip to main content

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#

import { Stepper } from 'moraine'

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

Playground#

Step 1: Set up your profile and login credentials.

Props
Slots

Anatomy#

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.

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.

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
→ / ↓ Moves toward the next enabled step where applicable.
← / ↑ Moves toward the previous enabled step where applicable.
↵ / Space Activates the focused step in manual activation mode.

Examples#

Controlled step#

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#

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
data-disabledSlot: stepper-item, stepper-separatorDescription: Present when the component, slot, or item is disabled.
data-stateSlot: stepper-item, stepper-trigger, stepper-indicator, stepper-separatorDescription: Stores the component state used by styling hooks (e.g. open, closed, active).
data-clickableSlot: stepper-triggerDescription: Present when the step or item accepts direct activation.
data-selectedSlot: stepper-trigger, stepper-contentDescription: Present when the item or tab is selected.

Props#

Props for the Stepper component. Renders a <div> element by default.

Prop