Command Palette

Search for a command...

Stepper

Accessible and typesafe Stepper component to create step-by-step workflows.

Content for step-1

"use client";

import * as React from "react";
import { styled } from "styled-system/jsx";
import { Button } from "@/components/ui/button";
import { defineStepper } from "@/components/ui/stepper";

const {
  StepperProvider,
  StepperControls,
  StepperNavigation,
  StepperPanel,
  StepperStep,
  StepperTitle,
} = defineStepper(
  {
    id: "step-1",
    title: "Step 1",
  },
  {
    id: "step-2",
    title: "Step 2",
  },
  {
    id: "step-3",
    title: "Step 3",
  }
);

export default function StepperDemo() {
  return (
    <StepperProvider variant="horizontal" css={{ spaceY: "4" }}>
      {({ methods }) => (
        <React.Fragment>
          <StepperNavigation>
            {methods.all.map((step) => (
              <StepperStep key={step.id} of={step.id} onClick={() => methods.goTo(step.id)}>
                <StepperTitle>{step.title}</StepperTitle>
              </StepperStep>
            ))}
          </StepperNavigation>
          {methods.switch({
            "step-1": (step) => <Content id={step.id} />,
            "step-2": (step) => <Content id={step.id} />,
            "step-3": (step) => <Content id={step.id} />,
          })}
          <StepperControls>
            {!methods.isLast && (
              <Button variant="secondary" onClick={methods.prev} disabled={methods.isFirst}>
                Previous
              </Button>
            )}
            <Button onClick={methods.isLast ? methods.reset : methods.next}>
              {methods.isLast ? "Reset" : "Next"}
            </Button>
          </StepperControls>
        </React.Fragment>
      )}
    </StepperProvider>
  );
}

const Content = ({ id }: { id: string }) => {
  return (
    <StepperPanel
      css={{
        h: "200px",
        alignContent: "center",
        rounded: "md",
        borderWidth: "1px",
        bg: "slate.50",
        p: "8",
        _dark: { bg: "slate.950" },
      }}
    >
      <styled.p css={{ textStyle: "xl", fontWeight: "normal" }}>Content for {id}</styled.p>
    </StepperPanel>
  );
};

About

Stepperize is built and maintained by damianricobelli.

Installation

npx nore-ui-cli@latest add stepper

Structure

A Stepper component is composed of the following parts:

  • StepperProvider - Handles the stepper logic.
  • StepperNavigation - Contains the buttons and labels to navigate through the steps.
  • StepperStep - Step component.
  • StepperTitle - Step title.
  • StepperDescription - Step description.
  • StepperPanel - Section to render the step content based on the current step.
  • StepperControls - Section to render the buttons to navigate through the steps.

Usage

import { defineStepper } from "@/components/ui/stepper";

const {
  StepperProvider,
  StepperControls,
  StepperDescription,
  StepperNavigation,
  StepperPanel,
  StepperStep,
  StepperTitle,
} = defineStepper(
  { id: "step-1", title: "Step 1" },
  { id: "step-2", title: "Step 2" },
  { id: "step-3", title: "Step 3" }
);

export function Component() {
  return (
    <StepperProvider>
      <StepperNavigation>
        <StepperStep>
          <StepperTitle />
          <StepperDescription />
        </StepperStep>
        ...
      </StepperNavigation>
      <StepperPanel />
      <StepperControls>...</StepperControls>
    </StepperProvider>
  );
}

Your first Stepper

Let's start with the most basic stepper. A stepper with a horizontal navigation.

Create a stepper instance with the defineStepper function.

const { StepperProvider, ... } = defineStepper(
  { id: "step-1", title: "Step 1" },
  { id: "step-2", title: "Step 2" },
  { id: "step-3", title: "Step 3" }
)

Wrap your application in a StepperProvider component.

export function MyFirstStepper() {
  return <StepperProvider>...</StepperProvider>;
}

Add a StepperNavigation component to render the navigation buttons and labels.

