Stepper | CoUI
LogoCoUI

Stepper

다단계 프로세스를 인터랙티브하게 탐색할 수 있는 스텝 컴포넌트

Stepper#

사용자가 단계를 클릭하여 이동할 수 있는 인터랙티브 프로세스 컴포넌트입니다.

Live Preview#

사용 시기 (When to Use)#

이 컴포넌트를 사용하세요:

  • 회원가입, 결제, 설정 마법사 등 다단계 폼
  • 사용자가 이전 단계로 돌아가거나 특정 단계로 점프할 수 있어야 할 때

대신 다른 컴포넌트를 사용하세요:

  • Steps: 읽기 전용 진행 상태 표시 (탭 불가)
  • Tabs: 독립적인 콘텐츠 간 전환 (순서/진행 개념 없음)

기본 사용법 (Basic Usage)#

Stepper(
  currentStep: 1,
  steps: [
    CoreStepItem(label: 'Account', description: 'Create account'),
    CoreStepItem(label: 'Profile', description: 'Set up profile'),
    CoreStepItem(label: 'Complete', description: 'All done'),
  ],
  onStepChanged: (index) => setState(() => _step = index),
)

Props / Parameters#

속성타입기본값설명
stepsList<CoreStepItem>필수단계 목록
currentStep int 0 현재 활성 단계 인덱스
orientation CoreStepperOrientation .horizontal 배치 방향 (horizontal/vertical)
onStepChanged void Function(int)? null 단계 클릭 시 호출
stepperStyle CoreStepperStyle? / CoreStepperStyle? null 인스턴스 스타일 (Style 시스템 참조)

스타일 시스템 (Style System)#

Stepper 의 모든 chrome / dimensional / nested-slot 오버라이드는 CoreStepperStyle 단일 슬롯으로 흐릅니다. 시맨틱 enum (orientation) 과 behaviour (steps / currentStep / onStepChanged) 는 위젯/컴포넌트 파라미터로 직접 전달합니다.

시맨틱 vs 스타일#

  • 시맨틱 enum / behaviour: 위젯/컴포넌트 파라미터로 직접 (steps, currentStep, orientation, onStepChanged)
  • chrome / dimensional / 슬롯 스타일: CoreStepperStyle 한 곳으로 (indicatorActiveColor / indicatorInactiveColor / indicatorBorderColor / indicatorBorderWidth / indicatorSize / connectorActiveColor / connectorInactiveColor / connectorThickness / indicatorContentGapStyle / stepPadding / connectorMargin / horizontalLabelGapStyle / connectorMinExtent / iconSizeRatio / labelStyle / descriptionStyle / indicatorIconStyle)

Resolve chain#

design system default for stepper
  → CoreStepperTheme.style                    // 프로젝트 공통
  → parent component slot override
  → widget.stepperStyle                       // 인스턴스별

각 nested 슬롯 스타일 (labelStyle / descriptionStyle / indicatorIconStyle) 은 자기 컴포넌트의 자체 resolve chain 으로 다시 한 번 머지됩니다.

CoreStepperStyle 필드#

