LogoSkills

coui-stepper

다단계 폼, 위저드 플로우, 결제 진행 인디케이터, 단계별 내비게이션을 CoUI Flutter(coui_flutter) 또는 CoUI Web(coui_web)에서 Stepper와 CoreCoStepItem, CoreStepperOrientation(horizontal, vertical), CoreStepperStyle, CoreStepperTheme로...

CoUI Stepper#

Quick Reference#

  • Widget: Stepper
  • Variant: 없음 (variant 파라미터 없음)
  • Size: 없음
  • Style slot: stepperStyle: CoreStepperStyle(...)
  • Canonical snippet:
Stepper(
  currentStep: _currentStep,
  steps: const [
    CoreCoStepItem(label: 'Account'),
    CoreCoStepItem(label: 'Details'),
    CoreCoStepItem(label: 'Confirm'),
  ],
  onStepChanged: (index) => setState(() => _currentStep = index),
)

한마디로#

이 스킬은 "여러 단계로 나뉜 화면"을 만드는 도구입니다. 마치 택배 배송 조회에서 "주문 → 결제 → 배송 → 완료"처럼 지금 어디까지 왔는지 동그란 번호와 연결선으로 보여주는 그 표시를 만들어 줍니다. 가입 절차나 결제 과정처럼 한 번에 다 하기 부담스러운 일을 작은 단계로 쪼개 안내할 때 씁니다.

무엇을·언제#

  • 무엇을 해주나요: 여러 단계로 진행되는 화면(단계 표시줄)을 만들어 줍니다. 가로/세로 배치, 단계 클릭으로 이동, 색·크기 등 모양 꾸미기를 지원합니다. 완료한 단계는 체크 표시로, 아직 안 한 단계는 번호로 보여줍니다.
  • 언제 작동하나요: 회원가입 마법사, 결제(장바구니 → 배송 → 결제 → 완료) 같은 여러 단계 폼, 단계별 안내 흐름을 만들 때 활성화됩니다.
  • 어디에 쓰나요: Flutter 앱과 웹(Jaspr) 양쪽에서 동일하게 사용할 수 있습니다.

핵심 용어#

용어쉬운 설명
Stepper(스테퍼)진행 단계를 번호와 연결선으로 보여주는 단계 표시줄
step(스텝)전체 과정 중 하나의 단계 (예: "결제")
indicator(인디케이터)각 단계를 나타내는 동그란 번호(또는 체크) 표시
connector(커넥터)단계와 단계 사이를 잇는 연결선
label(레이블)단계 이름 글자 (예: "배송")
description(설명)단계 아래 붙는 부가 설명 글자 (세로 배치에서만 표시)
orientation(방향)단계를 가로로 늘어놓을지 세로로 쌓을지 결정
horizontal/vertical가로 배치 / 세로 배치
style(스타일)색·크기·두께 등 단계 표시줄의 겉모양 설정
Legacy(레거시)더 이상 쓰면 안 되는 옛날 방식 (새 방식으로 교체 필요)

Overview#

Stepper displays a multi-step process with circular indicators, connector lines, and step labels. Completed steps render a LucideIcons.check glyph; the current and pending steps render their 1-based number. It supports horizontal and vertical layouts, click-to-navigate via onStepChanged, and full chrome customisation through a single stepperStyle slot.

The canonical API (coui v0.127.15) is Stepper on both platforms, sharing CoreStepperContract from coui_core. Behaviour fields (steps, currentStep, orientation, onStepChanged) stay flat on the widget; every chrome/dimensional/nested-slot override flows through the single stepperStyle: CoreStepperStyle(...) slot, with a project-level base in CoreStepperTheme.

Resolve chain (identical on both platforms):

design-system defaultCoreStepperTheme.style          // project-wide (coreComponentTheme.stepper)
  → widget.stepperStyle             // per-instance

Variants & Sizes#

The stepper is a layout/structural component: it has no variant enum and no size enum. There is no CoreStepperVariant and no CoreComponentSize parameter on any stepper class. The only semantic enum is the orientation — full set:

ValueDescription
CoreStepperOrientation.horizontal Horizontal layout (default). Indicator above label, connectors run between steps.
CoreStepperOrientation.vertical Vertical layout. Indicator column on the left, label + optional description on the right, vertical connectors.

