icon

Morainev0.5.0

Stepper
navigationstepper

Stepper

Tab-structured step navigation with configurable orientation and separator layout.

Import#

import { Stepper } from 'moraine'

Slot Structure#

Header with step items and optional per-step content panels.

root
├── header
│ └── item (×n)
│ ├── container
│ │ ├── trigger
│ │ └── separator (optional, between items)
│ └── wrapper (optional)
│ ├── title (optional)
│ └── description (optional)
└── content (×n, optional)

Examples#

Sizes#

Preview the Stepper across all supported sizes using the default linear, non-clickable tab navigation.

function Sizes() {
const createCheckoutSteps = () => [
{
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>,
},
]
const STEPPER_SIZES = ['xs', 'sm', 'md', 'lg', 'xl'] as const
return (
<div class="space-y-6">
<For each={STEPPER_SIZES}>
{(size) => (
<div class="space-y-2">
<p class="text-xs text-muted-foreground tracking-wide font-medium uppercase">{size}</p>
<Stepper items={createCheckoutSteps()} defaultValue="shipping" size={size} />
</div>
)}
</For>
</div>
)
}

Controlled + Non-linear#

Manage the active step externally and allow jumping to any step.

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 vs Non-Clickable#

Compare the default non-clickable mode with explicit click-enabled navigation.

function ClickableVsReadOnly() {
const createCheckoutSteps = () => [
{
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-6">
<div class="space-y-2">
<p class="text-xs text-muted-foreground tracking-wide font-medium uppercase">
Default (`linear=true`, `clickable=false`)
</p>
<Stepper items={createCheckoutSteps()} defaultValue="address" />
</div>
<div class="space-y-2">
<p class="text-xs text-muted-foreground tracking-wide font-medium uppercase">
Click enabled (`linear=true`, `clickable=true`)
</p>
<Stepper items={createCheckoutSteps()} defaultValue="address" clickable />
</div>
<div class="space-y-2">
<p class="text-xs text-muted-foreground tracking-wide font-medium uppercase">
Non-linear (`linear=false`, `clickable=true`)
</p>
<Stepper items={createCheckoutSteps()} defaultValue="address" linear={false} clickable />
</div>
</div>
)
}

Linear Checkout#

Enable clicking while still only allowing the next available step to be selected.

function LinearCheckout() {
const createCheckoutSteps = () => [
{
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 <Stepper items={createCheckoutSteps()} defaultValue="address" clickable />
}

Vertical#

Render the tab-structured step navigation vertically.

function Vertical() {
const PIPELINE_STEPS = () => [
{
title: 'Queued',
description: 'Waiting for worker capacity.',
value: 'queued',
content: <p class="text-sm text-foreground">This job is waiting in the queue.</p>,
},
{
title: 'Building',
description: 'Compiling and bundling assets.',
value: 'building',
content: <p class="text-sm text-foreground">The current build is running.</p>,
},
{
title: 'Ready',
description: 'Artifacts are available.',
value: 'ready',
content: <p class="text-sm text-foreground">The deployment artifact is ready to use.</p>,
},
]
return (
<div class="max-w-3xl">
<Stepper items={PIPELINE_STEPS()} orientation="vertical" defaultValue="building" />
</div>
)
}

API Reference#

Attributes#

Slotroot0 attributes
Stepper container that owns orientation, step state, and panel rendering.
No attribute metadata for this slot.

Props#

PropTypeDefaultDescription
activationMode"manual" | "automatic" | undefinedautomatic
Whether keyboard activation happens immediately or only after confirmation.
classClassValue
Class applied to the component root or trigger element.
classesStepperT.Classes | undefined
clickableboolean | undefinedfalse
Whether steps are clickable for navigation.
defaultValueStepperT.Value | undefined
Default active step value for uncontrolled usage.
disabledboolean | undefinedfalse
Whether the entire stepper is disabled.
idstring | undefined
Unique identifier for the stepper root element.
itemsStepperT.Item[] | undefined
Array of steps to display.
linearboolean | undefinedtrue
Whether to enforce linear navigation (must complete steps in order).
onChange((value: StepperT.Value) => void) | undefined
Callback when the active step changes.
orientation"horizontal" | "vertical" | undefinedhorizontal
The orientation of the stepper.
refJSX.HTMLElementTags["div"] extends { ref?: infer Ref; } ? Ref : never | undefined
size"xs" | "sm" | "md" | "lg" | "xl" | undefined
styleJSX.CSSProperties | undefined
stylesStepperT.Styles | undefined
valueStepperT.Value | undefined
Controlled active step value.

Items#

An individual step in the stepper.
PropTypeDefaultDescription
classstring | undefined
Additional class name for the step item.
contentJSX.Element
Content to display when the step is active.
descriptionJSX.Element
Secondary description of the step.
disabledboolean | undefinedfalse
Whether the step is disabled.
iconIconT.Nameindex + 1
Icon to display in the step indicator.
titleJSX.Element
Title of the step.
valueStepperT.Value | undefinedindex of the item
Unique value for the step.