필드타입설명
indicatorActiveColor CoreColor? Indicator (circle) fill colour for the active step.
indicatorInactiveColor CoreColor? Indicator fill colour for inactive / pending steps.
indicatorBorderColor CoreColor? Indicator border stroke colour. No default, and must not have one. The indicator has no ring by default, and this field is half of how a caller asks for one: both platforms compute hasBorder = indicatorBorderColor != null || indicatorBorderWidth != null and draw no stroke when it is false (Flutter leaves BoxDecoration.border null, Web writes no border-* declarations). A constant would make hasBorder permanently true and put a ring on every step indicator on both platforms. It could not hold the right value anyway. Once a caller does opt in without naming a colour, the stroke falls back to the indicator's own fill for that step — indicatorBorderColor ?? color on Flutter, ?? fillCss on Web — which is per-state (active fill vs pending fill) and therefore not something one static const can name. [indicatorBorderWidth] has a named default because a width is the same number in either state; a colour is not.
indicatorBorderWidth double? Indicator border stroke width (logical px).
indicatorSize double? Indicator diameter (logical px).
indicatorBorderRadius CoreBorderRadius? Indicator (circle) corner radius override. Falls back to [defaultIndicatorBorderRadius] (a full circle) when null.
connectorActiveColor CoreColor? Connector line colour for completed segments.
connectorInactiveColor CoreColor? Connector line colour for upcoming segments.
connectorThickness double? Connector line thickness (logical px).
indicatorContentGapStyle CoreGapStyle? Nested [CoreGapStyle] slot for the gap between the indicator column and the step content. Forwarded straight to Gap(gapStyle: …) by the platform resolvers. Per-instance override is merged on top of [defaultIndicatorContentGapStyle].
stepPadding CoreEdgeInsets? Padding below each step content in the vertical layout. Falls back to [defaultStepPadding] when null.
connectorMargin double? Margin around connectors in the horizontal layout (logical px). Falls back to [defaultConnectorMargin] when null.
labelGapStyle CoreGapStyle? Nested [CoreGapStyle] slot for the gap between the indicator and its label in the horizontal layout. Forwarded straight to Gap(gapStyle: …) by the platform resolvers. Per-instance override is merged on top of [defaultLabelGapStyle].
connectorMinExtent double? Minimum extent (length) of the connector in the vertical layout (logical px). Falls back to [defaultConnectorMinExtent] when null.
iconSizeRatio double? Check icon size ratio relative to the indicator diameter. Falls back to [defaultIconSizeRatio] when null.
labelStyle CoreTextStyle? Step label text style override.
descriptionStyle CoreTextStyle? Step description text style override.
indicatorIconStyle CoreIconStyle? Indicator icon style (applied when an indicator renders an icon — e.g. checkmark for completed steps).
indicatorNumberActiveStyle CoreTextStyle? Active (current-step) indicator number text style override. null → [defaultIndicatorNumberActiveStyle].
indicatorNumberInactiveStyle CoreTextStyle? Inactive (pending-step) indicator number text style override. null → [defaultIndicatorNumberInactiveStyle].
labelVerticalStyle CoreTextStyle? Step label text style override applied in vertical orientation. null → [defaultLabelVerticalStyle]. Merged with [labelStyle] on top (per-instance label override applies regardless of orientation).
labelHorizontalStyle CoreTextStyle? Step label text style override applied in horizontal orientation. null → [defaultLabelHorizontalStyle]. Merged with [labelStyle] on top.
clickableStyle CoreClickableStyle? Nested [CoreClickableStyle] slot for the composed per-indicator Clickable (press scale / durations / focus ring / disabled opacity). Merged on top of [defaultClickableStyle] and raw-forwarded — the Clickable's own resolver fills the rest.

빠른 오버라이드 (Chain)#

이미 만든 Stepper 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.

class StepperChainExample extends StatefulWidget {
  const StepperChainExample({super.key});

  @override
  State<StepperChainExample> createState() => _StepperChainExampleState();
}

class _StepperChainExampleState extends State<StepperChainExample> {
  int _currentStep = 1;

