Skip to content
IzzyBlazor 0.1.0-alpha.17

Enter all results ↑ ↓ move Esc close

Stepper

Where someone is in a process.

JavaScript off: what the HTML and CSS do on their own.

Usage

razor
<Stepper Current="2" Label="Sign-up progress">
    <Step Label="Account" Href="/signup/account" />
    <Step Label="Profile" Description="Name and photo" />
    <Step Label="Done" />
</Stepper>

A stepper shows where someone is in a process, such as a checkout or a setup wizard. It's an ordered list with no script, so it looks the same on every page; the page decides what Current is.

Steps of a checkout

Use Current for the step someone is on, counting from 1: the steps before it show as done. A Step's Href makes its label a link, such as back to a finished step.

Preview width

Preview

Code

Checkout.razor
@using System.ComponentModel.DataAnnotations
@using System.Globalization
@inject NavigationManager Navigation

<Grid Template='new() { Base = "1fr", Lg = "1fr 18rem" }' Gap="8" Align="Align.Start" Grow>
    <Stack Gap="6">
        @* Finished steps link back; the step itself is ?step= in the address. *@
        <Stepper Current="Current" Label="Checkout progress">
            <Step Label="Cart" Href="@BackTo(1)" />
            <Step Label="Shipping" Href="@BackTo(2)" />
            <Step Label="Payment" Href="@BackTo(3)" />
            <Step Label="Review" />
        </Stepper>

        @switch (Current)
        {
            case 1:
                <Stack Gap="4">
                    <Heading Level="2" Size="TextSize.Xl">Your cart</Heading>
                    <TextBlock Color="Color.Muted">Both items are in stock and ready to ship.</TextBlock>
                    <Row><Button Href="@StepUrl(2)" EndIcon="Lucide.ArrowRight">Continue to shipping</Button></Row>
                </Stack>
                break;

            case 2:
                <Form Model="Shipping" FormName="checkout-shipping" OnValidSubmit="ToPayment" Columns="2">
                    <TextField @bind-Value="Shipping!.FullName" AutoComplete="name" ColSpan="GridSpan.Full" />
                    <TextField @bind-Value="Shipping!.Street" AutoComplete="street-address" ColSpan="GridSpan.Full" />
                    <TextField @bind-Value="Shipping!.City" AutoComplete="address-level2" />
                    <TextField @bind-Value="Shipping!.PostalCode" AutoComplete="postal-code" />
                    <RadioGroup @bind-Value="Shipping!.Delivery" ColSpan="GridSpan.Full" />
                    <FormActions Justify="Justify.Between">
                        <Button Href="@StepUrl(1)" Variant="ButtonVariant.Outline" Accent="Accent.Neutral" StartIcon="Lucide.ArrowLeft">Back</Button>
                        <Button Submit EndIcon="Lucide.ArrowRight">Continue to payment</Button>
                    </FormActions>
                </Form>
                break;

            case 3:
                <Form Model="Payment" FormName="checkout-payment" OnValidSubmit="ToReview" Columns="2">
                    <TextField @bind-Value="Payment!.NameOnCard" AutoComplete="cc-name" ColSpan="GridSpan.Full" />
                    <TextField @bind-Value="Payment!.CardNumber" AutoComplete="cc-number" Placeholder="4242 4242 4242 4242" ColSpan="GridSpan.Full" />
                    <TextField @bind-Value="Payment!.Expiry" AutoComplete="cc-exp" Placeholder="MM/YY" />
                    <TextField @bind-Value="Payment!.SecurityCode" AutoComplete="cc-csc" />
                    <FormActions Justify="Justify.Between">
                        <Button Href="@StepUrl(2)" Variant="ButtonVariant.Outline" Accent="Accent.Neutral" StartIcon="Lucide.ArrowLeft">Back</Button>
                        <Button Submit EndIcon="Lucide.ArrowRight">Review order</Button>
                    </FormActions>
                </Form>
                break;

            case 4:
                <Stack Gap="4">
                    <Heading Level="2" Size="TextSize.Xl">Review your order</Heading>
                    <DescriptionList Divided>
                        <DescriptionItem Term="Delivery">@(Express ? "Express, next day" : "Standard, 3 to 5 days")</DescriptionItem>
                        <DescriptionItem Term="Payment">Card, charged when the order ships</DescriptionItem>
                    </DescriptionList>
                    @* Placing the order is a post, not a link. *@
                    <form method="post" @formname="checkout-place" @onsubmit="Place">
                        <AntiforgeryToken />
                        <Row Gap="2" Justify="Justify.Between">
                            <Button Href="@StepUrl(3)" Variant="ButtonVariant.Outline" Accent="Accent.Neutral" StartIcon="Lucide.ArrowLeft">Back</Button>
                            <Button Submit StartIcon="Lucide.Lock">Place order</Button>
                        </Row>
                    </form>
                </Stack>
                break;

            default:
                <Alert Status="Status.Success" Title="Order placed">
                    Order #10482 is confirmed. We've emailed your receipt.
                </Alert>
                <Row><Button Href="@NewOrderUrl" Variant="ButtonVariant.Outline" Accent="Accent.Neutral">Start a new order</Button></Row>
                break;
        }
    </Stack>

    <Card Title="Order summary" TitleLevel="2">
        <Stack Gap="3">
            @foreach (var line in Lines)
            {
                <Row Gap="3" Justify="Justify.Between">
                    <TextBlock As="span">@line.Name <TextBlock As="span" Color="Color.Muted">× @line.Quantity</TextBlock></TextBlock>
                    <TextBlock As="span" TabularNums>@Money(line.Price * line.Quantity)</TextBlock>
                </Row>
            }
            <Divider />
            <Row Gap="3" Justify="Justify.Between">
                <TextBlock As="span" Color="Color.Muted">Delivery</TextBlock>
                <TextBlock As="span" TabularNums>@(Express ? Money(ExpressCost) : "Free")</TextBlock>
            </Row>
            <Row Gap="3" Justify="Justify.Between">
                <TextBlock As="span" Weight="FontWeight.Semibold">Total</TextBlock>
                <TextBlock As="span" Weight="FontWeight.Semibold" TabularNums>@Money(Total)</TextBlock>
            </Row>
        </Stack>
    </Card>
