Skip to main content
astrocraft-ui/ components · 101

Stepper

Free · MIT

Headless, accessible, and styled entirely through the attribute surface below — bring your own CSS system, or start from the reference theme.

Install

Without a flag you get no styling at all — plain HTML carrying the attribute surface below. --theme copies the same theme in two dialects;--bridge lumos re-points either one onto Lumos for Astro's tokens (it needs a theme to re-point, and Lumos itself, already in your project).

Unstyled — behavior only, you write the CSS

Install command
npx astrocraft-ui add navigation/stepper

Plain-CSS theme — no build step

Install command
npx astrocraft-ui add navigation/stepper --theme css

Tailwind theme — needs Tailwind v4

Install command
npx astrocraft-ui add navigation/stepper --theme tailwind

Plain-CSS theme, re-pointed onto Lumos tokens

Install command
npx astrocraft-ui add navigation/stepper --theme css --bridge lumos

Tailwind theme, re-pointed onto Lumos tokens

Install command
npx astrocraft-ui add navigation/stepper --theme tailwind --bridge lumos

Live demo

Stepper

One number drives it: data-active on the root. The buttons below do nothing but write that attribute — no API, no event name — and the script derives every step's data-state, moves aria-current="step", and swaps the panel.

  1. Cart
  2. Delivery
  3. Payment
Two items, £48.00.
Standard delivery, 3–5 days.
Card ending 4242.

Attribute surface

With no class-merging machinery, these attributes are the whole extension point. State is a data-attribute or a native attribute; visibility is the hiddenattribute. Generated from source by pnpm slots.

ComponentSlotsVariantsRuntime stateNative state
`Step.astro``step`———
`StepIndicator.astro``step-indicator`———
`StepPanel.astro``step-panel`———
`StepSeparator.astro``step-separator`———
`Stepper.astro``step-list` `stepper``data-orientation`: `horizontal` · `vertical``data-active` `data-state`—

Source

What the command copies — 6 files, alias-free so the copy lands standing alone. This is the whole component, shown without its comments; the files you install keep them, with the reasoning behind every rule.

src/components/ui/navigation/stepper/Step.astro
---
import type { HTMLAttributes } from "astro/types";

type Props = HTMLAttributes<"li">;

const { class: className, ...rest } = Astro.props;
---

<li class={className} data-slot="step" data-state="upcoming" {...rest}>
  <slot />
</li>
src/components/ui/navigation/stepper/StepIndicator.astro
---
import type { HTMLAttributes } from "astro/types";

type Props = HTMLAttributes<"span">;

const { class: className, ...rest } = Astro.props;
---

<span aria-hidden="true" class={className} data-slot="step-indicator" {...rest}><slot /></span>
src/components/ui/navigation/stepper/StepPanel.astro
---
import type { HTMLAttributes } from "astro/types";

type Props = HTMLAttributes<"div">;

const { class: className, ...rest } = Astro.props;
---

<div class={className} data-slot="step-panel" {...rest}>
  <slot />
</div>
src/components/ui/navigation/stepper/StepSeparator.astro
---
import type { HTMLAttributes } from "astro/types";

type Props = HTMLAttributes<"span">;

const { class: className, ...rest } = Astro.props;
---

<span aria-hidden="true" class={className} data-slot="step-separator" {...rest}></span>
src/components/ui/navigation/stepper/Stepper.astro
---
import "../../../../styles/structure.css";

import type { HTMLAttributes } from "astro/types";

type Props = HTMLAttributes<"div"> & {
  orientation?: "horizontal" | "vertical";
  active?: number;
};

const { orientation = "horizontal", active = 1, class: className, ...rest } = Astro.props;
---

<div
  class={className}
  data-slot="stepper"
  data-orientation={orientation}
  data-active={active}
  {...rest}
>
  <ol data-slot="step-list">
    <slot />
  </ol>
  <slot name="panels" />
</div>

<script>
  import { onReadyOnce } from "../../_once";

  function render(root: HTMLElement) {
    const steps = [...root.querySelectorAll<HTMLElement>('[data-slot="step"]')];
    const panels = [...root.querySelectorAll<HTMLElement>('[data-slot="step-panel"]')];
    const active = Number(root.dataset.active) || 1;

    steps.forEach((step, i) => {
      const n = i + 1;
      step.dataset.state = n < active ? "done" : n === active ? "current" : "upcoming";
      if (n === active) step.setAttribute("aria-current", "step");
      else step.removeAttribute("aria-current");
    });

    panels.forEach((panel, i) => {
      panel.hidden = i + 1 !== active;
    });
  }

  function wire(root: HTMLElement) {
    render(root);
    new MutationObserver(() => render(root)).observe(root, {
      attributes: true,
      attributeFilter: ["data-active"],
    });
  }

  onReadyOnce('[data-slot="stepper"]', wire);
</script>
src/components/ui/navigation/stepper/index.ts
import Step from "./Step.astro";
import StepIndicator from "./StepIndicator.astro";
import StepPanel from "./StepPanel.astro";
import Stepper from "./Stepper.astro";
import StepSeparator from "./StepSeparator.astro";

export { Step, StepIndicator, StepPanel, Stepper, StepSeparator };
export default Stepper;

What you get

The component source, copied into your project by npx astrocraft-ui add navigation/stepper — no package dependency, no CSS to fight. Whatever the paid blocks compose, this is it.