Stepper#
사용자가 단계를 클릭하여 이동할 수 있는 인터랙티브 프로세스 컴포넌트입니다.
Live Preview#
class StepperDefaultExample extends StatefulComponent {
const StepperDefaultExample({super.key});
@override
State<StepperDefaultExample> createState() => _StepperDefaultExampleState();
}
class _StepperDefaultExampleState extends State<StepperDefaultExample> {
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'),
],
);
}
}
class StepperDefaultExample extends StatefulWidget {
const StepperDefaultExample({super.key});
@override
State<StepperDefaultExample> createState() => _StepperDefaultExampleState();
}
class _StepperDefaultExampleState extends State<StepperDefaultExample> {
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'),
],
);
}
}
class StepperVerticalExample extends StatefulComponent {
const StepperVerticalExample({super.key});
@override
State<StepperVerticalExample> createState() => _StepperVerticalExampleState();
}
class _StepperVerticalExampleState extends State<StepperVerticalExample> {
int _currentStep = 1;
void handleStepChanged(int index) {
setState(() {
_currentStep = index;
});
}
@override
Component build(BuildContext context) {
return Stepper(
currentStep: _currentStep,
orientation: CoreStepperOrientation.vertical,
onStepChanged: handleStepChanged,
steps: const [
CoreStepItem(label: 'Account', description: 'Create account'),
CoreStepItem(label: 'Profile', description: 'Set up profile'),
CoreStepItem(label: 'Complete', description: 'All done'),
],
);
}
}
class StepperVerticalExample extends StatefulWidget {
const StepperVerticalExample({super.key});
@override
State<StepperVerticalExample> createState() => _StepperVerticalExampleState();
}
class _StepperVerticalExampleState extends State<StepperVerticalExample> {
int _currentStep = 1;
void handleStepChanged(int index) {
setState(() {
_currentStep = index;
});
}
@override
Widget build(BuildContext context) {
return Stepper(
currentStep: _currentStep,
orientation: CoreStepperOrientation.vertical,
onStepChanged: handleStepChanged,
steps: const [
CoreStepItem(label: 'Account', description: 'Create account'),
CoreStepItem(label: 'Profile', description: 'Set up profile'),
CoreStepItem(label: 'Complete', description: 'All done'),
],
);
}
}
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),
),
);
}
}
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),
),
);
}
}
사용 시기 (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#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
steps | List<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: (_) {},
)
onStepChanged가 null이 아니면 모든 인디케이터가 Clickable(포커스 링·호버·커서 포함)로 감싸집니다. 진짜 읽기 전용 진행 표시가 필요하면 onStepChanged를 아예 생략하거나 Steps 컴포넌트를 사용하세요.
접근성 (Accessibility)#
키보드#
| 키 | 동작 |
|---|---|
Tab | 다음 단계로 포커스 이동 |
Enter / Space | 포커스된 단계로 이동 |
스크린 리더#
- 각 단계는 순서 + 라벨 + 설명 읽음
- 완료/현재/대기 상태 시각 표시 기반
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | Stepper | Stepper |
| 렌더링 | Column/Row + GestureDetector |
<div> + 인라인 스타일 |
| 아이콘 | Icon(LucideIcons.check) |
Icon(LucideIcons.check) |
| 테마 | CoreStepperTheme | CoreStepperTheme |