DotIndicator#
캐러셀, 온보딩 화면 등에서 현재 페이지 위치를 점으로 표시하는 인디케이터 컴포넌트입니다.
Live Preview#
class DotIndicatorDefaultExample extends StatefulComponent {
const DotIndicatorDefaultExample({super.key});
@override
State<DotIndicatorDefaultExample> createState() =>
_DotIndicatorDefaultExampleState();
}
class _DotIndicatorDefaultExampleState
extends State<DotIndicatorDefaultExample> {
int _index = 2;
@override
Component build(BuildContext context) {
return DotIndicator(
index: _index,
length: 5,
onChanged: (v) => setState(() => _index = v),
);
}
}
class DotIndicatorDefaultExample extends StatefulWidget {
const DotIndicatorDefaultExample({super.key});
@override
State<DotIndicatorDefaultExample> createState() =>
_DotIndicatorDefaultExampleState();
}
class _DotIndicatorDefaultExampleState
extends State<DotIndicatorDefaultExample> {
int _index = 2;
@override
Widget build(BuildContext context) {
return DotIndicator(
index: _index,
length: 5,
onChanged: (v) => setState(() => _index = v),
);
}
}
class DotIndicatorChainExample extends StatefulComponent {
const DotIndicatorChainExample({super.key});
@override
State<DotIndicatorChainExample> createState() => _DotIndicatorChainExampleState();
}
class _DotIndicatorChainExampleState extends State<DotIndicatorChainExample> {
int _index = 2;
@override
Component build(BuildContext context) {
return DotIndicator(
index: _index,
length: 5,
onChanged: (v) => setState(() => _index = v),
)
.withStyle(
const CoreDotIndicatorStyle(
activeColor: CoreColor.token(CoreColors.tertiary),
dotSize: CoreSpace.space16,
spacing: CoreSpace.space12,
),
)
.radius16;
}
}
class DotIndicatorChainExample extends StatefulWidget {
const DotIndicatorChainExample({super.key});
@override
State<DotIndicatorChainExample> createState() => _DotIndicatorChainExampleState();
}
class _DotIndicatorChainExampleState extends State<DotIndicatorChainExample> {
int _index = 2;
@override
Widget build(BuildContext context) {
return DotIndicator(
index: _index,
length: 5,
onChanged: (v) => setState(() => _index = v),
)
.withStyle(
const CoreDotIndicatorStyle(
activeColor: CoreColor.token(CoreColors.tertiary),
dotSize: CoreSpace.space16,
spacing: CoreSpace.space12,
),
)
.radius16;
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 캐러셀이나
PageView에서 현재 페이지 위치를 표시할 때 - 온보딩 화면에서 전체 단계 중 현재 위치를 안내할 때
- 슬라이드나 갤러리에서 탐색 위치 표시와 함께 클릭으로 직접 이동을 제공할 때
대신 다른 컴포넌트를 사용하세요:
Steps: 완료/진행 중/미완료 상태가 있는 단계별 진행 상황을 표시할 때Progress: 진행률을 퍼센트로 표시할 때Pagination: 페이지 번호 목록 탐색이 필요할 때
기본 사용법 (Basic Usage)#
// 기본 인디케이터
DotIndicator(
index: currentPage,
length: 5,
)
// 색상 지정
DotIndicator(
index: currentPage,
length: 3,
dotIndicatorStyle: CoreDotIndicatorStyle(
activeColor: CoreColor.token(CoreColors.primary),
inactiveColor: CoreColor.token(CoreColors.surfaceContainerHigh),
),
)
// 크기 지정 — sm / md(기본) / lg 가 점 지름과 간격을 함께 정합니다
DotIndicator(
index: currentPage,
length: 4,
size: CoreDotIndicatorSize.lg,
)
// PageView와 연동 (점 클릭으로 이동)
PageView(
onPageChanged: (index) => setState(() => currentPage = index),
children: pages,
),
DotIndicator(
index: currentPage,
length: pages.length,
onChanged: (newIndex) => unawaited(pageController.animateToPage(
newIndex,
duration: const Duration(milliseconds: CoreDuration.moderate),
curve: Curves.easeInOut,
)),
)
// 기본 인디케이터
DotIndicator(
index: currentPage,
length: 5,
)
// 색상 지정
DotIndicator(
index: currentPage,
length: 3,
dotIndicatorStyle: CoreDotIndicatorStyle(
activeColor: CoreColor.token(CoreColors.primary),
inactiveColor: CoreColor.token(CoreColors.surfaceContainerHigh),
),
)
// 크기 지정 — sm / md(기본) / lg 가 점 지름과 간격을 함께 정합니다
DotIndicator(
index: currentPage,
length: 4,
size: CoreDotIndicatorSize.lg,
)
// 세로 방향 + 페이지 연동 (onChanged 콜백)
DotIndicator(
index: currentPage,
length: pages.length,
direction: CoreAxis.vertical,
onChanged: handlePageChange,
)
Props / Parameters#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
index | int | 필수 | 현재 활성 점 인덱스 (0-based) |
length | int | 필수 | 총 점 개수 |
onChanged |
CoreDotIndicatorOnChanged? |
null |
점 클릭 시 페이지 이동 핸들러 |
direction |
CoreAxis |
CoreAxis.horizontal |
레이아웃 방향 (Flutter / Web 동일) |
size |
CoreDotIndicatorSize |
CoreDotIndicatorSize.md |
점 지름 + 점 간격 (
sm
6/6 ·
md
8/8 ·
lg
12/12 px). widget-only 시맨틱 식별자 — 테마가 override 하지 않음
|
dotIndicatorStyle |
CoreDotIndicatorStyle? |
null |
chrome / 치수 단일 진입점 (아래 표 참고) |
스타일 시스템 (Style System)#
모든 chrome / 치수 override 는 dotIndicatorStyle 슬롯 하나로 흐릅니다.
CoreDotIndicatorStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
spacing |
double? |
Spacing between individual dots (logical px).
null
defers to [defaultsBySize] for the active
CoreDotIndicatorSize
.
|
padding |
CoreEdgeInsets? |
Uniform padding around the dot indicator container. |
dotSize |
double? |
Dot size override (logical px).
null
defers to [defaultsBySize] for the active
CoreDotIndicatorSize
.
|
borderRadius |
CoreBorderRadius? |
Dot border radius override. |
activeColor |
CoreColor? |
Active dot colour override. |
inactiveColor |
CoreColor? |
Inactive dot fill override. Defaults to [defaultInactiveColor] — a solid
outlineVariant
disc.
CoreColor.transparent
restores a ring (fill absent, [inactiveBorderColor] alone).
|
inactiveBorderColor |
CoreColor? |
Inactive dot border colour override. |
inactiveBorderWidth |
double? |
Inactive dot border width override (logical px). |
hoverBorderColor |
CoreColor? |
Hover border colour override for clickable inactive dots.
null
defers to [defaultHoverBorderColor]. Only reaches the screen when a border width is stated — see [defaultInactiveBorderWidth].
|
transitionDuration |
Duration? |
Transition duration override for active / hover state changes.
null
defers to [defaultTransitionDuration].
|
focusOutlineStyle |
CoreFocusOutlineStyle? |
Focus ring chrome drawn around the selected dot while the indicator group holds keyboard focus (roving radio-group model — the group is one tab stop and arrow keys move the selection). Raw-forwarded so the focus-outline layer fills its own defaults. |
크기별 기본값 (CoreDotIndicatorStyle.defaultsBySize)#
size 가 고르는 rung 입니다. dotSize / spacing 을 슬롯으로 주면 그 값이 rung 을 덮습니다.
size | dotSize | spacing |
|---|---|---|
sm |
CoreSpace.space6 (6px) |
CoreSpace.space6 (6px) |
md (기본) |
CoreSpace.space8 (8px) |
CoreSpace.space8 (8px) |
lg |
CoreSpace.space12 (12px) |
CoreSpace.space12 (12px) |
Resolve chain#
CoreDotIndicatorStyle.defaultX / defaultsBySize[size] (static const, 단일 출처)
→ CoreDotIndicatorTheme.style // 프로젝트 공통
→ widget.dotIndicatorStyle // 인스턴스별
빠른 오버라이드 (Chain)#
이미 만든 DotIndicator 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
.radius4처럼 Core 토큰 상수 이름과 똑같은 이름의 getter도 있습니다 — withStyle을 한 번 더 줄인 sugar로, 이름이 곧 값이라(radius4 ==
CoreRadius.radius4) 어느 컴포넌트에서 써도 뜻이 갈리지 않습니다.
class DotIndicatorChainExample extends StatefulWidget {
const DotIndicatorChainExample({super.key});
@override
State<DotIndicatorChainExample> createState() => _DotIndicatorChainExampleState();
}
class _DotIndicatorChainExampleState extends State<DotIndicatorChainExample> {
int _index = 2;
@override
Widget build(BuildContext context) {
return DotIndicator(
index: _index,
length: 5,
onChanged: (v) => setState(() => _index = v),
)
.withStyle(
const CoreDotIndicatorStyle(
activeColor: CoreColor.token(CoreColors.tertiary),
dotSize: CoreSpace.space16,
spacing: CoreSpace.space12,
),
)
.radius16;
}
}
class DotIndicatorChainExample extends StatefulComponent {
const DotIndicatorChainExample({super.key});
@override
State<DotIndicatorChainExample> createState() => _DotIndicatorChainExampleState();
}
class _DotIndicatorChainExampleState extends State<DotIndicatorChainExample> {
int _index = 2;
@override
Component build(BuildContext context) {
return DotIndicator(
index: _index,
length: 5,
onChanged: (v) => setState(() => _index = v),
)
.withStyle(
const CoreDotIndicatorStyle(
activeColor: CoreColor.token(CoreColors.tertiary),
dotSize: CoreSpace.space16,
spacing: CoreSpace.space12,
),
)
.radius16;
}
}
변형 (Variants)#
기본형 (가로)#
동일한 크기의 점으로 활성/비활성을 색상으로 구분합니다.
DotIndicator(index: 0, length: 4)
세로형#
direction 으로 축을 바꿉니다 — 화살표 키도 ↑/↓ 로 따라갑니다.
DotIndicator(
index: 0,
length: 4,
direction: CoreAxis.vertical,
)
클릭 가능형#
onChanged 를 주면 점이 클릭 대상이 되고 키보드 조작이 켜집니다.
DotIndicator(
index: currentPage,
length: 4,
onChanged: (i) => setState(() => currentPage = i),
)
동작 스펙 (Behavior)#
인터랙션#
- 점 클릭:
onChanged가 있을 때만 활성화되며 클릭한 인덱스를 콜백으로 전달 -
hover: 클릭 가능한 비활성 점이 살짝 확대됩니다. 기본 점은 테두리 폭이 0 이므로
hoverBorderColor는 폭을 직접 진술한 호출자에게만 닿습니다 index변경: 외부에서 상태를 바꾸면 활성 점이 갱신됩니다 (컴포넌트는 자체 상태를 갖지 않음)
애니메이션#
- 색상 / hover scale 전환:
dotIndicatorStyle.transitionDuration(기본 200ms) -
Flutter:
AnimatedContainer+AnimatedScale, Web: CSS transition
사용 가이드라인 (Usage Guidelines)#
✅ Do#
PageView와 함께 연동
Column(
children: [
Expanded(
child: PageView(
onPageChanged: (index) => setState(() => _currentPage = index),
children: onboardingPages,
),
),
Padding(
padding: const EdgeInsets.only(bottom: CoreSpace.space24),
child: DotIndicator(
index: _currentPage,
length: onboardingPages.length,
onChanged: (index) => unawaited(pageController.animateToPage(
index,
duration: const Duration(milliseconds: CoreDuration.moderate),
curve: Curves.easeInOut,
)),
),
),
],
)
PageView와 연동하여 현재 페이지를 실시간으로 반영하고 점 클릭으로 직접 이동할 수 있게 합니다.
❌ Don't#
많은 점을 나열하지 않기
// ❌ 너무 많은 점
DotIndicator(
index: currentPage,
length: 15, // 너무 많음
)
점이 너무 많으면 현재 위치 파악이 어렵고 터치 타겟이 작아집니다. 5개 이하를 권장하며, 그 이상은 Pagination 사용을 고려하세요.
✅ Do#
활성 상태를 색상만으로 구분하지 않기
DotIndicator(
index: currentPage,
length: 4,
dotIndicatorStyle: CoreDotIndicatorStyle(
activeColor: CoreColor.token(CoreColors.onSurface),
// 비활성을 채움 없이 테두리만으로 — 색 강제 모드는 채움도 테두리도
// 시스템 색으로 접지만 형태는 그대로 두므로, 원과 링의 차이는 남습니다.
// 기본은 둘 다 채워진 원이라 그 모드에서 활성/비활성이 같아 보입니다
inactiveColor: CoreColor.transparent,
inactiveBorderColor: CoreColor.token(CoreColors.outlineVariant),
inactiveBorderWidth: CoreStrokeWidth.stroke2,
),
)
채움 유무까지 다르면 색을 구별하기 어려운 사용자도 활성 상태를 인식할 수 있습니다.
❌ Don't#
클릭 가능한 점을 너무 작게 두지 않기
// ❌ 클릭 가능한데 점이 4px
DotIndicator(
index: currentPage,
length: 4,
onChanged: handleDotTap,
dotIndicatorStyle: CoreDotIndicatorStyle(
dotSize: CoreSpace.space4, // 너무 작음
padding: CoreEdgeInsets.zero, // 히트 영역까지 함께 줄어듦
),
)
onChanged 가 있으면 각 점이 탭 타겟입니다. 히트 영역은 dotSize 와 padding /
spacing 이 함께 만들므로, 기본값(md: 8px 점 + 8px 간격 + 8px 여백)보다 줄일 때는
최소 터치 타겟(→ 전역 접근성 축)을 확인하세요.
접근성 (Accessibility)#
키보드·시맨틱은 onChanged 가 있을 때만 켜집니다. onChanged 가 없으면 점들은
장식이고 포커스도 받지 않습니다.
키보드 인터랙션#
인디케이터 전체가 하나의 탭 정지이고, 화살표 키가 선택과 포커스 링을 함께 옮깁니다 (라디오 그룹 모델 — 개별 점은 탭 순서에서 빠집니다).
| 키 | 동작 |
|---|---|
→ / ← (가로) | 다음/이전 점 선택 (onChanged 호출) |
↓ / ↑ (세로) | 다음/이전 점 선택 |
Tab | 인디케이터 밖 다음 요소로 이동 |
양 끝에서는 clamp 되어 순환하지 않습니다. Home / End 는 없습니다.
스크린 리더#
-
Web: 컨테이너에
role="tablist"+aria-label, 각 점에role="tab"+aria-label(항목 번호), 활성 점에aria-current="true"를 emit 합니다. 라벨은CouiLocalizations에서 옵니다. -
Flutter: 컨테이너를
Semantics(container: true, label: ...dotIndicatorLabel)로 감싸고, 각 점을Semantics(label: ...dotIndicatorGoToItem(i + 1), button: interactive, selected: i == index)로 감쌉니다 — Web 의 tablist/tab 구조와 같은 두 로컬라이제이션 멤버를 같은 값으로 읽으므로, 스크린 리더가 전달하는 이름·선택 상태는 양 플랫폼이 동일합니다.
포커스#
포커스 링은 선택된 점 주위에만 원형으로 그려지며(focusOutlineStyle 로 조정),
키보드 포커스일 때만 나타납니다.
알려진 제약#
- 점이 나타내는 대상(어떤 슬라이드인지)은 번호로만 전달됩니다.
전역으로 적용되는 축(동작 줄이기·고대비·색 강제 모드·최소 터치 타겟)은 전역 접근성 축에 있습니다.
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | DotIndicator | DotIndicator |
| 애니메이션 | AnimatedContainer + AnimatedScale |
CSS transition |
| 키보드 | FocusableActionDetector + 화살표 shortcut |
Clickable + 화살표 키 핸들러 |
| 시맨틱 | 없음 | role="tablist" / role="tab" / aria-current |
관련 컴포넌트 (Related Components)#
- Carousel: 내장 인디케이터가 있는 캐러셀 컴포넌트 (DotIndicator 통합)
- Steps: 단계 완료 상태가 있는 진행 표시가 필요할 때
- Progress: 퍼센트 기반 진행률 표시에 사용
조합 예제#
// DotIndicator + Carousel 외부 연동
Stacks(
alignment: CoreAlignment.bottomCenter,
children: [
Carousel(
items: items,
transition: const SlidingCarouselTransition(),
showIndicators: false,
currentIndex: currentPage,
onIndexChanged: (index) => setState(() => currentPage = index),
),
Position(
bottom: CoreSpace.space16,
child: DotIndicator(
index: currentPage,
length: items.length,
onChanged: (index) => setState(() => currentPage = index),
),
),
],
)