Indicator size, connector thickness, colours, and typography all go through CoreStepperStyle — see the style slot table below.

Flutter (coui_flutter)#

Import#

import 'package:coui_flutter/coui_flutter.dart';

Note: Flutter's Material library also exports a class named Stepper. When a file imports both package:flutter/material.dart and the coui barrel, hide the Material one: import 'package:flutter/material.dart' hide Stepper;.

Horizontal Stepper (default)#

Stepper(
  currentStep: _currentStep,
  steps: const [
    CoreCoStepItem(label: 'Account'),
    CoreCoStepItem(label: 'Details'),
    CoreCoStepItem(label: 'Confirm'),
  ],
  onStepChanged: (index) => setState(() => _currentStep = index),
)

Vertical Stepper#

Stepper(
  currentStep: _currentStep,
  orientation: CoreStepperOrientation.vertical,
  steps: const [
    CoreCoStepItem(label: 'Create Account', description: 'Name and email'),
    CoreCoStepItem(label: 'Set Up Profile'),
    CoreCoStepItem(label: 'Complete'),
  ],
  onStepChanged: (index) => setState(() => _currentStep = index),
)

Stepper Parameters (Flutter)#

ParameterTypeDefaultDescription
steps List<CoreCoStepItem> required List of steps to display
currentStep int 0 Index of the currently active step
orientation CoreStepperOrientation .horizontal Layout orientation
onStepChanged ValueChanged<int>? null Called with the tapped step index when an indicator is tapped. null disables navigation (cursor stays basic)
stepperStyle CoreStepperStyle? null Per-instance chrome and nested slot overrides

CoreCoStepItem Parameters#

ParameterTypeDefaultDescription
labelStringrequiredStep label text
description String? null Optional description — rendered only in the vertical layout

Rendering Rules (source-verified)#

  • Completed steps (index < currentStep): indicator shows the LucideIcons.check icon over the active fill.
  • Current step (index == currentStep): indicator shows its number over the active fill; label renders bold (unless labelStyle sets an explicit fontWeight).
  • Pending steps (index > currentStep): indicator shows its number over the inactive fill.
  • Connector segments before the current step use the active colour, the rest the inactive colour.
  • description renders only in the vertical layout, below the label.

Style Customisation#

All chrome flows through the single stepperStyle: CoreStepperStyle(...) slot:

Stepper(
  currentStep: _currentStep,
  steps: const [
    CoreCoStepItem(label: 'Step 1'),
    CoreCoStepItem(label: 'Step 2'),
    CoreCoStepItem(label: 'Step 3'),
  ],
  stepperStyle: const CoreStepperStyle(
    indicatorActiveColor: CoreColor.token(CoreColors.secondary),
    connectorActiveColor: CoreColor.token(CoreColors.secondary),
    indicatorSize: 36.0,
    connectorThickness: 2.0,
    labelStyle: CoreTextStyle(fontWeight: CoreFontWeight.semiBold),
  ),
  onStepChanged: (index) => setState(() => _currentStep = index),
)

CoreStepperStyle Fields (all 17, source-verified)#

FieldTypeDescription
indicatorActiveColor CoreColor? Indicator fill for the current and completed steps
indicatorInactiveColor CoreColor? Indicator fill for pending steps
indicatorBorderColor CoreColor? Indicator border stroke colour (no border painted when both border fields are null)
indicatorBorderWidth double? Indicator border stroke width (logical px; falls back to CoreStrokeWidth.stroke1 when only the colour is set)
indicatorSize double? Indicator circle diameter (logical px; explicit override is used as-is, un-scaled)
connectorActiveColor CoreColor? Connector colour for completed segments
connectorInactiveColor CoreColor? Connector colour for pending segments
connectorThickness double? Connector line thickness (logical px)
indicatorContentGapStyle CoreGapStyle? Gap between indicator column and step content (vertical layout)
stepPadding CoreEdgeInsets? Padding below each non-final step (vertical layout)
connectorMargindouble?Margin around connectors
labelGapStyle CoreGapStyle? Gap between indicator and label (horizontal layout)
connectorMinExtent double? Minimum connector length in the vertical layout (logical px)
iconSizeRatio double? Check icon size relative to indicator diameter
labelStyle CoreTextStyle? Step label typography override
descriptionStyle CoreTextStyle? Step description typography override
indicatorIconStyle CoreIconStyle? Completed-step check icon style override

