Schrittanzeige
Die Schritte eines Ablaufs, waagerecht oder senkrecht, mit erledigt, aktuell, offen und Fehler in Worten.
Eine von 6 Components in Navigation
Variants
Details
Was Schrittanzeige mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Navigation
- Bereich
- Elements
- Variants
- 2
- Version
- 1.0.1 · seit 1.0.0
- Datei
- components/ui/stepper.tsx
- Bringt mit
- utilskiticon
- npm-Pakete
- Keine
- Sprachen
- Englisch und Deutsch, 7 eigene Texte
- Studio
- Im Code anpassen
- Mit KI bauen
- Per MCP in Claude, Cursor, Copilot und Codex
Props
Die Schnittstelle StepperProps aus components/ui/stepper.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.
stepsPflichtStep[]currentPflichtnumberIndex of the current step, 0-based. Steps before it count as done.
orientationoptional'horizontal' | 'vertical'markersoptional'numbers' | 'icons'numbers or icons in the markers
compactOnMobileoptionalbooleanOn phones a horizontal stepper becomes "Step 2 of 4" with a bar. Default true.
Eingebaute Texte
Diese Texte schreibt Schrittanzeige selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.
Alle 7 Texte anzeigen
| Schlüssel | Englisch | Deutsch |
|---|---|---|
| label | Englisch Progress | Deutsch Fortschritt |
| step | Englisch Step | Deutsch Schritt |
| of | Englisch of | Deutsch von |
| done | Englisch completed | Deutsch erledigt |
| current | Englisch current step | Deutsch aktueller Schritt |
| error | Englisch needs attention | Deutsch braucht Aufmerksamkeit |
| upcoming | Englisch not started | Deutsch noch offen |
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- Setzt aria-current und aria-label, damit Screenreader Zweck und Zustand ansagen.
- Ergänzt Text für Screenreader, wo Icons und Grafiken allein stehen.
- Kommt ohne Client-State aus. Der Server schreibt sie vollständig, sie ist auch ohne JavaScript lesbar.
Bewegung
- Bringt eigene Animationen mit, ruhig getaktet und ohne Wackeln.
- Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig. 8 Stellen im Code schalten Bewegung dann ab.
Quellcode
Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.
Datei anzeigen, 197 Zeilen
'use client';
import { useRef } from 'react';
import { cn, round, useInView, useTexts } from '@/lib/kit';
import { Icon, type IconName } from '@/components/ui/icon';
const TEXTS = {
en: { label: 'Progress', step: 'Step', of: 'of', done: 'completed', current: 'current step', error: 'needs attention', upcoming: 'not started' },
de: { label: 'Fortschritt', step: 'Schritt', of: 'von', done: 'erledigt', current: 'aktueller Schritt', error: 'braucht Aufmerksamkeit', upcoming: 'noch offen' },
};
/* The current step breathes a quiet ring, so the eye finds it */
const STYLES =
'.kitfx-step-ping{animation:kitfx-step-ping 2.6s var(--ease-kit) infinite}@keyframes kitfx-step-ping{0%{opacity:.55;transform:scale(1)}70%,100%{opacity:0;transform:scale(1.6)}}' +
'@media (prefers-reduced-motion:reduce){.kitfx-step-ping{animation:none;opacity:0}}';
/** How long the track takes to fill, the markers light up along the way */
const FILL_MS = 900;
export interface Step {
title: string;
description?: React.ReactNode;
icon?: IconName;
/** Marks a step that failed or needs input */
error?: boolean;
href?: string;
}
export interface StepperProps {
steps: Step[];
/** Index of the current step, 0-based. Steps before it count as done. */
current: number;
orientation?: 'horizontal' | 'vertical';
/** numbers or icons in the markers */
markers?: 'numbers' | 'icons';
/** On phones a horizontal stepper becomes "Step 2 of 4" with a bar. Default true. */
compactOnMobile?: boolean;
labels?: Partial<(typeof TEXTS)['en']>;
className?: string;
}
type State = 'done' | 'current' | 'upcoming' | 'error';
function Marker({ state, index, icon, markers, lit, delay }: { state: State; index: number; icon?: IconName; markers: 'numbers' | 'icons'; lit: boolean; delay: number }) {
// Before the track reaches it, every marker waits in the upcoming look
const shown: State = lit ? state : state === 'error' ? 'error' : 'upcoming';
return (
<span
aria-hidden
className={cn(
'relative z-10 grid size-8 shrink-0 place-items-center rounded-full text-[13px] font-semibold tabular-nums transition-[background-color,border-color,color,box-shadow,scale] duration-500 ease-[var(--ease-kit)] motion-reduce:transition-none',
shown === 'done' && 'border border-transparent bg-primary text-primary-foreground',
shown === 'current' && 'scale-105 border-[1.5px] border-primary bg-background text-foreground shadow-[0_0_0_4px_color-mix(in_oklab,var(--primary)_14%,transparent)]',
shown === 'upcoming' && 'border border-border-strong bg-background text-subtle-foreground',
shown === 'error' && 'border-[1.5px] border-destructive bg-background text-destructive shadow-[0_0_0_4px_color-mix(in_oklab,var(--destructive)_12%,transparent)]',
)}
style={{ transitionDelay: `${delay}ms` }}
>
{state === 'current' && lit && <span className="kitfx-step-ping pointer-events-none absolute inset-0 rounded-full border border-primary" style={{ animationDelay: `${delay + 400}ms` }} />}
{state === 'done' ? (
<span className="relative grid place-items-center">
<span className={cn('transition-[opacity,scale] duration-300 motion-reduce:transition-none', lit ? 'scale-50 opacity-0' : 'opacity-100')} style={{ transitionDelay: `${delay}ms` }}>
{index + 1}
</span>
<Icon name="check" size={15} strokeWidth={2.2} className={cn('absolute transition-[opacity,scale] duration-500 ease-[cubic-bezier(0.34,1.56,0.64,1)] motion-reduce:transition-none', lit ? 'scale-100 opacity-100' : 'scale-50 opacity-0')} style={{ transitionDelay: `${delay + 80}ms` }} />
</span>
) : state === 'error' ? (
<Icon name="alert-circle" size={16} />
) : markers === 'icons' && icon ? (
<Icon name={icon} size={15} />
) : (
index + 1
)}
</span>
);
}
/**
* The steps of a process: checkout, onboarding, application. Done, current, upcoming and error,
* each said in words to screen readers, not only shown with color. When it scrolls into view the
* track fills up and the markers light up one after another.
* <Stepper current={1} steps={[{ title: 'Cart' }, { title: 'Address' }, { title: 'Payment' }]} />
*/
export function Stepper({ steps, current, orientation = 'horizontal', markers = 'numbers', compactOnMobile = true, labels, className }: StepperProps) {
const t = useTexts(TEXTS, labels);
const state = (i: number, s: Step) => (s.error ? 'error' : i < current ? 'done' : i === current ? 'current' : 'upcoming') as State;
const words = { done: t.done, current: t.current, upcoming: t.upcoming, error: t.error };
const [ref, inView] = useInView<HTMLElement>('0px 0px -10% 0px');
const waited = useRef(false);
if (!inView) waited.current = true;
// Only steps that were waiting below the fold play the fill, everything else stands finished
const play = inView && waited.current;
const gaps = Math.max(1, Math.min(current, steps.length - 1));
const at = (i: number) => (play ? round((Math.min(i, current) / gaps) * FILL_MS) : 0);
const title = (s: Step, st: State) => {
const inner = (
<>
<span className={cn('block text-sm font-medium leading-5', st === 'upcoming' ? 'text-muted-foreground' : st === 'error' ? 'text-destructive' : 'text-foreground')}>{s.title}</span>
<span className="sr-only">, {words[st]}</span>
</>
);
return s.href && st === 'done' ? (
<a href={s.href} className="rounded-sm hover:underline hover:underline-offset-4">
{inner}
</a>
) : (
inner
);
};
if (orientation === 'vertical') {
const per = round(FILL_MS / Math.max(1, current));
return (
<nav ref={ref} aria-label={t.label} className={className}>
<style href="kitfx-step" precedence="default">
{STYLES}
</style>
<ol className="grid">
{steps.map((s, i) => {
const st = state(i, s);
const last = i === steps.length - 1;
return (
<li key={s.title} aria-current={st === 'current' ? 'step' : undefined} className="relative flex gap-4 pb-8 last:pb-0">
{!last && (
<span aria-hidden className="absolute start-4 top-9 bottom-1 w-px -translate-x-1/2 overflow-hidden rounded-full bg-border rtl:translate-x-1/2">
<span
className={cn('block h-full w-full origin-top bg-primary transition-transform ease-linear motion-reduce:transition-none', i < current && inView ? 'scale-y-100' : 'scale-y-0')}
style={{ transitionDuration: `${per}ms`, transitionDelay: `${play ? i * per : 0}ms` }}
/>
</span>
)}
<Marker state={st} index={i} icon={s.icon} markers={markers} lit={inView} delay={play ? Math.min(i, current) * per : 0} />
<div className={cn('min-w-0 pt-1.5 transition-[opacity,translate] duration-500 ease-[var(--ease-kit)] motion-reduce:transition-none', !inView && 'translate-x-2 opacity-0')} style={{ transitionDelay: `${play ? i * 70 : 0}ms` }}>
{title(s, st)}
{s.description && <div className="mt-1 text-pretty text-[13px] leading-relaxed text-muted-foreground">{s.description}</div>}
</div>
</li>
);
})}
</ol>
</nav>
);
}
const share = steps.length > 1 ? current / (steps.length - 1) : 1;
return (
<nav ref={ref} aria-label={t.label} className={cn('w-full', className)}>
<style href="kitfx-step" precedence="default">
{STYLES}
</style>
{compactOnMobile && (
<div className="sm:hidden">
<div className="flex items-baseline justify-between gap-3 text-sm">
<span className="font-medium text-foreground">{steps[current]?.title}</span>
<span className="tabular-nums text-muted-foreground">
{t.step} {Math.min(current + 1, steps.length)} {t.of} {steps.length}
</span>
</div>
<div className="mt-2.5 grid gap-1" style={{ gridTemplateColumns: `repeat(${steps.length}, minmax(0, 1fr))` }} aria-hidden>
{steps.map((s, i) => (
<span key={s.title} className="h-1 overflow-hidden rounded-full bg-track">
{(s.error || i <= current) && (
<span
className={cn('block h-full w-full origin-left rounded-full transition-transform duration-500 ease-[var(--ease-kit)] motion-reduce:transition-none', s.error ? 'bg-destructive' : 'bg-primary', inView ? 'scale-x-100' : 'scale-x-0')}
style={{ transitionDelay: `${play ? i * 140 : 0}ms` }}
/>
)}
</span>
))}
</div>
</div>
)}
<ol data-clip-ok={compactOnMobile || undefined} className={cn('relative grid', compactOnMobile && 'max-sm:sr-only')} style={{ gridTemplateColumns: `repeat(${steps.length}, minmax(0, 1fr))` }}>
{/* One track behind the markers, from the first center to the last center */}
<span aria-hidden className="absolute top-4 h-px -translate-y-1/2 bg-border" style={{ left: `calc(50% / ${steps.length})`, right: `calc(50% / ${steps.length})` }}>
<span
className="block h-full w-full origin-left bg-primary transition-transform ease-[cubic-bezier(0.45,0,0.25,1)] motion-reduce:transition-none rtl:origin-right"
style={{ transform: `scaleX(${inView ? round(share, 4) : 0})`, transitionDuration: `${play ? FILL_MS : 500}ms` }}
/>
</span>
{steps.map((s, i) => {
const st = state(i, s);
return (
<li key={s.title} aria-current={st === 'current' ? 'step' : undefined} className="flex min-w-0 flex-col items-center px-2 text-center">
<Marker state={st} index={i} icon={s.icon} markers={markers} lit={inView} delay={at(i)} />
<div className={cn('mt-3 min-w-0 transition-[opacity,translate] duration-500 ease-[var(--ease-kit)] motion-reduce:transition-none', !inView && 'translate-y-1.5 opacity-0')} style={{ transitionDelay: `${play ? at(i) : 0}ms` }}>
{title(s, st)}
{s.description && <div className="mt-0.5 text-pretty text-xs text-muted-foreground">{s.description}</div>}
</div>
</li>
);
})}
</ol>
</nav>
);
}Häufige Fragen
Wie installiere ich Schrittanzeige?
Mit aiw add @ai-website/stepper. Der Befehl legt die Datei components/ui/stepper.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind icon. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Schrittanzeige?
2 Varianten, Waagerecht und Senkrecht.
Kann ich die Texte von Schrittanzeige übersetzen?
Ja. Schrittanzeige bringt 7 eigene Texte auf Deutsch und Englisch mit, etwa „Fortschritt“, „Schritt“ und „von“. Die Sprache folgt dem KitProvider, einzelne Texte ersetzt du über die Prop labels.
Kann ein KI-Assistent Schrittanzeige einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Schrittanzeige mit aiw_components und installieren sie mit aiw_add @ai-website/stepper.