Tooltip | CoUI
LogoCoUI

Tooltip

툴팁 컴포넌트

Tooltip#

요소에 호버하거나 포커스할 때 설명 텍스트를 표시하는 툴팁 컴포넌트입니다.

Live Preview#

사용 시기 (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#

속성타입기본값설명
messageString''툴팁 텍스트 메시지
tooltipChild Widget? / Component? null 커스텀 툴팁 콘텐츠 (message 대체)
child Widget / Component 필수 툴팁을 표시할 대상 위젯
position CoreTooltipPosition CoreTooltipPosition.top 툴팁 위치 (시맨틱 — 위젯 파라미터)
waitDuration Duration? CoreTooltipContract.defaultWaitDuration (150ms) 커서가 트리거에 머물러야 하는 시간 — 이만큼 지나야 열림
showDuration Duration? CoreTooltipContract.defaultShowDuration (150ms) 커서가 떠난 뒤 닫히기 시작할 때까지 머무는 시간
minDuration Duration? nullCoreTooltipContract.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 구현#

  • OverlayHosttooltip 레이어로 panel 을 portal 마운트 → ancestor overflow: 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)#

항목FlutterWeb
리치 콘텐츠 tooltipChild: Widget tooltipChild: Component
표시 딜레이 waitDuration / showDuration / minDuration 파라미터 → Hover 타이머 같은 파라미터 → resolveTooltipTimingmouseenter / 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
  • 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),
    ),
  ),
])