const { StepperProvider, StepperNavigation, StepperStep, StepperTitle } = defineStepper(
  { id: "step-1", title: "Step 1" },
  { id: "step-2", title: "Step 2" },
  { id: "step-3", title: "Step 3" }
);
export function MyFirstStepper() {
  return (
    <StepperProvider>
      {({ methods }) => (
        <StepperNavigation>
          {methods.all.map((step) => (
            <StepperStep of={step.id} onClick={() => methods.goTo(step.id)}>
              <StepperTitle>{step.title}</StepperTitle>
            </StepperStep>
          ))}
        </StepperNavigation>
      )}
    </StepperProvider>
  );
}

Add a StepperPanel component to render the content of the step.

const { StepperProvider, StepperPanel, ... } = defineStepper(
  { id: "step-1", title: "Step 1" },
  { id: "step-2", title: "Step 2" },
  { id: "step-3", title: "Step 3" }
)

export function MyFirstStepper() {
  return (
    <StepperProvider>
      {({ methods }) => (
        <>
          {/* StepperNavigation code */}
          {methods.switch({
            "step-1": (step) => <StepperPanel />,
            "step-2": (step) => <StepperPanel />,
            "step-3": (step) => <StepperPanel />,
          })}
        </>
      )}
    </StepperProvider>
  )
}

Add a StepperControls component to render the buttons to navigate through the steps.

const { StepperProvider, StepperControls, ... } = defineStepper(
  { id: "step-1", title: "Step 1" },
  { id: "step-2", title: "Step 2" },
  { id: "step-3", title: "Step 3" }
)

export function MyFirstStepper() {
  return (
    <StepperProvider>
      {({ methods }) => (
        <>
          {/* StepperNavigation code */}
          {/* StepperPanel code */}
          <StepperControls>
            {!methods.isLast && (
              <Button
                variant="secondary"
                onClick={methods.prev}
                disabled={methods.isFirst}
              >
                Previous
              </Button>
            )}
            <Button onClick={methods.isLast ? methods.reset : methods.next}>
              {methods.isLast ? "Reset" : "Next"}
            </Button>
          </StepperControls>
        </>
      )}
    </StepperProvider>
  )
}

Add some styles to make it look nice.

const {
  StepperProvider,
  StepperNavigation,
  StepperStep,
  StepperTitle,
  StepperControls,
  StepperPanel,
} = defineStepper(
  { id: "step-1", title: "Step 1" },
  { id: "step-2", title: "Step 2" },
  { id: "step-3", title: "Step 3" }
);

export function MyFirstStepper() {
  return (
    <StepperProvider className="space-y-4">
      {({ methods }) => (
        <>
          <StepperNavigation>
            {methods.all.map((step) => (
              <StepperStep of={step} onClick={() => methods.goTo(step.id)}>
                <StepperTitle>{step.title}</StepperTitle>
              </StepperStep>
            ))}
          </StepperNavigation>
          {methods.switch({
            "step-1": (step) => <Content id={step.id} />,
            "step-2": (step) => <Content id={step.id} />,
            "step-3": (step) => <Content id={step.id} />,
          })}
          <StepperControls>
            {!methods.isLast && (
              <Button variant="secondary" onClick={methods.prev} disabled={methods.isFirst}>
                Previous
              </Button>
            )}
            <Button onClick={methods.isLast ? methods.reset : methods.next}>
              {methods.isLast ? "Reset" : "Next"}
            </Button>
          </StepperControls>
        </>
      )}
    </StepperProvider>
  );
}

const Content = ({ id }: { id: string }) => {
  return (
    <StepperPanel className="h-[200px] content-center rounded border bg-slate-50 p-8">
      <p className="text-xl font-normal">Content for {id}</p>
    </StepperPanel>
  );
};

Components

The components in stepper.tsx are built to be composable i.e you build your stepper by putting the provided components together. They also compose well with other shadcn/ui components such as DropdownMenu, Collapsible or Dialog etc.

If you need to change the code in stepper.tsx, you are encouraged to do so. The code is yours. Use stepper.tsx as a starting point and build your own.

In the next sections, we'll go over each component and how to use them.

If you want to use @stepperize/react API directly, like when, switch, match, etc. you can use the useStepper hook from your stepper instance and build your own components.

defineStepper

The defineStepper function is used to define the steps. It returns a Stepper instance with a hook and utils to interact with the stepper.

