Tooltip#
요소에 호버하거나 포커스할 때 설명 텍스트를 표시하는 툴팁 컴포넌트입니다.
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'),
),
);
}
}
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),
),
),
);
}
}
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),
),
),
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 아이콘 버튼이나 축약된 UI에 추가 설명이 필요할 때
- 키보드 단축키 정보를 호버로 표시할 때
- 짧은 텍스트 힌트(1~2줄)로 충분할 때
대신 다른 컴포넌트를 사용하세요:
HoverCard: 이미지, 버튼 등 풍부한 콘텐츠가 필요할 때Popover: 클릭으로 열리는 인터랙티브 콘텐츠일 때Toast: 액션 결과 피드백을 표시할 때
기본 사용법 (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('실행 취소'),
),
)
Props / Parameters#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
message | String | '' | 툴팁 텍스트 메시지 |
tooltipChild |
Widget? / Component? |
null |
커스텀 툴팁 콘텐츠 (message 대체) |
child |
Widget / Component |
필수 | 툴팁을 표시할 대상 위젯 |
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),
),
),
);
}
}
스타일 시스템 — 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 + 전경색)이 기본값으로 상속된다. 자식이 스스로 지정한 색·크기는 그대로 이긴다.
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)#
✅ 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)#
키보드 인터랙션#
Tooltip 은 현재 포인터 hover 로만 열립니다 — 키보드 포커스만으로 여는 트리거,
그리고 Escape 로 닫는 동작은 아직 배선되지 않았습니다(양 플랫폼 동일한
제약). 아이콘 전용 트리거처럼 툴팁이 유일한 설명 수단인 경우, 키보드 사용자를
위해 트리거 자체에 접근 가능한 이름(Semantics(label:) / aria-label)을
별도로 제공하세요.
스크린 리더#
-
Flutter: 트리거를 감싸는
Semantics(container: true, tooltip: message)로 메시지를 노출합니다 —tooltipChild로 커스텀 콘텐츠를 넣은 경우는 텍스트가 없으므로 생략됩니다. -
Web: 패널이 열려 있을 때만 DOM 에 마운트되며, 그 패널 요소(
<div>)에role="tooltip"이 적용됩니다.aria-describedby로 트리거와 연결되지는 않으므로, 툴팁이 유일한 설명 수단인 경우 트리거에aria-label을 추가로 제공하는 것을 권장합니다.
모바일#
- Flutter: 포인터 hover 전용, 롱프레스 등 터치 대안 없음
- Web: 포인터 호버 전용, 터치 대안 없음
크로스 플랫폼 차이점 (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),
),
),
])