Steps | CoUI
본문으로 건너뛰기
LogoCoUI

Steps

진행 단계를 시각적으로 표시하는 스텝 인디케이터

목차

다단계 진행 상황을 시각적으로 표시하는 읽기 전용 컴포넌트입니다. 번호 원과 연결선으로 단계를 이어 보여 주는 numbered(기본)와, 단계마다 상태 글리프 · 이름 · 오른쪽 글을 한 줄씩 쌓는 list 두 가지 모양이 있습니다.

기본 사용법 (Basic Usage)#

Steps(
  currentStep: 2,
  items: [
    CoreStepData(
      title: 'Create Account',
      description: 'Sign up with your email',
    ),
    CoreStepData(
      title: 'Verify Email',
      description: 'Check your inbox',
    ),
    CoreStepData(
      title: 'Setup Profile',
      description: 'Add your information',
    ),
    CoreStepData(
      title: 'Complete',
      description: 'Ready to go',
    ),
  ],
)

빠른 오버라이드 (Chain)#

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

class StepsChainExample extends StatelessWidget {
  const StepsChainExample({super.key});

  @override
  Widget build(BuildContext context) {
    return const Steps(
      currentStep: 2,
      items: [
        CoreStepData(
          title: 'Create Account',
          description: 'Sign up with your email',
        ),
        CoreStepData(
          title: 'Verify Email',
          description: 'Check your inbox',
        ),
        CoreStepData(
          title: 'Setup Profile',
          description: 'Add your information',
        ),
        CoreStepData(
          title: 'Complete',
          description: 'Ready to go',
        ),
      ],
    ).withStyle(
      const CoreStepsStyle(
        indicatorSize: CoreSize.size40,
        activeIndicatorColor: CoreColor.token(CoreColors.primary),
        connectorThickness: CoreStrokeWidth.stroke4,
        indicatorContentGapStyle: CoreGapStyle(size: CoreSpace.space12),
      ),
    );
  }
}
class StepsChainExample extends StatelessComponent {
  const StepsChainExample({super.key});

  @override
  Component build(BuildContext context) {
    return const Steps(
      currentStep: 2,
      items: [
        CoreStepData(
          title: 'Create Account',
          description: 'Sign up with your email',
        ),
        CoreStepData(
          title: 'Verify Email',
          description: 'Check your inbox',
        ),
        CoreStepData(
          title: 'Setup Profile',
          description: 'Add your information',
        ),
        CoreStepData(
          title: 'Complete',
          description: 'Ready to go',
        ),
      ],
    ).withStyle(
      const CoreStepsStyle(
        indicatorSize: CoreSize.size40,
        activeIndicatorColor: CoreColor.token(CoreColors.primary),
        connectorThickness: CoreStrokeWidth.stroke4,
        indicatorContentGapStyle: CoreGapStyle(size: CoreSpace.space12),
      ),
    );
  }
}

Live Preview#

사용 시기 (When to Use)#

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

  • 주문 진행, 배송 추적, 절차 안내 등 진행 단계를 보여줄 때
  • 각 단계의 완료/진행/대기/실패 상태를 시각적으로 구분해야 할 때
  • 실행 기록·배포 로그처럼 단계마다 상태와 짧은 결과(걸린 시간, 사유)를 한 줄씩 보고할 때 (variant: list)

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

  • Stepper: 사용자가 단계를 클릭하여 이동할 수 있는 인터랙티브 프로세스
  • Progress: 단일 진행률을 바 형태로 표시
  • StepItem: 목록의 행 하나를 직접 놓아야 할 때 (Steps(variant: list) 가 쌓는 바로 그 행)

Props / Parameters#

속성타입기본값설명
items List<CoreStepData> 필수 단계 데이터 목록
currentStep int 0 현재 활성 단계 인덱스 (0-based)
vertical bool true 세로 배치 여부. numbered 에서만 읽습니다 — list 는 늘 세로이고 이 값을 무시합니다
variant CoreStepsVariant CoreStepsVariant.defaultVariant(numbered) 배치 모양 — 번호 원과 연결선(numbered) 또는 상태 행 목록(list)
stepsStyle CoreStepsStyle? null 인스턴스별 chrome 단일 진입점

