EmStep
The progress header for a multi-step flow — numbered markers, one active step, and navigation methods that walk the sequence and skip disabled steps.
Usage
EmStep is the container and EmStepItem the steps. Each item carries a value; v-model on the root holds the active one, and clicking a step selects it. The numbers in the markers come from document order — there is no index prop to manage, so inserting a step renumbers the rest automatically.
By default mandatory is 'force': the first enabled step selects itself when nothing is chosen, and the active step cannot be clicked off. A stepper always points somewhere.
Anatomy
<script setup lang="ts">
import { EmStep, EmStepItem } from '@paper/emerald'
</script>
<template>
<EmStep>
<EmStepItem />
</EmStep>
</template>Composed on v0
EmStep wraps v0’s Step compound — Step.Root and Step.Item — which is the renderless single-selection provider with sequential navigation, built on createStep.
Both v0 parts render no element of their own, so Emerald owns every element you see. The root is a wrapping flex div (.emerald-step) that hosts Step.Root inside it; each item is a native <button> that takes Step.Item’s slot attrs — the click and Enter/Space handlers, aria-selected, aria-disabled and the data-selected / data-disabled attributes every rule in the stylesheet hangs off. v0 owns the state: registration, which step is active, the mandatory rule, and the first / last / next / prev / step navigation methods.
The numbered marker is the one part neither layer computes. It is a CSS counter — the root counter-resets it and each item counter-increments — so the number reflects pure document order and never consults the selection state. That is also why there is no step index prop to keep in sync.
EmStep’s default slot passes Step.Root’s slot props straight through, so the navigation methods are one v-slot away — that is what the wizard example below builds on. Navigation is bounded, not circular: next on the last step and prev on the first do nothing.
Examples
Props
EmStep
| Prop | Type | Default | Description |
|---|---|---|---|
v-model | T | T[] | — | Value of the active step. With the default mandatory: 'force', an empty model self-corrects to the first enabled step |
disabled | boolean | false | Freezes the whole group — the root gets data-disabled and dims, and every step reports disabled |
enroll | boolean | false | Select steps as they register. Single-selection, so the last enabled step to register ends up active |
mandatory | boolean | 'force' | 'force' | true prevents deselecting the active step; 'force' additionally auto-selects the first enabled step on registration. false allows an empty selection — clicking the active step toggles it off |
namespace | string | — | Which v0 Step instance the items bind to; falls through to v0’s default when unset. Only needed when nesting |
The default slot receives Step.Root’s slot props: isDisabled, the navigation methods first(), last(), next(), prev() and step(count), select(id), unselect(id) and toggle(id), plus v0’s root attrs (aria-multiselectable="false"), which Emerald’s own wrapper does not consume.
select, unselect and toggle take registration ids, not step values — and ids are auto-generated unless you pass id to each EmStepItem. To activate a step by value, write the value to v-model; the id-based methods are for the rare case where two steps share a value.
EmStepItem
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | auto-generated | Registration id, only needed for the id-based slot methods |
label | string | — | Fallback label, rendered when the default slot is empty |
value | unknown | — | Written to the root’s v-model when this step is active |
disabled | boolean | false | Step cannot be activated, leaves the tab order, and is skipped by the navigation methods |
namespace | string | — | Must match the root’s namespace when one is set |
The default slot is the step’s label and falls back to the label prop. The numbered marker is not a slot — it is a CSS counter and always renders.
Accessibility
Each step is a native <button type="button">, so focusability and Enter / Space activation rest on the platform. v0’s Step.Item binds role="tab", aria-selected and aria-disabled onto it, plus a keydown handler that keeps keyboard activation in sync with the click guard.
Naming
The accessible name is the label text — the default slot or the label prop. The numbered marker is aria-hidden, so a step announces as “Payment”, not “2 Payment”; the number is a visual affordance only. A step with neither slot content nor label has no accessible name — always provide one.
Focus and keyboard
Every enabled step has tabindex="0" — each is its own tab stop, in document order. There is no roving focus and no arrow-key navigation: the navigation methods exist on the slot for you to wire to your own controls, but the steps themselves respond only to Tab, Enter, Space and click. Disabled steps carry tabindex="-1" and are never encountered by a keyboard user.
The focus ring draws on the marker via :focus-visible, so it appears for keyboard focus and not after a mouse click.
States
| State | Attributes on the step | Focusable | Activation |
|---|---|---|---|
| Active | aria-selected="true", data-selected | Yes | With default mandatory, clicking again is a no-op; with :mandatory="false" it deselects |
| Disabled (item or group) | aria-disabled="true", data-disabled, tabindex="-1" | No | Blocked |
What it is not
The wrapper div carries no role, so the role="tab" steps are not inside a role="tablist", and EmStep renders no role="tabpanel" — it is a navigation header over state, not a complete tabs widget. If the surface you are building is tabs — panels swapped in place, arrow-key traversal — reach for EmTabs instead; EmStep is for flows where the steps mark progress and the content below is yours.