CoreStepperStyle also provides merge(other) and copyWith(...); nested labelStyle / descriptionStyle / indicatorIconStyle / gap slots merge recursively.

Design-system Defaults (source-verified statics on CoreStepperStyle)#

Static constantValue
defaultIndicatorSize CoreSpace.space32 (32.0, pre-scaling)
defaultConnectorThicknessCoreStrokeWidth.stroke2 (2.0)
defaultIndicatorContentGapStyle CoreGapStyle(size: CoreSpace.space12)
defaultStepPadding CoreEdgeInsets.only(bottom: CoreSpace.space24)
defaultConnectorMarginCoreSpace.space4 (4.0)
defaultLabelGapStyleCoreGapStyle(size: CoreSpace.space4)
defaultConnectorMinExtentCoreSpace.space8 (8.0)
defaultIconSizeRatio 0.5 (check icon = 50% of indicator diameter)
defaultIndicatorActiveColor CoreColor.token(CoreColors.primary)
defaultIndicatorInactiveColor CoreColor.token(CoreColors.surfaceContainer)
defaultConnectorActiveColor CoreColor.token(CoreColors.primary)
defaultConnectorInactiveColor CoreColor.token(CoreColors.surfaceContainer)
defaultIndicatorIconColor CoreColor.token(CoreColors.onPrimary)
defaultIndicatorNumberActiveStyle bodySmall role, bold, onPrimary
defaultIndicatorNumberInactiveStyle bodySmall role, bold, onSurface
defaultLabelVerticalStyle titleSmall role, onSurface
defaultLabelHorizontalStyle labelSmall role, onSurface
defaultDescriptionStyle bodySmall role, onSurfaceVariant

Project-level Theme#

Apply a shared style across all Stepper instances via CoreStepperTheme (the stepper slot on CoreComponentTheme):

// In your theme configuration:
coreComponentTheme: CoreComponentTheme(
  stepper: CoreStepperTheme(
    style: CoreStepperStyle(
      indicatorActiveColor: CoreColor.token(CoreColors.primary),
      connectorThickness: 2.0,
    ),
  ),
),

Multi-step Form Pattern#

import 'package:coui_flutter/coui_flutter.dart';
import 'package:flutter/widgets.dart';

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

  @override
  State<CheckoutStepper> createState() => _CheckoutStepperState();
}

class _CheckoutStepperState extends State<CheckoutStepper> {
  int _currentStep = 0;

  static const _steps = [
    CoreCoStepItem(label: 'Cart'),
    CoreCoStepItem(label: 'Shipping'),
    CoreCoStepItem(label: 'Payment'),
    CoreCoStepItem(label: 'Done'),
  ];

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Stepper(
          currentStep: _currentStep,
          steps: _steps,
          onStepChanged: (index) => setState(() => _currentStep = index),
        ),
        Expanded(child: _buildStepContent(_currentStep)),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            if (_currentStep > 0)
              Button(
                onPressed: () => setState(() => _currentStep--),
                child: const Text('Back'),
              ),
            Button(
              onPressed: _currentStep < _steps.length - 1
                  ? () => setState(() => _currentStep++)
                  : null,
              child: Text(_currentStep < _steps.length - 1 ? 'Next' : 'Complete'),
            ),
          ],
        ),
      ],
    );
  }

  Widget _buildStepContent(int step) {
    // Swap in the form body for each step here.
    return Center(child: Text('Step ${step + 1} content'));
  }
}

Web (coui_web / Jaspr)#

Import#

import 'package:coui_web/coui_web.dart';

Horizontal Stepper#

Stepper(
  currentStep: currentStep,
  steps: const [
    CoreCoStepItem(label: 'Account'),
    CoreCoStepItem(label: 'Details'),
    CoreCoStepItem(label: 'Confirm'),
  ],
  onStepChanged: (index) => setState(() => currentStep = index),
)

Vertical Stepper#

Stepper(
  currentStep: currentStep,
  orientation: CoreStepperOrientation.vertical,
  steps: const [
    CoreCoStepItem(label: 'Create Account', description: 'Name and email'),
    CoreCoStepItem(label: 'Set Up Profile'),
    CoreCoStepItem(label: 'Complete'),
  ],
  onStepChanged: (index) => setState(() => currentStep = index),
)