CoreStepData#

속성타입기본값설명
titleString필수단계 제목
description String? null 선택적 부가 설명. numbered 만 제목 아래에 그립니다 — list 행은 한 줄이라 그리지 않습니다
status CoreStepStatus? null 이 단계가 지금 어디에 있는지( done · current · todo · failed ). null 이면 currentStep 과 순번으로 정해집니다
trailing String? null 행 끝에 붙는 짧은 글(걸린 시간 · 개수 · 사유). list 만 그립니다

CoreStepsStyle 필드#

필드타입설명
indicatorSize double? Indicator circle diameter (logical px).
indicatorBorderRadius CoreBorderRadius? Indicator (circle) corner radius override. Falls back to [defaultIndicatorBorderRadius] (a full circle) when null.
stepPadding CoreEdgeInsets? Padding below each step content in the vertical layout. Falls back to [defaultStepPadding] when null.
stepPaddingHorizontal CoreEdgeInsets? Outer padding around each step content in the horizontal layout — carries the top + right insets (the widget zeroes the right inset on the last item so the connector still ends cleanly).
connectorThickness double? Connector line stroke thickness (logical px).
iconSizeRatio double? Check icon size ratio relative to the indicator diameter.
connectorColor CoreColor? Colour of a connector line that has not been walked yet — the one leading out of a current, upcoming or failed step. Falls back to [defaultConnectorColor].
activeConnectorColor CoreColor? Colour of the connector line leading out of a completed step. Falls back to [defaultActiveConnectorColor].
indicatorColor CoreColor? Inactive indicator fill colour. Falls back to [defaultIndicatorColor].
indicatorBorderColor CoreColor? Ring colour of upcoming indicators. Falls back to [defaultIndicatorBorderColor].
indicatorBorderWidth double? Ring thickness of upcoming indicators. Falls back to [defaultIndicatorBorderWidth].
activeIndicatorColor CoreColor? Active indicator fill colour. Falls back to [defaultActiveIndicatorColor].
failedIndicatorColor CoreColor? Failed step indicator fill colour. Falls back to [defaultFailedIndicatorColor].
activeLabelColor CoreColor? Label colour inside completed / current step indicators (on the active fill). Falls back to [defaultActiveLabelColor].
inactiveLabelColor CoreColor? Label colour inside upcoming step indicators (on the inactive fill). Falls back to [defaultInactiveLabelColor].
failedLabelColor CoreColor? Colour of the cross inside a failed step's indicator (on the failed fill). Falls back to [defaultFailedLabelColor].
titleColor CoreColor? Content title colour of every step that has been reached, and of a failed one. Falls back to [defaultTitleColor].
inactiveTitleColor CoreColor? Content title colour of an upcoming step. Falls back to [defaultInactiveTitleColor].
descriptionColor CoreColor? Content description colour. Falls back to [defaultDescriptionColor].
labelStyle CoreTextStyle? Indicator label typography. Merged on top of [defaultLabelStyle].
titleStyle CoreTextStyle? Content title typography for every step. Merged on top of [defaultTitleStyle], and on the current step on top of the [currentTitleStyle] overlay too, so a weight stated here reaches every step, the current one included.
currentTitleStyle CoreTextStyle? Overlay for the current step's title typography. Merged on top of [defaultCurrentTitleStyle]; the result sits between [defaultTitleStyle] and the per-instance [titleStyle].
descriptionStyle CoreTextStyle? Content description typography. Merged on top of [defaultDescriptionStyle].
indicatorContentGapStyle CoreGapStyle? Nested [CoreGapStyle] slot for the gap between the indicator column and the step content. Merged on top of [defaultIndicatorContentGapStyle].
indicatorConnectorGapStyle CoreGapStyle? Nested [CoreGapStyle] slot for the gap above and below the connector line inside the indicator column. Merged on top of [defaultIndicatorConnectorGapStyle].
indicatorConnectorGapHorizontalStyle CoreGapStyle? Nested [CoreGapStyle] slot for the gap on either side of the connector line inside the indicator row of the horizontal layout. Merged on top of [defaultIndicatorConnectorGapHorizontalStyle]; the vertical layout reads [indicatorConnectorGapStyle].
stepItemStyle CoreStepItemStyle? Nested chrome of every row the list layout ([CoreStepsVariant.list]) stacks — the StepItem it composes. Merged on top of [defaultStepItemStyle] and forwarded raw to each row; the StepItem resolver fills the rest from [CoreStepItemStyle]. The status is not here — each row takes its item's.