Unlike @stepperize/react, defineStepper also offers all the components for rendering the stepper.

For example, you can define the steps like this:

const stepperInstance = defineStepper(
  { id: "step-1", title: "Step 1", description: "Step 1 description" },
  { id: "step-2", title: "Step 2", description: "Step 2 description" },
  { id: "step-3", title: "Step 3", description: "Step 3 description" }
);

Each instance will return:

  • steps - Array of steps.
  • useStepper - Hook to interact with the stepper component.
  • utils - Provides a set of pure functions for working with steps.

And the components:

  • StepperProvider
  • StepperNavigation
  • StepperStep
  • StepperTitle
  • StepperDescription
  • StepperPanel
  • StepperControls

Each step in the defineStepper needs only an id to work and they are not limited to any type. You can define anything within each step, even components!

useStepper

The useStepper hook is used to interact with the stepper. It provides methods to interact with and render your stepper.

StepperProvider

The StepperProvider component is used to provide the stepper instance from defineStepper to the other components. You should always wrap your application in a StepperProvider component.

Allow us to work with the useStepper hook in components that are within the provider.

For example:

const { StepperProvider, useStepper } = defineStepper(
  { id: "step-1", title: "Step 1" },
  { id: "step-2", title: "Step 2" },
  { id: "step-3", title: "Step 3" }
);

export function MyStepper() {
  const methods = useStepper(); // ❌ This won't work if the component is not within the provider
  return (
    <StepperProvider>
      <MyCustomComponent />
    </StepperProvider>
  );
}

function MyCustomComponent() {
  const methods = useStepper(); // ✅ This will work
  return <div>{methods.currentStep.title}</div>;
}

You also get access to the methods in the children's component

export function MyStepper() {
  return (
    <StepperProvider>
      {({ methods }) => (
        ...
      )}
    </StepperProvider>
  )
}

You can set the initial step and metadata for the stepper passing these props:

  • initialStep - The ID of the initial step to display
  • initialMetadata - The initial metadata to set for the steps. See Metadata for more information.

If you don't need the methods prop, you can just pass the children directly and get the methods from the useStepper hook from your stepper instance.

Props

NameTypeDescription
varianthorizontal, vertical or circleStyle of the stepper.
labelOrientationhorizontal, verticalOrientation of the labels. This is only applicable if variant is "horizontal".
trackingbooleanTrack the scroll position of the stepper.
initialStepstringInitial step to render.
initialMetadataRecord<string, any>Initial metadata.

StepperNavigation

The StepperNavigation component is used to render the navigation buttons and labels.

StepperStep

The StepperStep component is a wrapper of the button and labels. You just need to pass the of prop which is the step id you want to render.

This is a good place to add your onClick handler.

Props

NameTypeDescription
ofstringStep to render.
iconReact.ReactNodeIcon to render instead of the step number

To keep the stepper simple and consistent, StepperStep only accepts these 3 types of children: StepperTitle, StepperDescription and StepperPanel

StepperTitle

The StepperTitle component is used to render the title of the step.

Props

NameTypeDescription
childrenReact.ReactNodeTitle to render.
asChildbooleanRender as child.

StepperDescription

The StepperDescription component is used to render the description of the step.

Props

NameTypeDescription
childrenReact.ReactNodeDescription to render.
asChildbooleanRender as child.

StepperPanel

The StepperPanel component is used to render the content of the step.

Props

NameTypeDescription
childrenReact.ReactNodeContent to render.
asChildbooleanRender as child.

StepperControls

The StepperControls component is used to render the buttons to navigate through the steps.

Props

NameTypeDescription
childrenReact.ReactNodeButtons to render.
asChildbooleanRender as child.

Before/after actions

You can add a callback to the next and prev methods to execute a callback before or after the action is executed. This is useful if you need to validate the form or check if the step is valid before moving to the prev/next step.

For example:

methods.beforeNext(async () => {
  const valid = await form.trigger();
  if (!valid) return false;
  return true;
});

That function will validate the form and check if the step is valid before moving to the next step returning a boolean value.

More info about the beforeNext and beforePrev methods can be found in the API References.

Skip steps

Through the methods you can access functions like goTo to skip to a specific step.

