GrSteps

  • core
  • navigation
  • API · 9 props
  • ⌨ keys

Shows where the user is in a multi-step process and can hold them back until the step checks out.

import { GrSteps } from '@feugene/granularity/components/GrSteps'

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 GrFormSection no longer saves it;
  • a process with a check at every step — beforeLeave will not let you go further until the current step adds up.

When to take something else

NeedTake
Switch the panels of one screen, the order does not matterGrTabs
Show a history of events rather than go through a processGrTimeline
Show where the user is in a hierarchy of pagesGrBreadcrumbs
The share of what is done as a number, without stagesGrProgressBar / GrProgressCircle
Group the fields of one form with headingsGrFormSection

Variants every tone and view, rendered by the real component

variant

  • steps
  • compact

Sizes

  • xs
  • sm
  • md
  • lg

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

Stage

Loading…

0 changed· state is in the URL

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 — linear keeps the way forward closed. The wizard knows nothing about forms: the page decides in before-leave whether the delivery step may be left, and going back is always allowed. The buttons live in the page’s footer and drive it through next(), back(), isFirst and isLast.

  • 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 keeps linear from going past it. The nightly schedule is not on this plan and is disabled, and goTo() jumps straight back to the columns. The label slot 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

PropTypedefaultDescription
variantGrStepsVariant | undefinedundefinedA compact look for a narrow column: the label of the current step and a progress bar.
size"xs" | "sm" | "md" | "lg" | undefinedundefinedThe size of the markers and the type size of the labels. `md` by default; taken from `GrConfigProvider`.
ariaLabelstring | undefinedundefinedThe name of the `navigation` landmark. Not set — taken from the locale.
orientationGrStepsOrientation | undefinedundefinedThe direction of the ribbon. `horizontal` by default; taken from `GrConfigProvider`.
clickableboolean | undefinedundefinedA move by a click on a step. Off and the ribbon becomes an indicator only.
linearboolean | undefinedundefinedForward — 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>) | undefinedundefinedThe 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-modelstring—The `value` of the current step (`v-model`).
stepsrequiredGrStep[]—The steps of the wizard in order: `value`, a label, a description, an icon, a status, `disabled`.

Slots

SlotTypeDescription
step
{ step: GrStep; index: number; status: GrStepStatus; enterab{ step: GrStep; index: number; status: GrStepStatus; enterable: boolean; markerClass: string; stateText: string; }
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

EventSignatureDescription
update:modelValue(event: "update:modelValue", value: string): void—
change(event: "change", value: string): void—

Methods

MethodsSignatureDescription
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.
isFirstbooleanThere are no available steps before the current one — a "Back" button is not needed.
isLastbooleanThere 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
import type { GrStepsConfigurableProps, GrStepsSize, GrStepsOrientation } from '@feugene/granularity/components/GrSteps'

Filedist/types/components/GrSteps/config.d.ts

grStepsConfigconst
const grStepsConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrSteps">;

Filedist/types/components/GrSteps/defaults.d.ts

GrStepsConfigurablePropsinterface
interface GrStepsConfigurableProps {
    size: GrStepsSize;
    orientation: GrStepsOrientation;
    variant: GrStepsVariant;
    linear: boolean;
    clickable: boolean;
}

Filedist/types/components/GrSteps/grStepsStyles.d.ts

GrStepsSizetype
type GrStepsSize = GrComponentSize;
GrStepsOrientationtype
type GrStepsOrientation = 'horizontal' | 'vertical';
GrStepsVarianttype
type GrStepsVariant = 'steps' | 'compact';

Filedist/types/components/GrSteps/safelist.d.ts

grStepsSafelistconst
const grStepsSafelist: string[];

Filedist/types/components/GrSteps/stepsModel.d.ts

GrStepStatustype
type GrStepStatus = 'complete' | 'current' | 'upcoming' | 'error';
GrSteptype
type GrStep = {
    value: string;
    label: string;
    description?: string;
    icon?: string | Component;
    status?: GrStepStatus;
    disabled?: boolean;
};

Filedist/types/components/GrSteps/GrSteps.vue.d.ts

GrStepsPropsinterface
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>;
GrStepsEmitsinterface
interface GrStepsEmits {
    (e: 'update:modelValue', value: string): void;
    (e: 'change', value: string): void;
}

Filedist/types/components/GrSteps/index.d.ts

GrStepsInstancetype
type GrStepsInstance = ComponentExposed<typeof GrStepsComponent>;

Accessibility

APG pattern

—

Usage from the library prose

This is navigation, not tabs

The root is a <nav> with an <ol> list, and there are no roles on the items at all. role="tab" is not applicable here: a tablist without a tabpanel is a broken pattern, and the panel of a step carries no role — it is ordinary markup of the application.

Hence the keyboard as well: every available step is a Tab stop of its own, and the arrows are not involved. The “a composite widget is one stop” rule concerns widgets that select a value (GrTabs, GrSegmented); steps are closer to GrBreadcrumbs and GrBottomNav, where every item is tabbed separately. The practical consideration is the same: the future steps are unavailable, and a ring of arrows would walk mostly over what is switched off.

Three states give three different pieces of markup:

StepThe tagThe mark
passed, available<button>—
current<span>aria-current="step"
future or switched off<span> outside the tab orderaria-disabled on a switched-off one
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.

vue
<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:

ts
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.

vue
<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 GrButton and call next()/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.