변형 (Variants)#

세로 (기본)#

Steps(
  currentStep: 2,
  items: [
    CoreStepData(title: 'Create Account', description: 'Sign up with your email'),
    CoreStepData(title: 'Verify Email', description: 'Check your inbox'),
    CoreStepData(title: 'Setup Profile', description: 'Add your information'),
    CoreStepData(title: 'Complete', description: 'Ready to go'),
  ],
)

가로 (vertical: false)#

Steps(
  currentStep: 1,
  vertical: false,
  items: [
    CoreStepData(title: 'Cart'),
    CoreStepData(title: 'Shipping'),
    CoreStepData(title: 'Payment'),
    CoreStepData(title: 'Review'),
  ],
)

가로 레이아웃은 인디케이터가 동일한 행에 배치되고, 인디케이터 사이에 가로 커넥터 라인이 그려집니다. 각 단계의 콘텐츠는 인디케이터 아래에 배치됩니다.

목록 (variant: list)#

Steps(
  variant: CoreStepsVariant.list,
  currentStep: 1,
  items: [
    CoreStepData(title: 'Install dependencies', trailing: '12s'),
    CoreStepData(title: 'Run tests', trailing: '41s'),
    CoreStepData(title: 'Lint', status: CoreStepStatus.failed, trailing: 'exit 1'),
    CoreStepData(title: 'Deploy', trailing: 'Queued'),
  ],
)

번호 원과 연결선 대신 단계마다 StepItem 행을 하나씩 세로로 쌓습니다 — 상태 글리프 (완료 circle-check · 진행 중 circle-dot · 대기 circle · 실패 circle-x), 한 줄 이름, 행 끝의 오른쪽 글(trailing).

  • 늘 세로입니다. vertical 은 numbered 만 읽고 list 는 무시합니다 — vertical: false 를 줘도 같은 열이 나옵니다.
  • 받은 폭을 채웁니다. 오른쪽 글이 행 끝에 붙으려면 목록의 폭이 필요하므로, 폭은 호스트(카드 · 페이지 열)가 정합니다.
  • description 은 그리지 않습니다. 행은 한 줄이 계약입니다.
  • 행의 치수와 색은 StepItem 의 것입니다. stepsStyle.stepItemStyle 로 덮으면 모든 행에 닿습니다.

단계별 상태 (status)#

currentStep 은 위치로 상태를 정합니다 — 앞은 완료, 그 자리는 진행 중, 뒤는 대기. 그런데 "여기서 멈췄다"(실패)는 어떤 위치로도 말할 수 없으므로, 단계는 자기 status 를 직접 가질 수 있습니다.

Steps(
  currentStep: 1,
  items: [
    CoreStepData(title: 'Create Account'),                              // 위치로 → 완료
    CoreStepData(title: 'Verify Email'),                                // 위치로 → 진행 중
    CoreStepData(title: 'Setup Profile', status: CoreStepStatus.failed), // 직접 → 실패
  ],
)

status 를 가진 단계와 없는 단계는 한 목록에 섞여도 됩니다 — 각 단계가 자기 몫으로 정해집니다 (status ?? 위치로 정한 값). 두 플랫폼 모두 이 한 규칙(CoreStepStatus.effective)을 거치므로 절반만 적은 목록이 플랫폼마다 다르게 그려지지 않습니다.

동작 스펙 (Behavior)#

상태 표시#

단계의 상태는 status ?? 위치로 정한 값 입니다. 위치로 정한 값은 currentStep 이 정합니다.

  • currentStep 미만: 완료 상태 (primary 색상 원 + 체크 아이콘)
  • currentStep 인덱스: 현재 활성 (primary 색상 원 + 번호)
  • currentStep 초과: 대기 상태 (surface 채움 원 + outlineVariant 1px 테두리 + 번호 — 번호와 제목은 onSurfaceVariant 로 한 단계 낮춥니다)
  • status: failed: 실패 상태 (error 색상 원 + onError 가위표 아이콘) — 번호 대신 가위표입니다. 제목은 대기처럼 낮추지 않습니다