// From step 1 to step 3
methods.goTo("step-3");

Metadata

You can add metadata to each step to store any information you need. This data can be accessed in the useStepper hook and changed at any time.

const { metadata, getMetadata, setMetadata, resetMetadata } = useStepper();

Multi Scoped

The StepperProvider component can be used multiple times in the same application. Each instance will be independent from the others.

const stepperInstance1 = defineStepper(
  { id: "step-1", title: "Step 1" },
  { id: "step-2", title: "Step 2" },
  { id: "step-3", title: "Step 3" }
)

const stepperInstance2 = defineStepper(
  { id: "step-1", title: "Step 1" },
  { id: "step-2", title: "Step 2" },
  { id: "step-3", title: "Step 3" }
)

<stepperInstance1.StepperProvider>
  <stepperInstance2.StepperProvider>
    ...
  </stepperInstance2.StepperProvider>
</stepperInstance1.StepperProvider>

Examples

Variant

Content for step-1

"use client";

import * as React from "react";
import { styled } from "styled-system/jsx";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { defineStepper } from "@/components/ui/stepper";

type Variant = "horizontal" | "vertical" | "circle";

const {
  StepperProvider,
  StepperControls,
  StepperNavigation,
  StepperPanel,
  StepperStep,
  StepperTitle,
} = defineStepper(
  {
    id: "step-1",
    title: "Step 1",
  },
  {
    id: "step-2",
    title: "Step 2",
  },
  {
    id: "step-3",
    title: "Step 3",
  }
);

export default function StepperVariant() {
  const [variant, setVariant] = React.useState<Variant>("horizontal");

  return (
    <styled.div css={{ display: "flex", w: "full", flexDir: "column", gap: "8" }}>
      <RadioGroup value={variant} onValueChange={(value) => setVariant(value as Variant)}>
        <styled.div css={{ display: "flex", alignItems: "center", columnGap: "2" }}>
          <RadioGroupItem value="horizontal" id="horizontal-variant" />
          <Label htmlFor="horizontal-variant">Horizontal</Label>
        </styled.div>
        <styled.div css={{ display: "flex", alignItems: "center", columnGap: "2" }}>
          <RadioGroupItem value="vertical" id="vertical-variant" />
          <Label htmlFor="vertical-variant">Vertical</Label>
        </styled.div>
        <styled.div css={{ display: "flex", alignItems: "center", columnGap: "2" }}>
          <RadioGroupItem value="circle" id="circle-variant" />
          <Label htmlFor="circle-variant">Circle</Label>
        </styled.div>
      </RadioGroup>
      {variant === "horizontal" && <HorizontalStepper />}
      {variant === "vertical" && <VerticalStepper />}
      {variant === "circle" && <CircleStepper />}
    </styled.div>
  );
}

const HorizontalStepper = () => {
  return (
    <StepperProvider variant="horizontal" css={{ spaceY: "4" }}>
      {({ methods }) => (
        <React.Fragment>
          <StepperNavigation>
            {methods.all.map((step) => (
              <StepperStep key={step.id} of={step.id} onClick={() => methods.goTo(step.id)}>
                <StepperTitle>{step.title}</StepperTitle>
              </StepperStep>
            ))}
          </StepperNavigation>
          {methods.switch({
            "step-1": (step) => <Content id={step.id} />,
            "step-2": (step) => <Content id={step.id} />,
            "step-3": (step) => <Content id={step.id} />,
          })}
          <StepperControls>
            {!methods.isLast && (
              <Button variant="secondary" onClick={methods.prev} disabled={methods.isFirst}>
                Previous
              </Button>
            )}
            <Button onClick={methods.isLast ? methods.reset : methods.next}>
              {methods.isLast ? "Reset" : "Next"}
            </Button>
          </StepperControls>
        </React.Fragment>
      )}
    </StepperProvider>
  );
};

const Content = ({ id }: { id: string }) => {
  return (
    <StepperPanel
      css={{
        h: "200px",
        alignContent: "center",
        rounded: "md",
        borderWidth: "1px",
        bg: "slate.50",
        p: "8",
        _dark: { bg: "slate.950" },
      }}
    >
      <styled.p css={{ textStyle: "xl", fontWeight: "normal" }}>Content for {id}</styled.p>
    </StepperPanel>
  );
};

