GrSteps
Shows where the user is in a multi-step process and can hold them back until the step checks out.
Overview
When to take it
- a checkout or a registration — several screens with a form, between which one has to move while seeing how much is left;
- an import of data — the upload, the mapping of columns, the preview, the run: every stage depends on the previous one;
- a long form broken into stages — when a single page of forty fields frightens and
GrFormSectionno longer saves it; - a process with a check at every step —
beforeLeavewill not let you go further until the current step adds up.
When to take something else
| Need | Take |
|---|---|
| Switch the panels of one screen, the order does not matter | GrTabs |
| Show a history of events rather than go through a process | GrTimeline |
| Show where the user is in a hierarchy of pages | GrBreadcrumbs |
| The share of what is done as a number, without stages | GrProgressBar / GrProgressCircle |
| Group the fields of one form with headings | GrFormSection |
Variants every tone and view, rendered by the real component
variant
stepscompact
Sizes
xssmmdlg
The grid is live: hover and press it. Hover, focus and active states are not drawn here — they belong to the browser, not to a prop.
Playground controls come from the API, not from hand
Loading…
0 changed· state is in the URL
<GrSteps :steps="steps" v-model="value" />Examples 3 each with a live demo and its code
A checkout that checks the address
Steps are navigation: passed steps are buttons, the current one is
aria-current="step", the future ones wait —linearkeeps the way forward closed. The wizard knows nothing about forms: the page decides inbefore-leavewhether the delivery step may be left, and going back is always allowed. The buttons live in the page’s footer and drive it throughnext(),back(),isFirstandisLast.An import with a step that did not add up
A column has room for a line under each step.
status: 'error'comes from the page — the error lives in the column mapping, not in the order of steps — and an unfinished step keepslinearfrom going past it. The nightly schedule is not on this plan and isdisabled, andgoTo()jumps straight back to the columns. Thelabelslot puts a time estimate beside the label and leaves the marker and the hidden state to the component.Check a previewTwo columns are still not matched, so the import cannot run yet.Seven steps on a phone
Seven steps do not fit a row on a phone, and a scroller would hide half of them.
variant="compact"shows the label of the current step, a counter and a bar; the bar is decorative, because the text and a hidden live region already say the same.Where customers pay; IBAN and the account holder.
API generated from the package · cannot drift
Props
| Prop | Type | default | Description |
|---|---|---|---|
variant | GrStepsVariant | undefined | undefined | A compact look for a narrow column: the label of the current step and a progress bar. |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | The size of the markers and the type size of the labels. `md` by default; taken from `GrConfigProvider`. |
ariaLabel | string | undefined | undefined | The name of the `navigation` landmark. Not set — taken from the locale. |
orientation | GrStepsOrientation | undefined | undefined | The direction of the ribbon. `horizontal` by default; taken from `GrConfigProvider`. |
clickable | boolean | undefined | undefined | A move by a click on a step. Off and the ribbon becomes an indicator only. |
linear | boolean | undefined | undefined | Forward — no further than the first one not passed. Backwards is always free: going back and correcting what has been filled in is an ordinary scenario rather than a way round the rule. |
beforeLeave | ((from: string, to: string) => boolean | Promise<boolean>) | undefined | undefined | The gate of the move. Returned `false` — there is no move. This is where the consumer puts the validation of the step: `GrSteps` knows nothing about `GrForm` and should not, and the form already offers `validate()`/`validateField()`. |
modelValuerequiredv-model | string | — | The `value` of the current step (`v-model`). |
stepsrequired | GrStep[] | — | The steps of the wizard in order: `value`, a label, a description, an icon, a status, `disabled`. |
Slots
| Slot | Type | Description |
|---|---|---|
step |
| Markup of your own for the **whole** item: the marker, the label and the state in words. The root tag, `aria-current` and the click stay with the component. The slot must render `stateText` itself (visually hidden): otherwise a completed step and a step with an error sound the same — the colour of the marker is not available to a screen reader. The class of the marker by status is `markerClass`. If you only need to add to the label, take `#label`. |
label | { step: GrStep; index: number; status: GrStepStatus; } | Only the text block of a step: the label, the description and your own content next to them — a time estimate, a badge. The marker and the visually hidden state stay with the component. |
Events
| Event | Signature | Description |
|---|---|---|
update:modelValue | (event: "update:modelValue", value: string): void | — |
change | (event: "change", value: string): void | — |
Methods
| Methods | Signature | Description |
|---|---|---|
next | () => Promise<boolean> | Go to the next step, skipping disabled ones; asks `beforeLeave`. `true` — the move took place. |
back | () => Promise<boolean> | Go back to the previous step, skipping disabled ones; asks `beforeLeave`. `true` — the move took place. |
goTo | (value: string) => Promise<boolean> | Go to a step by `value`. The `linear` restriction does not apply, `beforeLeave` does; `true` — the move took place. |
isFirst | boolean | There are no available steps before the current one — a "Back" button is not needed. |
isLast | boolean | There are no available steps after the current one — time to show "Done". |
Types
Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.
import type { GrStepsConfigurableProps, GrStepsSize, GrStepsOrientation } from '@feugene/granularity/components/GrSteps'Filedist/types/components/GrSteps/config.d.ts
const grStepsConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrSteps">;Filedist/types/components/GrSteps/defaults.d.ts
interface GrStepsConfigurableProps {
size: GrStepsSize;
orientation: GrStepsOrientation;
variant: GrStepsVariant;
linear: boolean;
clickable: boolean;
}Filedist/types/components/GrSteps/grStepsStyles.d.ts
type GrStepsSize = GrComponentSize;type GrStepsOrientation = 'horizontal' | 'vertical';type GrStepsVariant = 'steps' | 'compact';Filedist/types/components/GrSteps/safelist.d.ts
const grStepsSafelist: string[];Filedist/types/components/GrSteps/stepsModel.d.ts
type GrStepStatus = 'complete' | 'current' | 'upcoming' | 'error';type GrStep = {
value: string;
label: string;
description?: string;
icon?: string | Component;
status?: GrStepStatus;
disabled?: boolean;
};Filedist/types/components/GrSteps/GrSteps.vue.d.ts
interface GrStepsProps {
modelValue: string;
steps: GrStep[];
orientation?: GrStepsOrientation;
size?: GrStepsSize;
variant?: GrStepsVariant;
linear?: boolean;
clickable?: boolean;
ariaLabel?: string;
beforeLeave?: (from: string, to: string) => boolean | Promise<boolean>;interface GrStepsEmits {
(e: 'update:modelValue', value: string): void;
(e: 'change', value: string): void;
}Filedist/types/components/GrSteps/index.d.ts
type GrStepsInstance = ComponentExposed<typeof GrStepsComponent>;Accessibility
APG pattern
—
Full contract of the package: docs/keyboard.md ↗
Usage from the library prose
The component does not draw the content of a step
The wizard has no panel and will not have one. In tabs it is justified by role="tabpanel" and the
aria-labelledby pairing, while here the content of a step is an ordinary v-if in the consumer’s
markup, and a separate component would add a second entity with a synchronised idBase for the
sake of zero semantics.
<GrSteps v-model="step" :steps="steps" />
<GrForm v-if="step === 'delivery'" :model="form" :rules="rules">
…
</GrForm>
<GrForm v-else-if="step === 'payment'" :model="form" :rules="rules">
…
</GrForm>The validation of a step goes through beforeLeave rather than through knowledge of the form
GrSteps knows nothing about GrForm: it does not read its context and does not
climb into its fields. The check is set by the application — into the gate that blocks the
transition:
async function validateStep(from: string, to: string): Promise<boolean> {
// Going back is always allowed: editing what is filled in must not run into validation.
if (stepIndex(to) < stepIndex(from))
return true
return formRef.value!.validate()
}A click on a step, next(), back() and goTo() go through the gate — everything the component
initiates itself. A direct change of v-model from the outside does not go through it: that is
already a decision of the application, and intercepting it would be a lie.
The error status is set from the outside
Three statuses are inferred from the position: before the current one it is complete, the current
one is current, after it upcoming. The fourth, error, has nowhere to be inferred from: an
error lives in the form rather than in the order of the steps. Set it yourself when a step has been
passed but does not add up — otherwise the wizard cannot say “there are still errors on the second
step”.
A step with an error does not count as the edge of what has been passed: with linear it will not
let you go past it.
linear limits movement forward only
Going back is always allowed — returning to fix what was filled in is an edit rather than a
circumvention of the rule. Forward — no further than the first unpassed one, but steps already
passed that lie ahead remain reachable: mark them status: 'complete', and the user will be able
to jump back to the step they came back from without going through everything again.
A compact variant for a narrow column
Seven steps do not fit into a side panel and do not fit onto a phone.
variant="compact" shows the label of the current step, a counter and a bar instead of the strip.
The bar is decorative (aria-hidden): the same thing has already been said by the text beside it
and by a hidden live region, and a second progressbar in the tree would make a screen reader read
the progress twice.
Your own text beside the label — #label
#label fills only the text block of a step: the label, the description and your own content
beside them — a time estimate, a badge. The marker (the number, the tick, the icon) and the visually
hidden state in words (”— has errors”, ”— complete”) stay with the component.
<template>
<GrSteps v-model="step" :steps="steps" orientation="vertical">
<template #label="{ step: item }">
<span>{{ item.label }}</span>
<small class="text-[var(--gr-muted-fg)]">{{ item.estimate }}</small>
</template>
</GrSteps>
</template>#step replaces the item entirely — both the marker and the state. Then the slot renders them:
the marker class for the status arrives as the markerClass prop, the state in words as
stateText, and it must be printed visually hidden. Without it a passed step and a step with an
error sound the same: the colour of the marker is not available to a screen reader.
Limits
- the component has no “Back” and “Next” buttons of its own. They are placed by the application
from
GrButtonand callnext()/back()through a ref: in a wizard they live in the footer, together with “Cancel” and “Save draft”, and drawing them inside the indicator would mean dictating the layout of the page; - the component does not store the data of the steps — the model of the form is entirely yours;
- the horizontal strip does not scroll. Many steps in a narrow place is
variant="compact"rather than a scroller half of whose stages are invisible.
Props in this example