  void handleStepChanged(int index) {
    setState(() {
      _currentStep = index;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Stepper(
      currentStep: _currentStep,
      onStepChanged: handleStepChanged,
      steps: const [
        CoreStepItem(label: 'Account', description: 'Create account'),
        CoreStepItem(label: 'Profile', description: 'Set up profile'),
        CoreStepItem(label: 'Complete', description: 'All done'),
      ],
    ).withStyle(
      const CoreStepperStyle(
        indicatorActiveColor: CoreColor.token(CoreColors.primary),
        indicatorSize: CoreSize.size40,
        connectorThickness: CoreStrokeWidth.stroke6,
        indicatorContentGapStyle: CoreGapStyle(size: CoreSpace.space12),
      ),
    );
  }
}
class StepperChainExample extends StatefulComponent {
  const StepperChainExample({super.key});

  @override
  State<StepperChainExample> createState() => _StepperChainExampleState();
}

class _StepperChainExampleState extends State<StepperChainExample> {
  int _currentStep = 1;

  void handleStepChanged(int index) {
    setState(() {
      _currentStep = index;
    });
  }

  @override
  Component build(BuildContext context) {
    return Stepper(
      currentStep: _currentStep,
      onStepChanged: handleStepChanged,
      steps: const [
        CoreStepItem(label: 'Account', description: 'Create account'),
        CoreStepItem(label: 'Profile', description: 'Set up profile'),
        CoreStepItem(label: 'Complete', description: 'All done'),
      ],
    ).withStyle(
      const CoreStepperStyle(
        indicatorActiveColor: CoreColor.token(CoreColors.primary),
        indicatorSize: CoreSize.size40,
        connectorThickness: CoreStrokeWidth.stroke6,
        indicatorContentGapStyle: CoreGapStyle(size: CoreSpace.space12),
      ),
    );
  }
}

변형 (Variants)#

가로 배치#

Stepper(
  currentStep: 1,
  orientation: CoreStepperOrientation.horizontal,
  steps: [
    CoreStepItem(label: '기본 정보'),
    CoreStepItem(label: '주소 입력'),
    CoreStepItem(label: '결제 수단'),
    CoreStepItem(label: '확인'),
  ],
  onStepChanged: (i) => setState(() => _step = i),
)

세로 배치#

Stepper(
  currentStep: 1,
  orientation: CoreStepperOrientation.vertical,
  steps: [
    CoreStepItem(label: '기본 정보', description: '이름과 이메일'),
    CoreStepItem(label: '주소 입력', description: '배송지 설정'),
    CoreStepItem(label: '결제 수단', description: '카드 등록'),
  ],
  onStepChanged: (i) => setState(() => _step = i),
)

동작 스펙 (Behavior)#

상태 표시#

  • currentStep 미만: 완료 (primary 원 + 체크 아이콘)
  • currentStep 인덱스: 현재 (primary 원 + 번호, 볼드 라벨)
  • currentStep 초과: 대기 (surfaceContainer 원 + 번호)

인터랙션#

  • 각 단계 인디케이터 클릭 → onStepChanged(index) 호출
  • 키보드 네비게이션은 표준 Tab 순서 따름

사용 가이드라인 (Usage Guidelines)#

✅ Do#

임의 점프 허용 여부는 콜백 안에서 직접 검증

Stepper(
  currentStep: step,
  steps: steps,
  onStepChanged: (index) {
    if (index <= step) setState(() => step = index); // 완료된 단계로만 되돌아가기 허용
  },
)

onStepChanged가 있으면 완료 여부와 무관하게 모든 인디케이터가 동일하게 클릭 가능해집니다. 아직 도달하지 않은 단계로의 점프를 막으려면 콜백 안에서 인덱스를 직접 검증해야 합니다.


❌ Don't#

읽기 전용으로 쓰려고 no-op 콜백을 넘기지 않기

// ❌ 읽기 전용 진행 표시가 목적인데 onStepChanged를 채워둠
Stepper(
  currentStep: step,
  steps: steps,
  onStepChanged: (_) {},
)

onStepChangednull이 아니면 모든 인디케이터가 Clickable(포커스 링·호버·커서 포함)로 감싸집니다. 진짜 읽기 전용 진행 표시가 필요하면 onStepChanged를 아예 생략하거나 Steps 컴포넌트를 사용하세요.

접근성 (Accessibility)#

키보드#

동작
Tab다음 단계로 포커스 이동
Enter / Space포커스된 단계로 이동

스크린 리더#

  • 각 단계는 순서 + 라벨 + 설명 읽음
  • 완료/현재/대기 상태 시각 표시 기반

크로스 플랫폼 차이점 (Platform Differences)#

항목FlutterWeb
클래스명StepperStepper
렌더링 Column/Row + GestureDetector <div> + 인라인 스타일
아이콘 Icon(LucideIcons.check) Icon(LucideIcons.check)
테마CoreStepperThemeCoreStepperTheme
  • Steps: 읽기 전용 진행 상태 표시
  • Tabs: 독립적인 콘텐츠 전환
  • Progress: 단일 진행률 표시