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 default
→ CoreStepperTheme.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:
| Value | Description |
|---|---|
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)#
| Parameter | Type | Default | Description |
|---|---|---|---|
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#
| Parameter | Type | Default | Description |
|---|---|---|---|
label | String | required | Step label text |
description |
String? |
null |
Optional description — rendered only in the vertical layout |
Rendering Rules (source-verified)#
-
Completed steps (
index < currentStep): indicator shows theLucideIcons.checkicon over the active fill. -
Current step (
index == currentStep): indicator shows its number over the active fill; label renders bold (unlesslabelStylesets an explicitfontWeight). -
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.
descriptionrenders 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)#
| Field | Type | Description |
|---|---|---|
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) |
connectorMargin | double? | 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 constant | Value |
|---|---|
defaultIndicatorSize |
CoreSpace.space32 (32.0, pre-scaling) |
defaultConnectorThickness | CoreStrokeWidth.stroke2 (2.0) |
defaultIndicatorContentGapStyle |
CoreGapStyle(size: CoreSpace.space12) |
defaultStepPadding |
CoreEdgeInsets.only(bottom: CoreSpace.space24) |
defaultConnectorMargin | CoreSpace.space4 (4.0) |
defaultLabelGapStyle | CoreGapStyle(size: CoreSpace.space4) |
defaultConnectorMinExtent | CoreSpace.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 parameter | Type | Description |
|---|---|---|
id |
String? |
DOM id on the root <div> |
classes | String? | 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#
| Aspect | Flutter | Web (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 bothpackage:flutter/material.dartand the coui barrel, you must hide the Material one:import 'package:flutter/material.dart' hide Stepper;. -
Setting
onStepChangedtonulldisables navigation entirely — the cursor stays basic and indicators are no longer clickable. -
CoreCoStepItem.descriptiononly renders in the vertical layout; it is silently ignored whenorientationis.horizontal. -
On Web, indicators get
role="button"andtabindex="0"whenonStepChangedis set, but only theclickDOM 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) bytheme.scaling, but an explicitindicatorSizeoverride 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 |
|---|---|
❌ CoStepper | Stepper |
❌ CoButton | Button |
❌ CoGap | Gap |
❌ CoIcon | Icon |
❌ CoDivider | Divider |
❌ CoLucideIcons | LucideIcons |
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.