Tooltip
툴팁 컴포넌트
목차
요소에 호버하거나 포커스할 때 설명 텍스트를 표시하는 툴팁 컴포넌트입니다.
기본 사용법 (Basic Usage)#
// 기본 툴팁
Tooltip(
message: '이 버튼을 클릭하면 저장됩니다',
child: Button(
variant: CoreButtonVariant.primary,
onPressed: handleSave,
child: Icon(LucideIcons.save),
),
)
// 위치 지정
Tooltip(
message: '설정 메뉴',
position: CoreTooltipPosition.right,
child: Icon(LucideIcons.settings),
)
// 리치 콘텐츠 툴팁
Tooltip(
message: '단축키: Ctrl + S',
tooltipChild: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('단축키').labelMedium.semiBold,
Text('Ctrl + S').bodySmall,
],
),
child: Icon(LucideIcons.keyboard),
)
// 기본 툴팁 (호버 시 텍스트 표시)
Tooltip(
message: '이 버튼을 클릭하면 저장됩니다',
child: Button(
variant: CoreButtonVariant.primary,
onPressed: handleSave,
child: Text('저장'),
),
)
// 위치 제어
Tooltip(
message: '설정 메뉴',
position: CoreTooltipPosition.bottom,
child: Icon(LucideIcons.settings),
)
// 단축키 정보 포함
Tooltip(
message: '실행 취소 (Ctrl+Z)',
child: Button(
variant: CoreButtonVariant.ghost,
onPressed: handleUndo,
child: Text('실행 취소'),
),
)
Live Preview#
코드 보기
class TooltipDefaultExample extends StatefulComponent {
const TooltipDefaultExample({super.key});
@override
State<TooltipDefaultExample> createState() => _TooltipDefaultExampleState();
}
class _TooltipDefaultExampleState extends State<TooltipDefaultExample> {
@override
Component build(BuildContext context) {
return Tooltip(
message: 'Helpful tip',
position: CoreTooltipPosition.bottom,
child: Button(
variant: CoreButtonVariant.outline,
onPressed: () {},
child: Text('Hover me'),
),
);
}
}
class TooltipDefaultExample extends StatefulWidget {
const TooltipDefaultExample({super.key});
@override
State<TooltipDefaultExample> createState() => _TooltipDefaultExampleState();
}
class _TooltipDefaultExampleState extends State<TooltipDefaultExample> {
@override
Widget build(BuildContext context) {
return Tooltip(
message: 'Helpful tip',
position: CoreTooltipPosition.bottom,
child: Button(
variant: CoreButtonVariant.outline,
onPressed: () {},
child: const Text('Hover me'),
),
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 아이콘 버튼이나 축약된 UI에 추가 설명이 필요할 때
- 키보드 단축키 정보를 호버로 표시할 때
- 짧은 텍스트 힌트(1~2줄)로 충분할 때
대신 다른 컴포넌트를 사용하세요:
HoverCard: 이미지, 버튼 등 풍부한 콘텐츠가 필요할 때Popover: 클릭으로 열리는 인터랙티브 콘텐츠일 때Toast: 액션 결과 피드백을 표시할 때
Props / Parameters#
W는 Flutter의 Widget, Web의 Component를 뜻하는 합성 콘텐츠 타입입니다. wait/show/minDuration은 Duration이며 표의 ms는 밀리초입니다.
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
message | String | '' | 툴팁 텍스트 메시지 |
tooltipChild |
W? |
null |
커스텀 툴팁 콘텐츠 (message 대체) |
child | W | 필수 | 툴팁을 표시할 대상 콘텐츠 |
position |
CoreTooltipPosition |
CoreTooltipPosition.top |
툴팁 위치 (시맨틱 — 위젯 파라미터) |
waitDuration |
Duration? |
CoreTooltipContract.defaultWaitDuration (150ms) |
커서가 트리거에 머물러야 하는 시간 — 이만큼 지나야 열림 |
showDuration |
Duration? |
CoreTooltipContract.defaultShowDuration (150ms) |
커서가 떠난 뒤 닫히기 시작할 때까지 머무는 시간 |
minDuration |
Duration? |
null → CoreTooltipContract.defaultMinDuration (0ms) |
최소 표시 유지 시간 — 커서 도착 시점부터 이만큼 지나기 전에 떠나면 남은 시간만큼 유지 |
tooltipStyle |
CoreTooltipStyle? |
null |
panel chrome / nested labelStyle 묶음 |
빠른 오버라이드 (Chain)#
이미 만든 Tooltip 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
class TooltipChainExample extends StatelessWidget {
const TooltipChainExample({super.key});
@override
Widget build(BuildContext context) {
return Tooltip(
message: 'Helpful tip',
position: CoreTooltipPosition.bottom,
child: Button(
variant: CoreButtonVariant.outline,
onPressed: () {},
child: const Text('Hover me'),
),
).withStyle(
const CoreTooltipStyle(
panelBackgroundColor: CoreColor.token(CoreColors.primary),
panelBorderRadius: CoreBorderRadius.all(CoreRadius.radius4),
panelPadding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space16,
vertical: CoreSpace.space8,
),
labelStyle: CoreTextStyle.token(
CoreTextStyles.bodySmall,
color: CoreColor.token(CoreColors.onPrimary),
),
),
);
}
}
class TooltipChainExample extends StatelessComponent {
const TooltipChainExample({super.key});
@override
Component build(BuildContext context) {
return Tooltip(
message: 'Helpful tip',
position: CoreTooltipPosition.bottom,
child: Button(
variant: CoreButtonVariant.outline,
onPressed: () {},
child: Text('Hover me'),
),
).withStyle(
const CoreTooltipStyle(
panelBackgroundColor: CoreColor.token(CoreColors.primary),
panelBorderRadius: CoreBorderRadius.all(CoreRadius.radius4),
panelPadding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space16,
vertical: CoreSpace.space8,
),
labelStyle: CoreTextStyle.token(
CoreTextStyles.bodySmall,
color: CoreColor.token(CoreColors.onPrimary),
),
),
);
}
}
스타일 시스템 (Style System)#
tooltipStyle 슬롯#
Tooltip 의 panel chrome / 슬롯 미세 조정은 단일 tooltipStyle
(CoreTooltipStyle) 으로 흐릅니다. 시맨틱 enum (position) 은 위젯
파라미터.
Tooltip(
message: '도움말',
position: CoreTooltipPosition.top,
child: Icon(LucideIcons.info),
tooltipStyle: CoreTooltipStyle(
panelBackgroundColor: CoreColor.token(CoreColors.inverseSurface),
panelBorderRadius: CoreBorderRadius.all(CoreRadius.radius6),
panelPadding: CoreEdgeInsets.all(CoreSpace.space8),
surfaceBlur: 0,
surfaceOpacity: 0.95,
labelStyle: CoreTextStyle.token(
CoreTextStyles.bodySmall,
color: CoreColor.token(CoreColors.inverseOnSurface),
),
),
)
긴 안내문 줄바꿈#
툴팁은 기본적으로 한 줄로 늘어난다. 일정 폭에서 줄바꿈하려면 panelMaxWidth 를 준다 —
이때 tooltipChild 로 폭을 잡을 필요가 없다.
Tooltip(
message: '배너 이미지는 가로 1200px 이상을 권장합니다. '
'더 작은 이미지는 확대되어 흐리게 보일 수 있습니다.',
tooltipStyle: CoreTooltipStyle(panelMaxWidth: CoreSpace.space256),
child: Icon(LucideIcons.info),
)
tooltipChild로 커스텀 콘텐츠를 넣을 때도 라벨 스타일(타이포그래피 role + 전경색)이 기본값으로 상속된다. 자식이 스스로 지정한 색·크기는 그대로 이긴다.
기본 라벨은
labelSmall역할 그대로(12 Medium)이고 굵기를 덧쓰지 않는다. 판은 반전 채움 표면이라 어떤 서페이스 스타일에서도 테두리를 그리지 않는다 — 스타일이 박스에 주는 컨트롤 테두리도 받지 않는다.
꼬리#
툴팁은 기본으로 꼬리를 그리지 않는다 (tailWidth · tailHeight 기본값 0). 트리거를
가리키는 삼각형이 필요하면 두 축을 함께 준다. 어느 한 축이라도 0 이면 꼬리는 없다.
Tooltip(
message: '저장',
tooltipStyle: CoreTooltipStyle(
tailWidth: CoreSpace.space12,
tailHeight: CoreSpace.space6,
),
child: Icon(LucideIcons.save),
)
CoreTooltipStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
panelBackgroundColor |
CoreColor? |
Panel background fill colour override. |
panelBorderRadius |
CoreBorderRadius? |
Panel border radius override. |
panelMaxWidth |
double? |
Maximum panel width (logical px) override — the ceiling the bubble grows to before its content wraps onto another line. Setting it makes the bubble wrap on both platforms: Flutter constrains the bubble, Web drops the single-line
white-space: nowrap
it otherwise carries.
null
leaves the panel unconstrained, and has no paired
default*
on purpose — a shared ceiling would silently re-wrap every existing tooltip.
Deliberately has no default* — absence is the design.
PRE- EXISTING exemption, already written before this task — left untouched. Web adds
whitespace-nowrap
only
if (panelMaxWidth == null)
, so a shared ceiling would drop nowrap, add an inline max- width, and silently re-wrap every existing tooltip; Flutter's ConstrainedBox is likewise mounted only when non-null.
|
panelPadding |
CoreEdgeInsets? |
Panel content padding override. |
placementOffset |
double? |
Overlay placement offset (logical px) override — how far the floating panel sits from its trigger anchor along the placement axis (not content padding, not axis spacing). |
tailWidth |
double? |
Tail base width override — the triangle edge flush against the panel side.
0
removes the tail.
|
tailHeight |
double? |
Tail protrusion height override — how far the tip extends beyond the panel edge toward the trigger.
0
removes the tail.
|
surfaceBlur |
double? |
Backdrop blur sigma applied behind the panel. |
surfaceOpacity |
double? |
Panel surface opacity multiplier (0.0 – 1.0). |
labelStyle |
CoreTextStyle? |
Label text style override (applied when the contract's
message
string is rendered). Text colour is carried via [CoreTextStyle.color] inside this slot (sb8 — raw
panelForegroundColor
field removed).
|
Resolve chain#
design system default
→ CoreTooltipTheme.style // 프로젝트 공통
→ 부모 컴포넌트 슬롯 오버라이드
→ widget.tooltipStyle // 인스턴스별
변형 (Variants)#
위치#
// 위 (기본값)
Tooltip(
message: '위',
position: CoreTooltipPosition.top,
child: target,
)
// 아래
Tooltip(
message: '아래',
position: CoreTooltipPosition.bottom,
child: target,
)
// 좌/우
Tooltip(
message: '우',
position: CoreTooltipPosition.right,
child: target,
)
지연 시간 조절#
양 플랫폼 동일한 파라미터입니다.
// 즉시 표시
Tooltip(
message: '즉시 표시',
waitDuration: Duration.zero,
child: target,
)
// 긴 지연
Tooltip(
message: '1초 후 표시',
waitDuration: Duration(milliseconds: CoreDuration.ms1000),
child: target,
)
// 스쳐 지나가도 최소 500ms 는 읽을 수 있게
Tooltip(
message: '짧게 스쳐도 유지',
minDuration: Duration(milliseconds: CoreDuration.slow),
child: target,
)
리치 툴팁#
Tooltip(
message: '추가 정보',
tooltipChild: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
LucideIcons.info,
iconStyle: CoreIconStyle(size: CoreSize.size16),
),
Gap.space8(),
Text('추가 정보가 있습니다').bodySmall,
],
),
child: target,
)
동작 스펙 (Behavior)#
타이밍#
| 파라미터 | 기본값 | 설명 |
|---|---|---|
waitDuration |
CoreDuration.fast (150ms) |
호버 후 표시까지 대기 |
showDuration |
CoreDuration.fast (150ms) |
커서가 떠난 뒤 닫히기까지 머무는 시간 |
minDuration |
0ms (= 하한 없음) |
최소 표시 유지 시간 — 커서 도착 시점부터 측정 |
세 값의 단일 출처는
CoreTooltipContract.defaultWaitDuration/defaultShowDuration/defaultMinDuration이고, 양 플랫폼 모두 같은 이름의 위젯 파라미터로 인스턴스별 오버라이드를 받습니다. Flutter 는 이 값을Hover에 넘겨 타이머를 돌리고, Web 은resolveTooltipTiming(category E behavior timing) 이 채운 값으로mouseenter/mouseleave타이머를 돌립니다.minDuration은 양쪽 모두 "커서 도착 이후 경과 시간이minDuration보다 짧으면 남은 시간, 아니면showDuration" 이라는 동일한 계산으로 닫기 지연을 정합니다. 표시/닫기 애니메이션은 양 플랫폼 모두CoreTooltipContract.defaultOpenAnimationDuration(150 ms) /defaultCloseAnimationDuration(100 ms wall-clock ×Interval(0, 2/3)≈ 67 ms 가시 모션) 으로 고정.
위치#
// CoreTooltipPosition 기반 위치 제어
Tooltip(
position: CoreTooltipPosition.top,
message: '설명',
child: target,
)
-
position:top,bottom,left,right중 선택 - 뷰포트 경계 자동 감지 및 위치 조정
Surface 효과#
blur / opacity 도 chrome 이라 tooltipStyle 을 거칩니다.
Tooltip(
message: '블러 효과',
tooltipStyle: CoreTooltipStyle(
surfaceBlur: 10,
surfaceOpacity: 0.8,
),
child: target,
)
애니메이션#
양 플랫폼 모두 동일한 토큰을 사용하는 Fade + Scale 트랜지션입니다.
- Open: 150 ms
linear(CoreDuration.fast) -
Close: 67 ms 가시 모션 (100 ms wall-clock ×
Interval(0, 2/3)) — Flutter 의Interval곡선을 Web 은transition-duration단축으로 동등하게 표현 - Scale:
0.9 ↔ 1.0(Flutter popover 의 collapsed / open scale 범위) - Transform-origin:
center(트리거 중앙 기준)
Web 구현#
-
OverlayHost의tooltip레이어로 panel 을 portal 마운트 → ancestoroverflow: hidden/transform컨테이너 안에서도 잘리지 않음 - 트리거 래퍼는
inline-flex(line-height descender 회피) -
panel 은
position: fixed+ viewport 좌표 —top/left/transform: translate(...)인라인으로 출력 -
호버 enter/leave 는 JS 이벤트로
setState처리 (CSS:hover가 아니라mouseenter/mouseleave핸들러) - viewport scroll/resize 및 트리거
ResizeObserver변화 시 anchor 자동 재계산
사용 가이드라인 (Usage Guidelines)#
트리거의 짧은 보조 설명에는 Tooltip을 사용합니다. 프로필 등 풍부한 호버 미리보기는 HoverCard, 입력·액션을 담는 앵커 패널은 Popover, 응답을 기다려야 하는 확인은 Dialog를 선택합니다. Tooltip은 선택 값이나 모달 포커스 관리 표면이 아닙니다.
✅ Do#
아이콘 전용 버튼에 항상 툴팁을 추가하세요.
Tooltip(
message: '저장 (Ctrl+S)',
child: Button(
variant: CoreButtonVariant.primary,
onPressed: handleSave,
child: Icon(LucideIcons.save),
),
)
텍스트 라벨이 없는 아이콘은 의미를 알기 어렵습니다.
❌ Don't#
이미 레이블이 있는 버튼에 같은 내용의 툴팁을 추가하지 마세요.
// ❌ Bad — 중복 정보
Tooltip(
message: '저장',
child: Button(variant: CoreButtonVariant.primary, onPressed: handleSave, child: Text('저장')),
)
단축키 같은 추가 정보가 있을 때만 사용하세요.
✅ Do#
툴팁 텍스트는 짧고 명확하게 작성하세요.
// ✅ Good
Tooltip(message: '실행 취소 (Ctrl+Z)', child: undoButton)
// ❌ Bad — 너무 긴 설명
Tooltip(message: '이 버튼을 클릭하면 마지막 작업이 취소되고 이전 상태로 복원됩니다.', child: undoButton)
1~2줄 이내로 유지합니다. 긴 설명은 HoverCard를 사용하세요.
❌ Don't#
인터랙티브 콘텐츠를 툴팁에 넣지 마세요.
마우스가 벗어나면 사라지므로 버튼, 링크 등을 배치할 수 없습니다. 클릭 가능한 콘텐츠는 Popover를 사용하세요.
✅ Do#
적절한 지연 시간을 유지하세요.
기본 150ms 지연(CoreDuration.fast)이 대부분의 경우에 적합합니다. 너무 짧으면 의도치 않게 나타나고, 너무 길면 사용자가 기다립니다.
접근성 (Accessibility)#
역할#
| 트리거 | 버블 | |
|---|---|---|
| Flutter |
Semantics(container: true, tooltip: message)
— 메시지를 트리거의 시맨틱 힌트로 노출.
tooltipChild
를 쓰거나
message
가 비면
tooltip
이
null
이 되어 아무것도 붙지 않습니다
|
없음 — Flutter 에는 떠 있는 패널에 대응하는 시맨틱 노드가 없습니다 |
| Web | 없음 — 트리거 <div> 에는 role 도 aria-* 도 붙지 않습니다 |
열려 있을 때만 DOM 에 마운트되는 <div role="tooltip"> |
aria-describedby 배선이 없어 Web 버블은 트리거와 접근성 트리에서 연결되지 않습니다.
키보드#
구현된 키가 없습니다. 툴팁은 포인터 호버로만 열립니다.
| 키 | Flutter | Web |
|---|---|---|
Tab 으로 트리거 도달 |
Tooltip 래퍼는 포커스 대상이 아님 | 트리거 <div> 에 tabindex 없음 |
| 포커스로 열기 | 없음 — Hover 가 MouseRegion 만 사용 |
없음 — 트리거에 mouseenter / mouseleave 만 배선 |
Escape 로 닫기 | 없음 | 없음 (closeOnEsc: false) |
child 로 넘긴 위젯이 그 자체로 포커스 가능하더라도(예: Button) 포커스만으로는 툴팁이 열리지 않습니다 — 포커스 리스너가 없습니다.
스크린 리더#
-
Flutter — 메시지가 트리거의 시맨틱 tooltip 힌트로 노출되므로, 트리거를 탐색하면 툴팁을 열지 않고도 읽힙니다. 단
tooltipChild로 커스텀 콘텐츠를 넣은 경로는 텍스트가 없어 이 힌트가 생략됩니다. -
Web — 버블은 열렸을 때만 DOM 에 있고
role="tooltip"을 갖지만,aria-describedby로 트리거와 이어지지 않아 트리거를 읽을 때 함께 읽히지 않습니다.
툴팁이 열렸음을 알리는 live region 은 양 플랫폼 모두 없습니다.
포커스 관리#
-
진입 — 없음. Flutter 는 오버레이를
modal: false로 띄워 모달 전용requestFocus()경로를 타지 않고, Web 은 버블로 포커스를 옮기는 코드가 없습니다. - 이탈 — 진입이 없으므로 복귀도 없습니다.
- 트랩 — 없음. Web 버블은
#coui-overlay-tooltip레이어로 포탈되어 문서 끝에 놓입니다.
알려진 제약#
-
터치 전용 기기에서는 툴팁을 열 방법이 없습니다. Flutter 는
Hover의MouseRegion, Web 은mouseenter/mouseleave만 듣고 롱프레스·탭 대안이 양쪽 다 없습니다. 대안: 설명이 꼭 필요하면 툴팁이 아니라 화면에 상시 표시하거나 트리거의 접근 가능한 이름 안에 넣으세요. -
툴팁 내용이 트리거와 접근성 트리에서 이어지지 않습니다. Web 은
aria-describedby가 없고, Flutter 도tooltipChild경로에서는 시맨틱 힌트가 사라집니다. 아이콘 전용 트리거처럼 툴팁이 유일한 설명 수단이라면 트리거 자체에 이름(Semantics(label:)/aria-label)을 별도로 주세요 — 툴팁만으로는 이름이 되지 않습니다.
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 리치 콘텐츠 | tooltipChild: Widget |
tooltipChild: Component |
| 표시 딜레이 |
waitDuration
/
showDuration
/
minDuration
파라미터 →
Hover
타이머
|
같은 파라미터 → resolveTooltipTiming → mouseenter / mouseleave 타이머 |
| 애니메이션 | Fade + Scale (open 150 ms / close 67 ms, scale 0.9↔1.0) | Fade + Scale (open 150 ms / close 67 ms, scale 0.9↔1.0) — 동일 토큰 |
| 오버레이 마운트 | Root Overlay (OverlayManager) |
OverlayHost.tooltip 레이어 portal |
| 모바일 | 미지원 (터치 대안 없음) | 미지원 (터치 대안 없음) |
| Surface 효과 | surfaceBlur, surfaceOpacity |
surfaceBlur (backdrop-filter), surfaceOpacity |
| 테마 | Theme.of(context) |
Tailwind CSS + CoreComponentTheme |
관련 컴포넌트 (Related Components)#
- HoverCard: 풍부한 호버 미리보기. Tooltip보다 크고 복잡한 콘텐츠
- Popover: 클릭 트리거 팝업. 인터랙티브 콘텐츠에 적합
- Toast: 알림 메시지. 액션 결과 피드백 용도
조합 예제#
// 툴바 아이콘 버튼 패턴
Row(children: [
Tooltip(
message: '실행 취소 (Ctrl+Z)',
child: Button(
variant: CoreButtonVariant.ghost,
onPressed: handleUndo,
child: Icon(LucideIcons.undo),
),
),
Tooltip(
message: '다시 실행 (Ctrl+Y)',
child: Button(
variant: CoreButtonVariant.ghost,
onPressed: handleRedo,
child: Icon(LucideIcons.redo),
),
),
Tooltip(
message: '서식 지우기',
child: Button(
variant: CoreButtonVariant.ghost,
onPressed: handleClearFormat,
child: Icon(LucideIcons.removeFormatting),
),
),
])