번호와 제목은 모든 단계가 SemiBold 입니다 — 현재 단계를 가르는 것은 굵기가 아니라 채운 원과 제목 색입니다.

list 는 같은 상태를 행의 글리프와 색으로 말합니다(StepItem 의 상태 표).

커넥터#

  • 단계 간 선으로 연결 (세로 레이아웃: 세로선, 가로 레이아웃: 가로선). 선은 두 원 사이를 채웁니다
  • 완료된 단계에서 나가는 구간은 primary, 나머지 구간은 outlineVariant — 선도 원처럼 진행을 따라 채워집니다 (activeConnectorColor · connectorColor)
  • 원과 선 사이 간격은 세로 4, 가로 8 입니다 — 가로 행은 선이 옆으로 뻗으므로 더 넓게 둡니다 (indicatorConnectorGapStyle · indicatorConnectorGapHorizontalStyle)
  • list 에는 연결선이 없습니다

인터랙션#

  • 읽기 전용 — 탭/클릭 동작 없음
  • 인터랙티브한 단계 내비게이션은 Stepper 사용

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

현재·완료·대기 단계를 읽기 전용으로 보여주려면 Steps를 사용합니다. 단계 클릭 이벤트가 필요한 위저드에는 Stepper, 정해진 단계 없이 시간순 사건을 나열하려면 Timeline을 선택합니다. currentStep은 0-based 인덱스이며 데이터 진행은 호출자가 전달합니다.

✅ Do#

진행 상태만 필요하면 Steps, 클릭 이동이 필요하면 Stepper

Steps(
  currentStep: 2,
  items: [
    CoreStepData(title: 'Create Account', description: 'Sign up with your email'),
    CoreStepData(title: 'Verify Email'),
    CoreStepData(title: 'Setup Profile'),
  ],
)

Steps는 currentStep 값 하나로 완료/현재/대기 상태를 색상과 아이콘으로 자동 표시합니다 — 각 단계의 색을 직접 계산할 필요가 없습니다.


❌ Don't#

클릭 가능한 것처럼 감싸서 인터랙션을 흉내내지 않기

// ❌ Steps는 콜백이 없어 탭해도 아무 일도 일어나지 않음
GestureDetector(
  onTap: () => setState(() => step++),
  child: Steps(currentStep: step, items: items),
)

Steps는 읽기 전용 컴포넌트입니다 — items(CoreStepData)는 title/description만 받고 클릭 콜백 자체가 없습니다. 단계를 클릭해 이동해야 한다면 Stepper를 사용하세요.

접근성 (Accessibility)#

역할#

  • numbered — 양 플랫폼 모두 자체 role 이나 semantics 를 내보내지 않습니다. 루트에 list 류 role 도, 단계별 aria-current 도 없습니다. 읽기 전용 진행 표시이므로 읽히는 것은 각 단계의 텍스트뿐입니다.
  • list — 루트가 목록(list)이고 행이 항목(listitem)이라 리더가 "5개 중 3번째" 처럼 읽습니다. 진행 중인 단계에는 Web 이 aria-current="step" 을, Flutter 가 selected 를 답니다 — 같은 뜻의 신호이지만 리더가 읽는 문구는 플랫폼마다 다릅니다(아래 알려진 제약 참조).

키보드#

처리하는 키가 없습니다. 양 플랫폼 모두 Clickable 이나 키 핸들러를 합성하지 않고, 클릭·탭 콜백 자체가 계약에 없습니다(steps_contract.dart:22). 클릭 이동이 필요하면 Stepper 를 씁니다.

스크린 리더#

번호·제목·설명이 위젯 / DOM 순서 그대로 텍스트로 읽힙니다.

완료된 단계는 번호가 체크 아이콘으로, 실패한 단계는 가위표 아이콘으로 대체됩니다. Icon 은 어느 쪽에서도 이름을 내보내지 않으므로(Flutter 는 SvgPicture 만, Web 은 <iconify-icon> 에 aria-label 없음) 리더에는 그 단계의 번호도, 완료·실패했다는 사실도 남지 않고 제목·설명만 읽힙니다. list 의 상태 글리프도 마찬가지입니다.

