Stepper
Free · MITHeadless, 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
npx astrocraft-ui add navigation/stepperPlain-CSS theme — no build step
npx astrocraft-ui add navigation/stepper --theme cssTailwind theme — needs Tailwind v4
npx astrocraft-ui add navigation/stepper --theme tailwindPlain-CSS theme, re-pointed onto Lumos tokens
npx astrocraft-ui add navigation/stepper --theme css --bridge lumosTailwind theme, re-pointed onto Lumos tokens
npx astrocraft-ui add navigation/stepper --theme tailwind --bridge lumosLive 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.
- Cart
- Delivery
- Payment
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.
| Component | Slots | Variants | Runtime state | Native 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.
---
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>
---
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>
---
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>
---
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>
---
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>
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.