const VerticalStepper = () => {
  return (
    <StepperProvider variant="vertical" css={{ spaceY: "4" }}>
      {({ methods }) => (
        <>
          <StepperNavigation>
            {methods.all.map((step) => (
              <StepperStep key={step.id} of={step.id} onClick={() => methods.goTo(step.id)}>
                <StepperTitle>{step.title}</StepperTitle>
                {methods.when(step.id, () => (
                  <StepperPanel
                    css={{
                      h: "200px",
                      alignContent: "center",
                      rounded: "md",
                      borderWidth: "1px",
                      bg: "slate.50",
                      p: "8",
                      _dark: { bg: "slate.950" },
                    }}
                  >
                    <styled.p css={{ textStyle: "xl", fontWeight: "normal" }}>
                      Content for {step.id}
                    </styled.p>
                  </StepperPanel>
                ))}
              </StepperStep>
            ))}
          </StepperNavigation>
          <StepperControls>
            {!methods.isLast && (
              <Button variant="secondary" onClick={methods.prev} disabled={methods.isFirst}>
                Previous
              </Button>
            )}
            <Button onClick={methods.isLast ? methods.reset : methods.next}>
              {methods.isLast ? "Reset" : "Next"}
            </Button>
          </StepperControls>
        </>
      )}
    </StepperProvider>
  );
};

const CircleStepper = () => {
  return (
    <StepperProvider variant="circle" css={{ spaceY: "4" }}>
      {({ methods }) => (
        <React.Fragment>
          <StepperNavigation>
            <StepperStep of={methods.current.id}>
              <StepperTitle>{methods.current.title}</StepperTitle>
            </StepperStep>
          </StepperNavigation>
          {methods.when(methods.current.id, () => (
            <StepperPanel
              css={{
                h: "200px",
                alignContent: "center",
                rounded: "md",
                borderWidth: "1px",
                bg: "slate.50",
                p: "8",
                _dark: { bg: "slate.950" },
              }}
            >
              <styled.p css={{ textStyle: "xl", fontWeight: "normal" }}>
                Content for {methods.current.id}
              </styled.p>
            </StepperPanel>
          ))}
          <StepperControls>
            {!methods.isLast && (
              <Button variant="secondary" onClick={methods.prev} disabled={methods.isFirst}>
                Previous
              </Button>
            )}
            <Button onClick={methods.isLast ? methods.reset : methods.next}>
              {methods.isLast ? "Reset" : "Next"}
            </Button>
          </StepperControls>
        </React.Fragment>
      )}
    </StepperProvider>
  );
};

Responsive variant

If you need to render the stepper in a responsive way, you can use a custom hook to detect the screen size and render the stepper in a different variant.

Resize the window to see the stepper in a different variant.

Content for step-1

"use client";

import * as React from "react";
import { styled } from "styled-system/jsx";
import { useMediaQuery } from "@/hooks/use-media-query";
import { Button } from "@/components/ui/button";
import { defineStepper } from "@/components/ui/stepper";

const {
  StepperProvider,
  StepperControls,
  StepperNavigation,
  StepperPanel,
  StepperStep,
  StepperTitle,
} = defineStepper(
  {
    id: "step-1",
    title: "Step 1",
  },
  {
    id: "step-2",
    title: "Step 2",
  },
  {
    id: "step-3",
    title: "Step 3",
  }
);