</Grid>

@code {
    private const int Steps = 4;

    [SupplyParameterFromQuery(Name = "step")]
    private int? StepQuery { get; set; }

    [SupplyParameterFromQuery(Name = "delivery")]
    private string? DeliveryQuery { get; set; }

    [SupplyParameterFromForm(FormName = "checkout-shipping")]
    private ShippingDetails? Shipping { get; set; }

    [SupplyParameterFromForm(FormName = "checkout-payment")]
    private PaymentDetails? Payment { get; set; }

    // Past the last step (5), every step shows as done.
    private int Current => Math.Clamp(StepQuery ?? 1, 1, Steps + 1);

    private bool Express => DeliveryQuery == "express";

    // A real checkout keeps the order on the server. This one keeps only the delivery choice, in
    // the address, so going back to Shipping shows it.
    // On an interactive page the component stays between steps, so the cart (step 1) starts a
    // new order.
    protected override void OnParametersSet()
    {
        if (Current == 1)
        {
            Shipping = null;
            Payment = null;
        }

        Shipping ??= new() { Delivery = Express ? DeliveryOption.Express : DeliveryOption.Standard };
        Payment ??= new();
    }

    private string StepUrl(int step) => Navigation.GetUriWithQueryParameter("step", step);

    private string? BackTo(int step) => step < Current && Current <= Steps ? StepUrl(step) : null;

    // On a static page this redirects after the post; on an interactive one it navigates.
    private void ToPayment() => Navigation.NavigateTo(Navigation.GetUriWithQueryParameters(new Dictionary<string, object?>
    {
        ["step"] = 3,
        ["delivery"] = Shipping!.Delivery == DeliveryOption.Express ? "express" : "standard",
    }));

    private void ToReview() => Navigation.NavigateTo(StepUrl(4));

    private void Place() => Navigation.NavigateTo(StepUrl(Steps + 1));

    private string NewOrderUrl => Navigation.GetUriWithQueryParameters(new Dictionary<string, object?> { ["step"] = 1, ["delivery"] = null });

}

Here the step is ?step= in the address, and each step's form moves on to the next step once it's valid, posting on a static page and running C# on an interactive one, so it works with JavaScript off too. Below 640px the steps stay a row of markers, and only the current step's label shows; a finished step's link is still there, on its marker.

Vertical, with descriptions

Use Orientation="Orientation.Vertical" to keep the steps in a column on every screen. A Step's Description adds a line under its label, and its Icon shows in the marker until the step is done.

Preview

  1. Create the project (completed)Name, region and plan
  2. Connect a repositoryGitHub, GitLab or Bitbucket
  3. DeployBuilds on every push to main

