Indicator
다른 요소의 모서리/가장자리에 배지/아이템을 띄우는 wrapper 컴포넌트
목차
Indicator 는 child 위젯/컴포넌트를 감싸고 그 모서리·가장자리에 item 을 띄우는 wrapper 입니다. 아바타 우상단의 온라인 점, 아이콘 우상단의 카운트 배지, 카드 좌상단의 "NEW" 라벨 같은 패턴에 적합합니다.
기본 사용법 (Basic Usage)#
// 기본 — 우상단 (end + top)
Indicator(
item: Badge(
tone: CoreBadgeTone.destructive,
variant: CoreBadgeVariant.filled,
form: CoreBadgeForm.count,
size: CoreComponentSize.xs,
child: Text('99+'),
),
child: Avatar(initials: 'DW'),
)
// 좌하단 (start + bottom) + offset 4 (Flutter 4 logical px)
Indicator(
horizontalPosition: CoreIndicatorHorizontalPosition.start,
verticalPosition: CoreIndicatorVerticalPosition.bottom,
indicatorStyle: CoreIndicatorStyle(offset: 4),
item: Status(),
child: Icon(LucideIcons.bell),
)
// 기본 — 우상단
Indicator(
item: Badge(
tone: CoreBadgeTone.destructive,
variant: CoreBadgeVariant.filled,
form: CoreBadgeForm.count,
size: CoreComponentSize.xs,
child: Text('99+'),
),
child: Avatar(initials: 'DW'),
)
// 좌하단 + offset 4 (Web inline `0.25rem`)
Indicator(
horizontalPosition: CoreIndicatorHorizontalPosition.start,
verticalPosition: CoreIndicatorVerticalPosition.bottom,
indicatorStyle: CoreIndicatorStyle(offset: 4),
item: Status(),
child: Icon(LucideIcons.bell),
)
Live Preview#
코드 보기
class IndicatorDefaultExample extends StatelessComponent {
const IndicatorDefaultExample({super.key});
@override
Component build(BuildContext context) {
return Indicator(
item: Status(
color: CoreStatusColor.success,
size: CoreStatusSize.lg,
),
child: Avatar(initials: 'DW'),
);
}
}
class IndicatorDefaultExample extends StatelessWidget {
const IndicatorDefaultExample({super.key});
@override
Widget build(BuildContext context) {
return const Indicator(
item: Status(
color: CoreStatusColor.success,
size: CoreStatusSize.lg,
),
child: Avatar(initials: 'DW'),
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 아바타에 온라인 상태 점 (status dot) 을 표시할 때
- 아이콘 위에 알림 카운트 배지를 띄울 때
- 카드/이미지 위에 "NEW" / "HOT" 라벨을 오버레이할 때
대신 다른 컴포넌트를 사용하세요:
- Badge: 다른 요소에 얹지 않고 인라인 상태·개수만 표시할 때
- DotIndicator: 코너의 항목 하나 대신 페이지 위치 목록·탐색이 필요할 때
Props / Parameters#
W 는 Flutter의 Widget, Web의 Jaspr Component 입니다.
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
child | W | 필수 | 메인 콘텐츠 |
item |
W |
필수 | corner 에 띄울 item (badge / status dot 등) |
horizontalPosition |
CoreIndicatorHorizontalPosition |
end |
start / center / end |
verticalPosition |
CoreIndicatorVerticalPosition |
top |
top / middle / bottom |
indicatorStyle |
CoreIndicatorStyle? |
null |
오프셋 등 chrome override (
offset: 4
→ 가장자리/코너로부터 Flutter 4 logical px / Web
0.25rem
)
|
빠른 오버라이드 (Chain)#
이미 만든 Indicator 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
class IndicatorChainExample extends StatelessWidget {
const IndicatorChainExample({super.key});
@override
Widget build(BuildContext context) {
return Indicator(
item: const Status(
color: CoreStatusColor.success,
size: CoreStatusSize.lg,
),
child: const Avatar(initials: 'DW'),
).withStyle(
const CoreIndicatorStyle(offset: CoreSpace.space8),
);
}
}
class IndicatorChainExample extends StatelessComponent {
const IndicatorChainExample({super.key});
@override
Component build(BuildContext context) {
return Indicator(
item: Status(
color: CoreStatusColor.success,
size: CoreStatusSize.lg,
),
child: Avatar(initials: 'DW'),
).withStyle(
const CoreIndicatorStyle(offset: CoreSpace.space8),
);
}
}
스타일 시스템 (Style System)#
CoreIndicatorStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
offset |
double? |
Pixel offset override from the chosen edge / corner. Measured from the item's own edge (not the ring's). |
ringWidth |
double? |
Thickness override of the ring around the item (logical px). 0 draws no ring. |
ringColor |
CoreColor? |
Colour override of the ring around the item. |
ringBorderRadius |
CoreBorderRadius? |
Corner radius override of the ring. null → [defaultRingBorderRadius]. |
변형 (Variants)#
Default#
우상단 (end + top) 에 item 을 배치 — 가장 흔한 알림 배지 패턴.
Indicator(
item: Badge(
tone: CoreBadgeTone.destructive,
variant: CoreBadgeVariant.filled,
form: CoreBadgeForm.count,
size: CoreComponentSize.xs,
child: Text('99+'),
),
child: Avatar(initials: 'DW'),
)
Bottom Start#
좌하단 (start + bottom) — "NEW" 라벨 같은 라벨 배지에 적합.
Indicator(
horizontalPosition: CoreIndicatorHorizontalPosition.start,
verticalPosition: CoreIndicatorVerticalPosition.bottom,
item: Badge(child: Text('NEW')),
child: Avatar(initials: 'DW'),
)
위치 조합 (Position Matrix)#
| start | center | end | |
|---|---|---|---|
| top | 좌상단 | 상단 가운데 | 우상단 (default) |
| middle | 좌측 가운데 | 정중앙 | 우측 가운데 |
| bottom | 좌하단 | 하단 가운데 | 우하단 |
center / middle 일 땐 transform: translate(-50%, -50%) (Web) 또는
Align(alignment: Alignment(h, v)) (Flutter) 으로 anchor 를 자식의 중심에 맞춥니다.
동작 스펙 (Behavior)#
- 표시 전용 wrapper. 인터랙션 없음.
-
Flutter:
Stacks(clipBehavior: .none)+Position. item 이 child 의 bounding box 를 넘어가도 잘리지 않음. -
Web:
position: relative; overflow: visible컨테이너 +position: absoluteoverlay div. 동일 시맨틱. -
둘레(ring): item 바깥에
ringWidth(기본 2,CoreStrokeWidth.stroke2) 두께의ringColor(기본surface) 알약 둘레가 그려져 item 이 겹친 child 에서 오려 낸 것처럼 읽힙니다. 둘레는 item 바깥으로 나갑니다 — item 의 모서리가 child 의 모서리와 만나고(.end+.top이면 item 의 오른쪽 위 모서리 = child 의 오른쪽 위 모서리) 둘레만큼 바깥으로 튀어나옵니다. 그래서 child 의 크기와 상관없이 item 은 항상 그 모서리에 붙고,offset은 그 기준에서 안쪽으로 겹치는 정도만 정합니다. 둘레의 모서리는ringBorderRadius(기본CoreRadius.radius9999, 알약)이고 두 플랫폼이 같은 필드를 읽습니다.ringWidth: 0이면 둘레가 없습니다. Flutter 는 item 을ringWidth만큼 패딩한 알약으로 감싸고 anchor 를 같은 만큼 바깥으로 당기며, Web 은 overlay 에box-shadow: 0 0 0 <ringWidth> <ringColor>를 얹습니다. -
indicatorStyle.offset은 양수일수록 item 이 child 안쪽으로 들어옴 — Flutter 는Position의top/right/bottom/left에 logical pixel 로, Web 은 같은 네 속성의 inline CSS 에offset / 16을rem으로 넣습니다 (offset: 4→0.25rem). 미지정 시CoreIndicatorTheme.style?.offset→CoreIndicatorStyle.defaultOffset(0) 순으로 폴백.
사용 가이드라인 (Usage Guidelines)#
✅ Do#
item 자체에 의미를 실어라
Indicator(
item: Badge(child: Text('3 unread')),
child: Icon(LucideIcons.bell),
)
이유: Indicator 자체는 표시 전용 wrapper 라 시맨틱 의미가 없습니다. item 위젯 자체가 (Badge(child: Text('3 unread'))처럼) 실제 의미를 담아야 스크린 리더가 그 내용을 읽을 수 있습니다.
❌ Don't#
여러 개를 나열하는 용도로 사용 금지
// ❌ Indicator 는 하나의 child 에 하나의 item 을 배치하는 wrapper
Row(
children: List.generate(
5,
(i) => Indicator(item: const Status(), child: const SizedBox()),
),
)
이유: Indicator 는 child 하나를 감싸고 그 모서리에 item 하나를 띄우는 용도입니다. 캐러셀 위치처럼 여러 점을 나열해야 한다면 DotIndicator 를 사용해야 합니다.
접근성 (Accessibility)#
역할#
양 플랫폼 모두 자체 role 이나 semantics 를 전혀 내보내지 않습니다. Flutter 는 Stacks(clipBehavior: .none)
안에 child 와 Position(item) 을 나열할 뿐 Semantics 호출이 없고 (indicator.dart:91-108), Web 은
configureAttributes 를 override 하지 않아 바깥 <div> 에도 overlay <div>
에도 role / aria-* 가 하나도 붙지 않습니다 (indicator.dart:82-100).
즉 Indicator 는 순수 배치 wrapper 이고, 의미는 전적으로 item 과 child 자신이 들고 있어야 합니다
— item: Badge(child: Text('3 unread')) 처럼 읽을 텍스트가 item 안에 있어야 스크린 리더가 그 뱃지를 안내할 수 있습니다.
키보드#
처리하는 키가 없습니다. Flutter 생성자는 콜백 파라미터를 하나도 받지 않고 (indicator.dart:25-32), Web 은 eventHandlers
를 받긴 하지만 (indicator.dart:26) 루트에 tabindex 를 내보내지 않아 래퍼 자신에게는 포커스가 오지 않습니다 — 감싼
child 가 포커스 가능할 때 그 자식에서 버블링된 키 이벤트만 도달합니다.
스크린 리더#
래퍼 자신에게서 나오는 이름은 없고, child 와 item 이 각자의 semantics 를 그대로 통과시킵니다.
읽기 순서는 child → item 입니다. 두 플랫폼 모두 child 를 먼저, 오버레이를 나중에 배치하기 때문입니다 (Flutter 는
Stacks 의 children 순서 indicator.dart:93-106, Web 은 DOM 순서 indicator.dart:91-99). 시각적으로는 뱃지가 콘텐츠
위에 겹쳐 보이지만, 안내는 "종 아이콘 → 3 unread" 순으로 콘텐츠 뒤에 옵니다.
상태 통보는 없습니다. item 이 바뀌어도 (뱃지 숫자가 늘어나도) 어느 쪽에도 live region 배선이 없어 조용히 교체됩니다.
포커스 관리#
포커스 대상이 아닙니다. Flutter 에 FocusNode / Focus 가 없고 Web 에 tabindex 가 없어 어느 플랫폼에서도 traversal 정지점이 되지 않습니다. 오버레이 컴포넌트가 아니므로 진입·이탈·트랩 모두 해당 없습니다.
알려진 제약#
-
오버레이를 장식으로 표시할 수단이 컴포넌트 쪽에 없습니다.
CoreIndicatorContract는child/item/ 두 position /indicatorStyle만 노출하고 (indicator_contract.dart:16-33), Web overlay<div>에도aria-hidden이 붙지 않습니다 (indicator.dart:93-98). 순수 장식 점(Status등)을 접근성 트리에서 빼려면item쪽에서 처리해야 합니다 — Flutter 는ExcludeSemantics로, Web 은aria-hidden을 실은 요소로item을 감싸세요. -
item값 변화가 통보되지 않습니다. 읽지 않은 개수가 0 → 3 으로 바뀌어도 스크린 리더는 아무 말도 하지 않습니다. 변화를 알려야 한다면 통보 책임을 지는 컴포넌트(Status/Toast등)를item자리에 두거나, 화면 쪽에서 별도 live region 을 마련해야 합니다.
모션 감소 · 고대비 · 최소 터치 타깃처럼 모든 컴포넌트에 공통으로 적용되는 축은 전역 접근성 축 에 정리되어 있습니다.
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 기반 | Stacks(clipBehavior: .none) + Position |
<div>
position: relative
+
<div>
position: absolute
|
| Center / Middle 정렬 | Align(alignment: Alignment(h, v)) |
transform: translate(-50%, -50%) |
| Offset 단위 | logical pixels | inline rem (offset / 16 — Tailwind 클래스 아님). 루트 font-size 에 따라 스케일 |
관련 컴포넌트 (Related Components)#
-
Badge:
Indicator.item으로 가장 흔히 쓰이는 컴포넌트. -
Avatar:
Indicator.child로 가장 흔히 쓰이는 컴포넌트. - DotIndicator: 캐러셀 위치 점들.