export default function StepperResponsiveVariant() {
  const isMobile = useMediaQuery("(max-width: 768px)");

  return (
    <StepperProvider variant={isMobile ? "vertical" : "horizontal"} css={{ spaceY: "4" }}>
      {({ methods }) => (
        <React.Fragment>
          <StepperNavigation>
            {methods.all.map((step) => (
              <StepperStep key={step.id} of={step.id} onClick={() => methods.goTo(step.id)}>
                <StepperTitle>{step.title}</StepperTitle>
                {isMobile &&
                  methods.when(step.id, (step) => (
                    <StepperPanel
                      css={{
                        h: "200px",
                        alignContent: "center",
                        rounded: "md",
                        borderWidth: "1px",
                        bg: "slate.50",
                        p: "8",
                        _dark: { bg: "slate.950" },
                      }}
                    >
                      <styled.p css={{ textStyle: "xl", fontWeight: "normal" }}>
                        Content for {step.id}
                      </styled.p>
                    </StepperPanel>
                  ))}
              </StepperStep>
            ))}
          </StepperNavigation>
          {!isMobile &&
            methods.switch({
              "step-1": (step) => <Content id={step.id} />,
              "step-2": (step) => <Content id={step.id} />,
              "step-3": (step) => <Content id={step.id} />,
            })}
          <StepperControls>
            {!methods.isLast && (
              <Button variant="secondary" onClick={methods.prev} disabled={methods.isFirst}>
                Previous
              </Button>
            )}
            <Button onClick={methods.isLast ? methods.reset : methods.next}>
              {methods.isLast ? "Reset" : "Next"}
            </Button>
          </StepperControls>
        </React.Fragment>
      )}
    </StepperProvider>
  );
}

const Content = ({ id }: { id: string }) => {
  return (
    <StepperPanel
      css={{
        h: "200px",
        alignContent: "center",
        rounded: "md",
        borderWidth: "1px",
        bg: "slate.50",
        p: "8",
        _dark: { bg: "slate.950" },
      }}
    >
      <styled.p css={{ textStyle: "xl", fontWeight: "normal" }}>Content for {id}</styled.p>
    </StepperPanel>
  );
};

Description

You can add a description to each step by using <StepperDescription /> component inside <StepperStep />.

Content for step-1

"use client";

import * as React from "react";
import { styled } from "styled-system/jsx";
import { Button } from "@/components/ui/button";
import { defineStepper } from "@/components/ui/stepper";

const {
  StepperProvider,
  StepperControls,
  StepperNavigation,
  StepperPanel,
  StepperStep,
  StepperTitle,
  StepperDescription,
} = defineStepper(
  {
    id: "step-1",
    title: "Step 1",
    description: "This is the first step",
  },
  {
    id: "step-2",
    title: "Step 2",
    description: "This is the second step",
  },
  {
    id: "step-3",
    title: "Step 3",
    description: "This is the third step",
  }
);

export default function StepperDemo() {
  return (
    <StepperProvider variant="horizontal" css={{ spaceY: "4" }}>
      {({ methods }) => (
        <React.Fragment>
          <StepperNavigation>
            {methods.all.map((step) => (
              <StepperStep key={step.id} of={step.id} onClick={() => methods.goTo(step.id)}>
                <StepperTitle>{step.title}</StepperTitle>
                <StepperDescription>{step.description}</StepperDescription>
              </StepperStep>
            ))}
          </StepperNavigation>
          {methods.switch({
            "step-1": (step) => <Content id={step.id} />,
            "step-2": (step) => <Content id={step.id} />,
            "step-3": (step) => <Content id={step.id} />,
          })}
          <StepperControls>
            {!methods.isLast && (
              <Button variant="secondary" onClick={methods.prev} disabled={methods.isFirst}>
                Previous
              </Button>
            )}
            <Button onClick={methods.isLast ? methods.reset : methods.next}>
              {methods.isLast ? "Reset" : "Next"}
            </Button>
          </StepperControls>
        </React.Fragment>
      )}
    </StepperProvider>
  );
}

const Content = ({ id }: { id: string }) => {
  return (
    <StepperPanel
      css={{
        h: "200px",
        alignContent: "center",
        rounded: "md",
        borderWidth: "1px",
        bg: "slate.50",
        p: "8",
        _dark: { bg: "slate.950" },
      }}
    >
      <styled.p css={{ textStyle: "xl", fontWeight: "normal" }}>Content for {id}</styled.p>
    </StepperPanel>
  );
};

Label Orientation

You can change the orientation of the labels by using the labelOrientation prop in the Stepper component.

This is only applicable if variant is "horizontal".

Content for step-1

"use client";

import * as React from "react";
import { styled } from "styled-system/jsx";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { defineStepper } from "@/components/ui/stepper";

type LabelOrientation = "horizontal" | "vertical";

