Tooltip | CoUI
본문으로 건너뛰기
LogoCoUI

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#

사용 시기 (When to Use)#

이 컴포넌트를 사용하세요:

  • 아이콘 버튼이나 축약된 UI에 추가 설명이 필요할 때
  • 키보드 단축키 정보를 호버로 표시할 때
  • 짧은 텍스트 힌트(1~2줄)로 충분할 때

대신 다른 컴포넌트를 사용하세요:

  • HoverCard: 이미지, 버튼 등 풍부한 콘텐츠가 필요할 때
  • Popover: 클릭으로 열리는 인터랙티브 콘텐츠일 때
  • Toast: 액션 결과 피드백을 표시할 때

Props / Parameters#

W는 Flutter의 Widget, Web의 Component를 뜻하는 합성 콘텐츠 타입입니다. wait/show/minDuration은 Duration이며 표의 ms는 밀리초입니다.

속성타입기본값설명
messageString''툴팁 텍스트 메시지
tooltipChild W? null 커스텀 툴팁 콘텐츠 (message 대체)
childW필수툴팁을 표시할 대상 콘텐츠
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 마운트 → 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)#

트리거의 짧은 보조 설명에는 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 버블은 트리거와 접근성 트리에서 연결되지 않습니다.

키보드#

구현된 키가 없습니다. 툴팁은 포인터 호버로만 열립니다.

키FlutterWeb
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)#

항목FlutterWeb
리치 콘텐츠 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
  • 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),
    ),
  ),
])