Stepper Parameters (Web)#

The behaviour and style parameters are identical to Flutter (steps, currentStep, orientation, onStepChanged, stepperStyle). In addition, the Web Stepper extends UiComponent (rendered as a <div>) and accepts the standard Web chrome parameters:

Extra parameterTypeDescription
id String? DOM id on the root <div>
classesString?Extra CSS classes on the root
css Styles? Inline styles combined with the resolved root styles
attributes Map<String, String>? Extra HTML attributes
eventHandlers Map<String, List<UiEventHandler>>? Extra DOM event handlers

The Web Stepper also provides a copyWith(...) method covering all of the above plus the behaviour fields.

Web Accessibility Behaviour (source-verified)#

When onStepChanged is non-null, each indicator <div> gets role="button" and tabindex="0", and a click DOM event that calls onStepChanged(index). Only the click event is wired in v0.127.15 (no keydown handler).

Flutter ↔ Web Differences#

AspectFlutterWeb (Jaspr)
Import package:coui_flutter/coui_flutter.dart package:coui_web/coui_web.dart
Widget class Stepper (extends StatelessWidget) Stepper (extends UiComponent, renders <div>)
Shared contract CoreStepperContract CoreStepperContract (identical)
Step item type CoreCoStepItem CoreCoStepItem (identical)
Orientation enum CoreStepperOrientation CoreStepperOrientation (identical)
Callback onStepChanged: ValueChanged<int>? onStepChanged — same name; contract type CoreValueChanged<int> (= void Function(int) )
Style slot / theme stepperStyle: CoreStepperStyle?, CoreStepperTheme identical
Completed-step icon Icon(LucideIcons.check, iconStyle: …) Icon(LucideIcons.check, iconStyle: …) (Web Icon component)
Click affordance MouseRegion + GestureDetector.onTap role="button" + tabindex="0" + click event
Extra chrome params none id , classes , css , attributes , eventHandlers ; plus copyWith(...)
Resolver output ResolvedStepper with paint-ready Color / TextStyle ResolvedStepper with WebElementChrome lists (Tailwind-style class names + inline CSS variables)
Scaling design defaults multiplied by theme.scaling ; explicit indicatorSize override used as-is no scaling step; values emitted as CSS px

The Stepper public API (parameter names, types, and every CoreStepperStyle field) is identical between Flutter and Web; the resolver internals differ but are transparent to callers.

Pitfalls#

  • Flutter's Material library also exports a class named Stepper — when a file imports both package:flutter/material.dart and the coui barrel, you must hide the Material one: import 'package:flutter/material.dart' hide Stepper;.
  • Setting onStepChanged to null disables navigation entirely — the cursor stays basic and indicators are no longer clickable.
  • CoreCoStepItem.description only renders in the vertical layout; it is silently ignored when orientation is .horizontal.
  • On Web, indicators get role="button" and tabindex="0" when onStepChanged is set, but only the click DOM event is wired in v0.127.15 — there is no keydown handler, so keyboard-only activation (Enter/Space) does not work despite the ARIA affordance.
  • Scaling differs cross-platform: Flutter multiplies design-default dimensions (e.g. indicatorSize) by theme.scaling, but an explicit indicatorSize override is used as-is, un-scaled; Web has no scaling step at all and emits values as raw CSS px — the same style values can render at different actual sizes on each platform.

v0.127 Migration (Legacy)#

v0.127.0 removed the Co prefix from widget classes — a hard break with no aliases. ❌ marks legacy identifiers that no longer exist and must not appear in new code. Core* classes (CoreCoStepItem, CoreStepperOrientation, CoreStepperStyle, CoreStepperTheme, CoreGapStyle, CoreTextStyle, CoreIconStyle, …) are unchanged.

Legacy (❌ removed in v0.127)v0.127 Canonical
CoStepperStepper
CoButtonButton
CoGapGap
CoIconIcon
CoDividerDivider
CoLucideIconsLucideIcons

The even older shadcn-style stepper API (a controller-based Stepper with StepperController, Step, StepVariant, StepSize) was already removed before v0.127 and does not exist in v0.127.15 — the bare name Stepper now refers exclusively to the unified contract-based widget documented above.