HoverCard#
트리거 요소 위에 마우스를 올리면 추가 정보를 담은 카드를 표시하는 컴포넌트입니다. 지연 시간을 조절하여 의도치 않은 표시를 방지할 수 있습니다.
Live Preview#
Web
@coui
Flutter
Loading Flutter...
class HoverCardDefaultExample extends StatelessComponent {
const HoverCardDefaultExample({super.key});
@override
Component build(BuildContext context) {
return CoHoverCard(
trigger: span(
[const Text('@coui').bodyMedium.primary.underline],
classes: 'cursor-pointer',
),
content: const Text('크로스 플랫폼 UI 디자인 시스템.').bodySmall.onSurface,
placement: CorePopoverPlacement.bottom,
);
}
}
class HoverCardDefaultExample extends StatefulWidget {
const HoverCardDefaultExample({super.key});
@override
State<HoverCardDefaultExample> createState() =>
_HoverCardDefaultExampleState();
}
class _HoverCardDefaultExampleState extends State<HoverCardDefaultExample> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return CoHoverCard(
trigger: Text(
'@coui',
style: theme.typography.bodyMedium.copyWith(
color: theme.colorScheme.primary,
decoration: TextDecoration.underline,
decorationColor: theme.colorScheme.primary,
),
),
content: Text(
'크로스 플랫폼 UI 디자인 시스템.',
style: theme.typography.bodySmall.copyWith(
color: theme.colorScheme.onSurface,
),
),
placement: CorePopoverPlacement.bottom,
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 사용자 멘션(@username), 링크 등에 호버 시 미리보기 카드를 표시할 때
- 아이콘이나 축약된 텍스트에 호버 시 상세 정보를 보여줄 때
- 클릭 없이 추가 맥락 정보를 제공하고 싶을 때
대신 다른 컴포넌트를 사용하세요:
Tooltip: 짧은 텍스트 힌트만 표시할 때 (복잡한 카드 내용이 아닌 경우)Popover: 클릭으로 표시되고 사용자 인터랙션이 필요한 오버레이에- 터치 기기 사용자에게는 HoverCard가 작동하지 않으므로 대안 UI 고려
기본 사용법 (Basic Usage)#
// 기본 호버 카드
CoHoverCard(
trigger: Text('@nextjs'),
content: Text('The React Framework for the Web.'),
)
// 표시 위치 및 지연 시간 설정
CoHoverCard(
trigger: Icon(Icons.info_outline),
content: Text('추가 정보를 여기에 표시합니다.'),
placement: CorePopoverPlacement.top,
openDelay: Duration(milliseconds: 500),
closeDelay: Duration(milliseconds: 200),
)
CoHoverCard(
trigger: Component.text('@nextjs'),
content: Component.text('The React Framework for the Web.'),
)
CoHoverCard(
trigger: Component.text('정보'),
content: Component.text('추가 정보'),
placement: CorePopoverPlacement.top,
openDelay: Duration(milliseconds: 300),
closeDelay: Duration(milliseconds: 150),
)
Props / Parameters#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
trigger |
Widget / Component |
필수 | 호버를 감지할 트리거 |
content |
Widget / Component |
필수 | 호버 시 표시할 카드 내용 |
placement |
CorePopoverPlacement |
bottom |
카드가 표시될 방향 |
openDelay |
Duration? |
500ms (CoreDuration.tooltipDelay) |
카드 표시까지의 지연 시간 |
closeDelay |
Duration? |
500ms (CoreDuration.tooltipDelay) |
카드 닫힘까지의 지연 시간 |
onClose |
VoidCallback? |
null |
카드가 닫힐 때 호출 |
변형 (Variants)#
방향 (Placement)#
카드가 트리거 기준으로 표시될 방향을 지정합니다.
// 상단 표시
CoHoverCard(
trigger: Text('상단'),
content: Text('상단에 표시'),
placement: CorePopoverPlacement.top,
)
// 우측 표시
CoHoverCard(
trigger: Text('우측'),
content: Text('우측에 표시'),
placement: CorePopoverPlacement.right,
)
// 좌측 표시
CoHoverCard(
trigger: Text('좌측'),
content: Text('좌측에 표시'),
placement: CorePopoverPlacement.left,
)
// 하단 표시 (기본)
CoHoverCard(
trigger: Text('하단'),
content: Text('하단에 표시'),
placement: CorePopoverPlacement.bottom,
)
동작 스펙 (Behavior)#
인터랙션#
- 호버 진입:
openDelay후 카드 표시 - 호버 이탈:
closeDelay후 카드 닫힘 - 카드로 마우스 이동: 카드가 열린 상태 유지 (사용자가 카드와 인터랙션 가능)
- 터치: Flutter는 길게 누르기로 열림, Web은 기본적으로 비활성
상태 전환#
hidden→visible: openDelay 이후 페이드 인visible→hidden: closeDelay 이후 페이드 아웃
애니메이션#
양 플랫폼 모두 동일한 토큰을 사용하는 Fade + Scale 트랜지션입니다.
- Open: 150 ms
linear(AnimationConstants.short) - Close: 67 ms 가시 모션 (100 ms wall-clock ×
Interval(0, 2/3)) -
Scale:
0.9 ↔ 1.0(CorePopoverTokens.scaleClosed/scaleOpen) -
Open delay / Close delay:
CoreHoverCardTokens.openDelayMs/closeDelayMs(각 500 ms 기본). 트리거 hover-enter 후openDelay만큼 지연된 뒤 카드 표시 / hover-leave 후closeDelay만큼 유예 후 닫힘 (그 동안 패널 안으로 진입하면 닫힘 취소)
사용 가이드라인 (Usage Guidelines)#
✅ Do#
openDelay를 충분히 설정해 의도치 않은 표시 방지
CoHoverCard(
trigger: Text('@username'),
content: UserProfileCard(username: 'username'),
openDelay: const Duration(milliseconds: 500),
)
너무 짧은 openDelay는 마우스가 지나갈 때마다 카드가 표시되어 방해가 된다.
❌ Don't#
터치 기기에서만 사용되는 UI에 HoverCard 적용
// ❌ 모바일 앱의 주요 정보를 HoverCard에만 표시
CoHoverCard(
trigger: Text('자세히'),
content: ImportantInfo(), // 터치 사용자는 볼 수 없음
)
HoverCard는 마우스 호버가 가능한 환경에서만 동작하므로 중요 정보는 다른 방법으로도 접근 가능해야 한다.
✅ Do#
카드 내용에 사용자 프로필, 링크 미리보기 등 풍부한 정보 제공
CoHoverCard(
trigger: Text('@hong_gildong', style: linkStyle),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
CoAvatar(imageUrl: user.avatarUrl),
Text(user.name, style: titleStyle),
Text(user.bio),
Row(children: [
Text('팔로워: ${user.followers}'),
const Gap.md(),
Text('팔로잉: ${user.following}'),
]),
],
),
)
HoverCard는 Tooltip보다 많은 정보를 담을 수 있어 풍부한 미리보기에 적합하다.
❌ Don't#
필수 액션(버튼, 폼)을 HoverCard에만 배치하지 않기
// ❌ 호버해야만 볼 수 있는 중요한 액션
CoHoverCard(
trigger: Icon(Icons.settings),
content: Column(
children: [
CoButton(onPressed: handleDeletePressed, child: Text('계정 삭제')),
],
),
)
호버에서만 접근 가능한 액션은 키보드 사용자와 터치 사용자가 접근할 수 없다.
접근성 (Accessibility)#
키보드 인터랙션#
| 키 | 동작 |
|---|---|
Tab | 트리거 요소로 포커스 이동 시 카드 표시 |
Escape | 열린 카드 닫기 |
스크린 리더#
- Flutter:
Semantics(tooltip: ...)또는 별도 읽기 영역으로 카드 내용 접근 가능 - Web:
role="dialog"+aria-describedby로 트리거와 카드 내용 연결
터치 타겟#
- 트리거 요소 최소 크기: 44x44dp
- 카드가 열려있는 경우 카드 영역도 터치 가능
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | CoHoverCard | CoHoverCard |
| 호버 감지 | MouseRegion 위젯 |
JS mouseenter / mouseleave 이벤트 |
| 위치 계산 | PopoverController 기반 root Overlay 마운트 |
CoOverlayHost.popover 레이어 portal + position: fixed + viewport 좌표 |
| 클리핑 | 영향 없음 (root Overlay) | 영향 없음 (overlay host portal) |
| 터치 지원 | 길게 누르기로 활성화 | 기본 비활성 |
| Open 애니메이션 | 150 ms linear, scale 0.9→1.0 |
150 ms linear, scale 0.9→1.0 — 동일 |
| Close 애니메이션 | 67 ms 가시 (Interval(0, 2/3)) |
67 ms 가시 (transition-duration 단축) — 동일 |
| Open / Close delay | CoreHoverCardTokens.openDelayMs / closeDelayMs (각 500 ms) |
CoreHoverCardTokens.openDelayMs / closeDelayMs (각 500 ms) — 동일 |
관련 컴포넌트 (Related Components)#
조합 예제#
// 소셜 피드에서 사용자 멘션 호버 카드
RichText(
text: TextSpan(
children: [
const TextSpan(text: '안녕하세요, '),
WidgetSpan(
child: CoHoverCard(
trigger: Text('@홍길동', style: mentionStyle),
content: UserHoverCard(username: '홍길동'),
openDelay: const Duration(milliseconds: 400),
),
),
const TextSpan(text: ' 반가워요!'),
],
),
)