const {
  StepperProvider,
  StepperControls,
  StepperNavigation,
  StepperPanel,
  StepperStep,
  StepperTitle,
} = defineStepper(
  {
    id: "step-1",
    title: "Step 1",
  },
  {
    id: "step-2",
    title: "Step 2",
  },
  {
    id: "step-3",
    title: "Step 3",
  }
);

export default function StepperVariants() {
  const [labelOrientation, setLabelOrientation] = React.useState<LabelOrientation>("horizontal");
  return (
    <styled.div css={{ display: "flex", w: "full", flexDir: "column", gap: "8" }}>
      <RadioGroup
        value={labelOrientation}
        onValueChange={(value) => setLabelOrientation(value as LabelOrientation)}
      >
        <styled.div css={{ display: "flex", alignItems: "center", columnGap: "2" }}>
          <RadioGroupItem value="horizontal" id="horizontal-label" />
          <Label htmlFor="horizontal-label">Horizontal</Label>
        </styled.div>
        <styled.div css={{ display: "flex", alignItems: "center", columnGap: "2" }}>
          <RadioGroupItem value="vertical" id="vertical-label" />
          <Label htmlFor="vertical-label">Vertical</Label>
        </styled.div>
      </RadioGroup>
      <StepperProvider
        variant="horizontal"
        labelOrientation={labelOrientation}
        css={{ spaceY: "4" }}
      >
        {({ methods }) => (
          <React.Fragment>
            <StepperNavigation>
              {methods.all.map((step) => (
                <StepperStep key={step.id} of={step.id} onClick={() => methods.goTo(step.id)}>
                  <StepperTitle>{step.title}</StepperTitle>
                </StepperStep>
              ))}
            </StepperNavigation>
            {methods.switch({
              "step-1": (step) => <Content id={step.id} />,
              "step-2": (step) => <Content id={step.id} />,
              "step-3": (step) => <Content id={step.id} />,
            })}
            <StepperControls>
              {!methods.isLast && (
                <Button variant="secondary" onClick={methods.prev} disabled={methods.isFirst}>
                  Previous
                </Button>
              )}
              <Button onClick={methods.isLast ? methods.reset : methods.next}>
                {methods.isLast ? "Reset" : "Next"}
              </Button>
            </StepperControls>
          </React.Fragment>
        )}
      </StepperProvider>
    </styled.div>
  );
}

const Content = ({ id }: { id: string }) => {
  return (
    <StepperPanel
      css={{
        h: "200px",
        alignContent: "center",
        rounded: "md",
        borderWidth: "1px",
        bg: "slate.50",
        p: "8",
        _dark: { bg: "slate.950" },
      }}
    >
      <styled.p css={{ textStyle: "xl", fontWeight: "normal" }}>Content for {id}</styled.p>
    </StepperPanel>
  );
};

Icon

You can add an icon to each step by using the icon prop in the StepperStep component.

Content for step-1

"use client";

import * as React from "react";
import { LuHouse, LuSettings, LuUser } from "react-icons/lu";
import { styled } from "styled-system/jsx";
import { Button } from "@/components/ui/button";
import { defineStepper } from "@/components/ui/stepper";

const {
  StepperProvider,
  StepperControls,
  StepperNavigation,
  StepperPanel,
  StepperStep,
  StepperTitle,
} = defineStepper(
  {
    id: "step-1",
    title: "Step 1",
    icon: <LuHouse />,
  },
  {
    id: "step-2",
    title: "Step 2",
    icon: <LuSettings />,
  },
  {
    id: "step-3",
    title: "Step 3",
    icon: <LuUser />,
  }
);

export default function StepperDemo() {
  return (
    <StepperProvider variant="horizontal" css={{ spaceY: "4" }}>
      {({ methods }) => (
        <React.Fragment>
          <StepperNavigation>
            {methods.all.map((step) => (
              <StepperStep
                key={step.id}
                of={step.id}
                onClick={() => methods.goTo(step.id)}
                icon={step.icon}
              >
                <StepperTitle>{step.title}</StepperTitle>
              </StepperStep>
            ))}
          </StepperNavigation>
          {methods.switch({
            "step-1": (step) => <Content id={step.id} />,
            "step-2": (step) => <Content id={step.id} />,
            "step-3": (step) => <Content id={step.id} />,
          })}
          <StepperControls>
            {!methods.isLast && (
              <Button variant="secondary" onClick={methods.prev} disabled={methods.isFirst}>
                Previous
              </Button>
            )}
            <Button onClick={methods.isLast ? methods.reset : methods.next}>
              {methods.isLast ? "Reset" : "Next"}
            </Button>
          </StepperControls>
        </React.Fragment>
      )}
    </StepperProvider>
  );
}

