// Stepper -- ordered progress through a sequence. // // // // Each step can be authored by hand instead of using the built-in body, by // passing a slot named for its index: // // //
...anything...
//
// // NOTE: the style block uses /* */ comments only -- // is not a CSS comment // and silently swallows the rule that follows it. component Stepper { outputs { change(payload: { index: number; step: object }) } props { steps: unknown[] = [] active: number = 0 orientation: string = "horizontal" clickable: boolean = false label: string = "Progress" class: string = "" } functions { shared function stepList() { return Array.isArray(steps) ? steps : [] } shared function activeIndex() { var count = stepList().length if (count < 1) { return 0 } var value = Number(active) if (!value || value < 0) { return 0 } return Math.min(value, count - 1) } shared function statusFor(index) { if (index < activeIndex()) { return "complete" } if (index === activeIndex()) { return "current" } return "upcoming" } // An empty orientation is how the roving runtime is told to stay out of // the way, so a read-only stepper never takes arrow-key focus. shared function rovingAxis() { if (!clickable) { return "" } return orientation === "vertical" ? "vertical" : "horizontal" } client function selectStep(index, step) { if (!clickable) { return } output.change({ index: index, step: step }) } } view {
    {#each stepList() as step, index}
  1. {/each}
} style { .wire-stepper { display: flex; gap: 0.5rem; margin: 0; padding: 0; list-style: none; max-width: 100%; } .wire-stepper[data-orientation="vertical"] { flex-direction: column; } .wire-stepper__step { display: flex; flex-direction: column; flex: 1 1 0; min-width: 0; gap: 0.35rem; } .wire-stepper__button { appearance: none; display: flex; align-items: center; gap: 0.6rem; width: 100%; padding: 0.5rem; border: 0; border-radius: var(--wire-radius-sm); background: transparent; color: inherit; font: inherit; text-align: left; cursor: default; } .wire-stepper[data-clickable="true"] .wire-stepper__button { cursor: pointer; } .wire-stepper[data-clickable="true"] .wire-stepper__button:hover { background: var(--wire-color-surface-soft); } .wire-stepper__button:focus-visible { outline: 2px solid var(--wire-color-primary); outline-offset: 2px; } .wire-stepper__marker { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; width: 2rem; height: 2rem; border: 1px solid var(--wire-color-border); border-radius: 999px; background: var(--wire-color-surface); font-size: 0.85rem; font-weight: 700; } .wire-stepper__step[data-status="complete"] .wire-stepper__marker { border-color: var(--wire-color-primary); background: var(--wire-color-primary); color: var(--wire-color-primary-contrast); } .wire-stepper__step[data-status="current"] .wire-stepper__marker { border-color: var(--wire-color-primary); color: var(--wire-color-primary); box-shadow: 0 0 0 3px color-mix(in srgb, var(--wire-color-primary) 22%, transparent); } .wire-stepper__step[data-status="upcoming"] .wire-stepper__marker { color: var(--wire-color-text-muted); } .wire-stepper__body { display: flex; flex-direction: column; min-width: 0; } .wire-stepper__label { font-size: 0.9rem; font-weight: 600; } .wire-stepper__description { color: var(--wire-color-text-muted); font-size: 0.78rem; } .wire-stepper__step[data-status="upcoming"] .wire-stepper__label { color: var(--wire-color-text-muted); } /* A horizontal stepper cannot stay side by side on a phone. */ @media (max-width: 639px) { .wire-stepper { flex-direction: column; } } } }