Stepper
Interactive UI Explorer
아래에서 각 컴포넌트의 다양한 Variants와 States를 문서화된 형태로 테스트해볼 수 있습니다.
Live Preview
- 계정
- 프로필
- 약관 동의
- 완료
Stepper
다단계 진행 인디케이터입니다. 완료/현재/예정 상태를 원+연결선으로 표시하고, onJump을 주면 통과한 단계로 클릭 점프할 수 있습니다.
진행 + 점프
이전/다음으로 단계를 옮기고, 통과한 단계는 인디케이터를 눌러 되돌아갑니다.
Source Code
- 계정
- 프로필
- 약관 동의
- 완료
Live Preview
표시 전용
onJump 없이 진행 상태만 보여줍니다.
Source Code
- 주문
- 결제
- 배송
- 완료
Live Preview
Implementation
제작 코드
이 컴포넌트가 실제로 어떻게 구현되어 있는지 — 본체 .tsx 파일을 그대로 보여줍니다. variant 매핑·접근성 처리·forwardRef 패턴 등 디테일을 그대로 확인할 수 있어요.
Steppertypescript
'use client';
/**
* Stepper 컴포넌트
*
* 다단계 진행 인디케이터. 완료/현재/예정 상태를 원 + 연결선으로 표시합니다.
* - onJump을 주면 통과한(이전) 단계로 클릭 점프 — current 이후·maxStep 초과는 비활성.
* - 표시 전용으로 쓰려면 onJump을 생략하면 됩니다.
*/
import { cn } from '@/lib/cn';
export interface StepperProps {
steps: string[];
/** 현재 단계 인덱스 (0-based) */
current: number;
/** 점프 허용 최고 단계 (기본 current) */
maxStep?: number;
/** 단계 클릭 시 호출 — 생략하면 표시 전용 */
onJump?: (index: number) => void;
className?: string;
}
export function Stepper({ steps, current, maxStep, onJump, className }: StepperProps) {
const reachable = maxStep ?? current;
return (
<ol className={cn('flex items-center', className)}>
{steps.map((label, i) => {
const state = i < current ? 'done' : i === current ? 'current' : 'upcoming';
const clickable = !!onJump && i < current && i <= reachable;
const last = i === steps.length - 1;
return (
<li key={`${label}-${i}`} className={cn('flex items-center', !last && 'flex-1')}>
<div className="flex flex-col items-center gap-1.5">
<button
type="button"
onClick={() => clickable && onJump?.(i)}
disabled={!clickable}
aria-current={state === 'current' ? 'step' : undefined}
className={cn(
'flex h-9 w-9 items-center justify-center rounded-full border-2 text-sm font-bold transition-all duration-300',
clickable && 'cursor-pointer hover:scale-105',
state === 'done' && 'border-slate-900 bg-slate-900 text-white',
state === 'current' && 'border-slate-900 bg-white text-slate-900 ring-4 ring-slate-900/10',
state === 'upcoming' && 'border-slate-200 bg-white text-slate-300',
)}
>
{state === 'done' ? (
<svg className="h-4 w-4" viewBox="0 0 20 20" fill="none" aria-hidden>
<path d="M5 10.5l3.2 3.2L15 7" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
) : (
i + 1
)}
</button>
<span
className={cn(
'whitespace-nowrap text-[11px] font-semibold transition-colors',
state === 'upcoming' ? 'text-slate-300' : 'text-slate-600',
)}
>
{label}
</span>
</div>
{!last && (
<div className="mx-1 -mt-5 h-0.5 flex-1 overflow-hidden rounded-full bg-slate-200">
<div
className="h-full rounded-full bg-slate-900 transition-all duration-500"
style={{ width: i < current ? '100%' : '0%' }}
/>
</div>
)}
</li>
);
})}
</ol>
);
}