const Content = ({ id }: { id: string }) => {
  return (
    <StepperPanel
      css={{
        h: "200px",
        alignContent: "center",
        rounded: "md",
        borderWidth: "1px",
        bg: "slate.50",
        p: "8",
        _dark: { bg: "slate.950" },
      }}
    >
      <styled.p css={{ textStyle: "xl", fontWeight: "normal" }}>Content for {id}</styled.p>
    </StepperPanel>
  );
};

Step tracking

If you need to track the scroll position of the stepper, you can use the tracking prop in the Stepper component.

"use client";

import * as React from "react";
import { styled } from "styled-system/jsx";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { defineStepper } from "@/components/ui/stepper";

const {
  StepperProvider,
  StepperControls,
  StepperNavigation,
  StepperPanel,
  StepperStep,
  StepperTitle,
} = defineStepper(
  {
    id: "step-1",
    title: "Step 1",
  },
  {
    id: "step-2",
    title: "Step 2",
  },
  {
    id: "step-3",
    title: "Step 3",
  },
  {
    id: "step-4",
    title: "Step 4",
  },
  {
    id: "step-5",
    title: "Step 5",
  },
  {
    id: "step-6",
    title: "Step 6",
  }
);

export default function StepperVerticalFollow() {
  const [tracking, setTracking] = React.useState(false);

  return (
    <styled.div css={{ display: "flex", w: "full", flexDir: "column", gap: "8" }}>
      <RadioGroup
        value={tracking.toString()}
        onValueChange={(value) => setTracking(value === "true")}
      >
        <styled.div css={{ display: "flex", alignItems: "center", columnGap: "2" }}>
          <RadioGroupItem value="true" id="tracking" />
          <Label htmlFor="tracking">Tracking</Label>
        </styled.div>
        <styled.div css={{ display: "flex", alignItems: "center", columnGap: "2" }}>
          <RadioGroupItem value="false" id="no-tracking" />
          <Label htmlFor="no-tracking">No Tracking</Label>
        </styled.div>
      </RadioGroup>
      <StepperProvider variant="vertical" tracking={tracking} css={{ spaceY: "4" }}>
        {({ methods }) => (
          <React.Fragment>
            <StepperNavigation>
              {methods.all.map((step) => (
                <StepperStep key={step.id} of={step.id} onClick={() => methods.goTo(step.id)}>
                  <StepperTitle>{step.title}</StepperTitle>
                  {methods.when(step.id, () => (
                    <StepperPanel css={{ spaceY: "4" }}>
                      <styled.div
                        css={{
                          h: "200px",
                          alignContent: "center",
                          rounded: "md",
                          borderWidth: "1px",
                          bg: "slate.50",
                          p: "8",
                          _dark: { bg: "slate.950" },
                        }}
                      >
                        <styled.p css={{ textStyle: "xl", fontWeight: "normal" }}>
                          Content for {step.id}
                        </styled.p>
                      </styled.div>
                      <StepperControls>
                        {!methods.isLast && (
                          <Button
                            variant="secondary"
                            onClick={methods.prev}
                            disabled={methods.isFirst}
                          >
                            Previous
                          </Button>
                        )}
                        <Button onClick={methods.isLast ? methods.reset : methods.next}>
                          {methods.isLast ? "Reset" : "Next"}
                        </Button>
                      </StepperControls>
                    </StepperPanel>
                  ))}
                </StepperStep>
              ))}
            </StepperNavigation>
          </React.Fragment>
        )}
      </StepperProvider>
    </styled.div>
  );
}

Custom components

If you need to add custom components, you can do so by using the useStepper hook and utils from your stepper instance.

If all this is not enough for you, you can use the @stepperize/react API to create your own stepper.