Horizontal Layout
The default layout. Steps run start to end with the connecting line between circles. Best for short wizards on wide screens, where every step label fits on one row.
// Inject a stepper dynamically and initialize it
const el = document.getElementById('demo-stepper-horizontal');
const stepper = NDS.Stepper.create(el);
// Navigate programmatically
NDS.Stepper.next('demo-stepper-horizontal');
NDS.Stepper.previous('demo-stepper-horizontal');
NDS.Stepper.goTo('demo-stepper-horizontal', 3);
Vertical Layout
Steps run top to bottom. Reach for it when a step carries detailed content or action buttons, or when the column is narrow, such as a sidebar or a drawer.
Radial Layout
One step at a time inside a progress ring, with the next step named underneath. Use it where vertical space is tight. This layout needs the progress ring markup, which the other two hide.
Timeline with Divider Labels
A past-to-present record: each step opens with a divider carrying its date, and the rule doubles as the separator between entries. Add nds-reverse so the newest entry sits on top, and write the steps oldest first.
- Led the 2024 program accreditation review.
- Opened two research labs with industry funding.
Responsive Stepper
A single stepper that morphs between variants as the viewport crosses breakpoints. Suited to flows that span mobile and desktop, where a compact radial on small screens should give way to a full vertical panel on desktop. Resize the window to see the transition
// Inject a stepper dynamically and initialize it
const el = document.getElementById('demo-stepper-responsive');
const stepper = NDS.Stepper.create(el);
// Read or change the fallback layout variant at runtime
const fallback = NDS.Stepper.getFallback('demo-stepper-responsive'); // 'horizontal'
NDS.Stepper.setFallback('demo-stepper-responsive', 'vertical');
// Navigate programmatically
NDS.Stepper.next('demo-stepper-responsive');
NDS.Stepper.previous('demo-stepper-responsive');
NDS.Stepper.goTo('demo-stepper-responsive', 3);
Built-in Features
Activates when .nds-stepper is on the page. Step states, progress display, and control button handlers attach automatically.
Horizontal, vertical, and radial layouts cover wide forms, narrow sidebars, and compact dashboard widgets respectively.
Set data-current and all steps update their completed, current, or upcoming states automatically.
Add data-stepper-control to any button to navigate steps without writing JavaScript.
The nds:stepper:change event fires on every navigation with current step, total, and percentage in the detail.
A single stepper adapts across breakpoints via modifier classes like nds-radial-sm and nds-vertical-lg (same pattern as nds-tableView-sm). One DOM tree renders as horizontal, vertical, or radial depending on viewport.
Add .nds-stepper-next inside any step to show the upcoming step name. Automatically hidden on horizontal and vertical layouts; surfaces only in radial.
Navigate with NDS.Stepper.next(id), NDS.Stepper.previous(id), and NDS.Stepper.goTo(id, step).
Usage Guidelines
Best Practices
- Use steppers for multi-step forms like registration, applications, and onboarding flows where the user completes discrete stages in order
- Use the horizontal layout when you have enough width and want all steps visible at once. This is the default and works best with 3 to 5 steps
- Use the vertical layout when steps need detailed content, action buttons, or the interface is narrow (sidebars, mobile drawers). Add
nds-reverseto flip progress direction from bottom-to-top, useful for timelines or chat-like flows - Use the radial layout for compact spaces like dashboard cards or mobile headers where only the current step needs to be visible
- Use the responsive modifier classes (
nds-{horizontal|vertical|radial}-{sm|md|lg}) when one flow spans multiple breakpoints. Same convention asnds-tableView-sm. The single DOM tree carrying the combined radial + linear markup morphs via JS; no parallel steppers or manual visibility switching needed - Do not use a stepper for indeterminate or percentage-based progress. Use the Progress component instead
- Do not use a stepper for navigation menus or tab-like interfaces. Use Tabs for switching between independent content panels
- Use the dot variant when step labels are not needed and visual progress alone is sufficient, such as onboarding slides or image carousels
- Connecting lines between steps are shown by default on all steps except the last
- Keep step titles short (2 to 4 words). Use the description for additional context
- In radial steppers, add
.nds-stepper-nextinside the step text to preview the upcoming step name. Omit it on the final step - Radial steppers work best with 3 to 6 steps. Fewer than 3 makes the circle progress hard to read; more than 6 makes step titles too compressed
- A horizontal stepper runs out of room on small screens. Pair it with the radial variant there:
nds-radial nds-horizontal-lgis radial by default and turns horizontal from thelgbreakpoint up. The same pairing works for vertical - Use
NDS.Stepper.next()for form steps, notdata-stepper-control. A form step is gated by validation or by a request, and the attribute moves the stepper on every click. See Advancing the Stepper above - Always provide a unique
idon the stepper container so control buttons and the JS API can target it
Advancing the Stepper
There are three ways to move a stepper. Pick by one question: can anything refuse the move?
- Nothing can refuse it — use
data-stepper-controlon a button. The click moves the stepper, always. This fits a Back button, a demo, a walkthrough, and agotothat starts the flow over. It needs no JavaScript. - Something can refuse it — call
NDS.Stepper.next(id)from the code that knows the answer. Validation, a request, a server check: the thing that decides is the thing that moves the stepper. Every form step is this case. - The step number lives in your own state — write
data-currenton the.nds-stepper. The component watches the attribute and re-renders. This fits a server-rendered page or a framework view that already holds the step number.
The stepper is a progress display. It never validates, never blocks, and never sends a request. A submit-typed button inside a form is handed to that form untouched: the stepper does not cancel the submit and does not move. Move it yourself once the form reports success.
<!-- Back: nothing can refuse it -->
<button type="button" class="nds-btn nds-secondary-outline"
data-stepper-control="previous" data-stepper-target="myStepper">
<span class="nds-label">Back</span>
</button>
<!-- Continue: validation can refuse it -->
<button type="button" class="nds-btn nds-primary" id="myContinue">
<span class="nds-label">Continue</span>
</button>
// Continue: move only if the visible step validates. validateForm() skips
// fields inside a hidden panel, so it checks the step on screen.
document.getElementById('myContinue').addEventListener('click', (e) => {
if (NDS.Forms.validateForm(e.currentTarget).valid) NDS.Stepper.next('myStepper');
});
// Final step: move only once the request succeeds. The form carries data-ajax,
// so Forms validates, stops the POST and fires nds:formValid.
document.getElementById('myForm').addEventListener('nds:formValid', () => {
sendApplication().then(() => NDS.Stepper.next('myStepper'));
});
Modifier Classes
| Class | Applies to | Description |
|---|---|---|
nds-vertical | Container | Switches to top-to-bottom layout with vertical connecting lines |
nds-radial | Container | Circular progress indicator showing one step at a time |
nds-dot | Container | Replaces numbered circles with 16px dots (horizontal and vertical) |
nds-xs | Container | 40px radial circle, 12px title, 10px description (radial only) |
nds-sm | Container | 48px radial circle, 14px title, 12px description (radial only) |
nds-md | Container | 64px radial circle, 16px title, 14px description. This is the radial default, so the class is only needed to override a size set higher up (radial only) |
nds-lg | Container | 40px linear circle. Radial: 80px circle, 16px title, 14px description |
nds-xl | Container | 48px linear circle. Radial: 120px circle, 20px title, 14px description |
nds-center | Container | Centers step content beneath each circle (horizontal only) |
nds-oncolor | Container | Adapts colors for dark or branded backgrounds |
nds-reverse | Container | Reverses vertical stepper direction so progress flows bottom-to-top (vertical only) |
nds-cardView | Container | Gives the step a card surface: padding, border, rounded corners, and card background. Vertical cards each step's content and lifts the box so its first row sits level with the circle. Radial cards the whole stepper, because it shows one step at a time beside the progress ring. Has no effect in the horizontal layout. Combine with nds-oncolor and the surface switches to the same translucent white a card uses on a colored background. Use it for timelines and history lists. Do not nest a card inside the step as well |
nds-neutral | Container | Neutral gray progress circle color (radial only) |
nds-horizontal-sm / -md / -lg | Container | Forces horizontal layout on mobile / tablet / desktop respectively. Combine with other breakpoint-scoped variants to compose a responsive layout |
nds-vertical-sm / -md / -lg | Container | Forces vertical layout on mobile / tablet / desktop respectively |
nds-radial-sm / -md / -lg | Container | Forces radial layout on mobile / tablet / desktop respectively. Example: nds-radial-sm nds-vertical-lg = radial on mobile, horizontal on tablet, vertical on desktop |
nds-loading | Container | Renders all steps as animated skeleton placeholders. Use while step data is being loaded. Equivalent to setting data-state="loading" on the root |
nds-stepper-action | .nds-stepper-content | Flex row container for action buttons (.nds-btn children) placed inside a step's content area. Each button stretches to fill equal width |
Data Attributes
| Attribute | Description |
|---|---|
data-current | Set on the .nds-stepper container. The active step number (starting from 1). Updating this attribute triggers an automatic UI refresh. |
data-total | Set on the .nds-stepper container. Total number of steps. Updated automatically on init but can be set manually. |
data-step-text | Set on .nds-stepper-circle. Overrides the auto-generated step number with custom text. |
data-stepper-control | Set on any button. Values: next, previous, or goto. Buttons inside a stepper target their parent automatically. The move is unconditional: the click always moves the stepper. A submit-typed button inside a form is handed to that form instead, so the stepper neither cancels the submit nor moves — see Advancing the Stepper above. |
data-stepper-target | Set on control buttons outside a stepper. The ID of the stepper to control. |
data-stepper-value | Set on goto control buttons. The step number to navigate to. |
data-state="completed" | Set on .nds-stepper-step. Marks the step as completed with a checkmark icon. Managed automatically by JS. |
data-state="current" | Set on .nds-stepper-step. Marks the step as the active step. Managed automatically by JS. |
data-state="upcoming" | Set on .nds-stepper-step. Marks the step as a future step with muted styling. Managed automatically by JS. |
data-state="loading" | Set on the .nds-stepper container. Renders all steps as animated skeleton placeholders. Equivalent to adding nds-loading. |
CSS Custom Properties
| Property | Default | Description |
|---|---|---|
--stepper-size | 32px | Circle diameter for linear steppers. Overridden by size classes |
--stepper-gap | Derived from the layout and size | Spacing between steps. Left unset it follows the layout: calc(var(--stepper-size) * 1.5) vertical, calc(var(--stepper-size) / 2) for a vertical nds-cardView without nds-dot, and the indicator gap scaled by the size classes when horizontal. Set it to override any of them |
--stepper-indicator-gap | var(--spacing-md) | Base spacing between steps that --stepper-gap derives from; the size classes scale it by 1.2 and 1.5 |
--divider-lift | calc(var(--stepper-size) / 4) | Drops a label divider inside a step so its rule meets the centre of the step circle. Scales with the circle, so the size classes carry it. Set 0 to leave the divider where it falls |
--stepper-content-width | var(--paragraph-max-width) | Maximum width of a step's content, so a step stays readable in a wide column and its text and any label divider end on the same edge. Set none on a step that holds something wide, such as a table or a full-bleed image |
--stepper-text-padding | var(--spacing-xl) | Gap between the step indicator and its text block |
--stepper-card-lift | calc(var(--stepper-size) / 4), or var(--stepper-size) with nds-dot | How far nds-cardView raises each card so its first row meets the circle (vertical only). Scales with the circle, so the size classes carry it. Set it when your content starts with something taller or shorter than a title |
--gap | var(--spacing-xl) | Vertical gap between the step's text block and any .nds-stepper-action buttons. Set on the .nds-stepper container |
--stepper-title-FS | Set by the size class | Font size of the step title in a radial stepper. Radial text does not scale with the ring, so set this for a large title beside a small circle |
--stepper-title-LH | Set by the size class | Line height of the step title in a radial stepper. Set it with --stepper-title-FS, so the pair stays matched |
--stepper-description-FS | Set by the size class | Font size of the step description and the next-step line in a radial stepper. Works the same way as --stepper-title-FS |
--stepper-description-LH | Set by the size class | Line height of the step description and the next-step line in a radial stepper |
--progress-size | var(--stepper-size) | Circle diameter for radial steppers. Inherits from --stepper-size, which radial size classes override |
JavaScript API
Steppers auto-initialize on page load. Access instances via NDS.Stepper.get(id) or call convenience methods directly. The nds:stepper:change event fires on every step change.