Code

Vertical.razor
<Stepper Current="2" Orientation="Orientation.Vertical">
    <Step Label="Create the project" Description="Name, region and plan" />
    <Step Label="Connect a repository" Description="GitHub, GitLab or Bitbucket" Icon="Lucide.GitBranch" />
    <Step Label="Deploy" Description="Builds on every push to main" Icon="Lucide.Rocket" />
</Stepper>

A step that only sometimes applies

Use Hidden to leave out a step that doesn't apply this time, such as a gift message: the steps after it are numbered to match.

Preview

  1. Cart (completed)
  2. Gift message (completed)
  3. Shipping
  4. Payment

Code

Optional.razor
@inject NavigationManager Navigation

@* A step that only sometimes applies keeps its place with Hidden: here the gift message step
   shows only for a gift (?gift=true in this example's address), and the steps after it are
   numbered to match. *@
<Stack Gap="5" Grow>
    <Stepper Current="@(Gift ? 3 : 2)" Label="Order progress">
        <Step Label="Cart" />
        <Step Label="Gift message" Hidden="!Gift" />
        <Step Label="Shipping" />
        <Step Label="Payment" />
    </Stepper>
    <Row>
        <Button Href="@Toggle" Variant="ButtonVariant.Outline" Accent="Accent.Neutral" StartIcon="Lucide.Gift">
            @(Gift ? "Not a gift" : "Send as a gift")
        </Button>
    </Row>
</Stack>

@code {
    [SupplyParameterFromQuery(Name = "gift")]
    private bool? GiftQuery { get; set; }

    private bool Gift => GiftQuery == true;

    private string Toggle => Navigation.GetUriWithQueryParameter("gift", Gift ? null : "true");
}

Use Hidden rather than @if: on an interactive page, Blazor renders a step that appears later after the others, so it would be numbered last.

Sizes

Use Size to scale the markers and the text with the buttons and fields around them.

Preview

  1. Account (completed)
  2. Profile
  3. Review
  1. Account (completed)
  2. Profile
  3. Review
  1. Account (completed)
  2. Profile
  3. Review

Code

Sizes.razor
<Stack Gap="6" Grow>
    <Stepper Current="2" Size="ControlSize.Sm" Label="Sign-up progress (small)">
        <Step Label="Account" />
        <Step Label="Profile" />
        <Step Label="Review" />
    </Stepper>
    <Stepper Current="2" Label="Sign-up progress">
        <Step Label="Account" />
        <Step Label="Profile" />
        <Step Label="Review" />
    </Stepper>
    <Stepper Current="2" Size="ControlSize.Lg" Label="Sign-up progress (large)">
        <Step Label="Account" />
        <Step Label="Profile" />
        <Step Label="Review" />
    </Stepper>
</Stack>

Before the first step and after the last

Use Current="0" when no step has started, and a number past the last step to show every step done, such as on an order's confirmation.

Its own words

Use Label for the list's name ("Progress"), and CompletedLabel for what screen readers hear after a finished step's label in place of its check mark ("(completed)").

Parts

Written inside Stepper. The API page lists their parameters.

  • Step: one step in a Stepper: a numbered marker (a check once done), its label and an optional description.

Mode support

What works in each mode, Stepper
FeatureOffStaticLive
EverythingYesYesYes

Off: JavaScript disabled. Static: a static SSR page with IzzyBlazor's script. Live: an interactive page (Server, WebAssembly or Auto).

Accessibility

  • It renders an ordered list named "Progress" (Label); the current step has aria-current="step".
  • The markers are hidden from screen readers; a finished step's check mark is heard as "(completed)" after its label (CompletedLabel).
  • A step with Href is a plain link; the others are text and don't take focus.
  • It doesn't move focus or announce anything when Current changes: a wizard moves focus itself, such as to the new step's heading.

Parameters

Stepper parameters
PropTypeDefault
ChildContentThe Steps, in order.RenderFragment?
CurrentThe current step, counting from 1.int1
OrientationA row (the default) or a column.OrientationOrientation.Horizontal
LabelThe list's accessible name.string?"Progress"
CompletedLabelRead after a finished step's label, in place of its check mark.string?"(completed)"
SizeThe marker and text size.ControlSizeControlSize.Md

See also

  • Progress: a progress bar, determinate or not.
  • Tabs: panels that switch with no script, or links to pages.
  • Timeline: events on a rail, such as an order's history.
  • Demos: Checkout.