numbered 의 현재 단계 역시 제목 굵기(현재 단계 700 · 나머지 600, CoreStepsStyle.defaultCurrentTitleStyle · defaultTitleStyle)와 인디케이터 색으로만 구분되어, 리더가 읽는 문자열은 대기 단계와 같습니다. list 에서는 현재 단계만 aria-current="step" 이 알려 줍니다.

진행이 바뀐 사실을 알리는 live region 은 양쪽 다 없습니다.

포커스 관리#

진입 · 이탈 · 트랩 모두 없습니다. 포커스 가능한 노드가 하나도 없어(Flutter 에 Focus / FocusNode 없음, Web 에 tabindex 없음) 어느 플랫폼에서도 traversal 정지점이 되지 않습니다. currentStep 이 바뀌어도 포커스는 움직이지 않습니다.

알려진 제약#

  • list 의 현재 단계 신호가 플랫폼마다 다릅니다 — Flutter selected vs Web aria-current="step". 같은 행을 가리키지만 두 플랫폼에서 리더가 읽는 문구가 같지 않습니다.
  • 완료 / 실패 / 대기 상태가 리더에 전달되지 않습니다 — 위에서 본 대로 글리프에는 이름이 없고 굵기·색은 문자열로 드러나지 않습니다. 상태를 읽어 주는 문구는 두 플랫폼의 로컬라이제이션에 문구를 더해야 하는 별도 작업입니다. 대안은 CoreStepData.description(numbered) 이나 trailing(list) 에 상태 문구를 담는 것입니다 — 양 플랫폼 모두 그 문자열을 평범한 텍스트로 읽어 줍니다.
  • 커넥터는 한 색입니다 — 완료 구간과 남은 구간이 같은 색·같은 두께라(connectorColor, 기본 outlineVariant) 진행 정도를 연결선 색으로 읽을 수 없습니다. 그것은 원(채움 · 체크 · 번호)이 말하므로, 색 강제 모드에서 연결선 색이 접혀도 잃는 진행 정보는 없습니다.
  • numbered 의 단계별 노드에 속성을 실을 진입점이 없습니다 — Web Steps(attributes: …) 는 루트 <div> 에만 닿고 인디케이터·제목·커넥터에는 caller 표면이 없으며, Flutter Steps 에는 attributes 표면 자체가 없습니다. numbered 에 role="list" 나 aria-current 를 소비자가 밖에서 보강할 수 없다는 뜻입니다(list 는 내장).
  • 라이트 모드에서 카드 위 list 행의 오른쪽 글은 4.26:1 입니다 — 세 번째 텍스트 단계(onSurfaceSubtle)가 surfaceContainer 위에서 4.5:1 에 못 미치는 기록된 설계 예외입니다. 페이지 면 위와 다크 모드, 그리고 실패한 단계의 오른쪽 글(오류 계열 글자 색)은 해당하지 않습니다.

전역 축(동작 줄이기 · 고대비 · 색 강제 모드 · 시맨틱 역할 · 최소 터치 타겟)은 접근성 페이지가 한 곳에서 다룹니다.

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

항목FlutterWeb
클래스명StepsSteps
렌더링 Column + Row + Container <div> + 인라인 스타일
list 렌더링 Column(stretch) 에 StepItem 을 쌓음 <div>(flex-col, 전폭) 에 StepItem 을 쌓음
list 역할 CoUISemantics list / listItem, 진행 중 행은 selected role="list" / role="listitem", 진행 중 행은 aria-current="step"
아이콘 Icon(LucideIcons.check) · Icon(LucideIcons.x) Icon(LucideIcons.check) · Icon(LucideIcons.x)
테마 CoreStepsTheme (style + variantStyles) CoreStepsTheme (style + variantStyles)
  • StepItem: variant: list 가 쌓는 행 하나 — 단독으로도 쓸 수 있습니다
  • Stepper: 사용자가 클릭으로 이동 가능한 인터랙티브 버전
  • Progress: 단일 진행률 표시