Hover#
자식 위젯을 감싸 두 가지 기능을 제공하는 래퍼 컴포넌트입니다.
-
호버 상태 추적 —
onHover/onEnter/onExit콜백으로 포인터 호버 상태를 보고합니다. -
호버 시각 효과 — 포인터가 위에 있는 동안
effect(scale / fade / brighten / shadow)를 애니메이션합니다.
Live Preview#
class HoverDefaultExample extends StatelessComponent {
const HoverDefaultExample({super.key});
@override
Component build(BuildContext context) {
return Hover(
effect: CoreHoverEffect.scaleUp,
child: Card(child: const Text('Hover me')),
);
}
}
class HoverDefaultExample extends StatelessWidget {
const HoverDefaultExample({super.key});
@override
Widget build(BuildContext context) {
return Hover(
effect: CoreHoverEffect.scaleUp,
child: Card(child: const Text('Hover me')),
);
}
}
class HoverFadeExample extends StatelessComponent {
const HoverFadeExample({super.key});
@override
Component build(BuildContext context) {
return Hover(
effect: CoreHoverEffect.fade,
child: Card(child: const Text('Hover me')),
);
}
}
class HoverFadeExample extends StatelessWidget {
const HoverFadeExample({super.key});
@override
Widget build(BuildContext context) {
return Hover(
effect: CoreHoverEffect.fade,
child: Card(child: const Text('Hover me')),
);
}
}
class HoverShadowExample extends StatelessComponent {
const HoverShadowExample({super.key});
@override
Component build(BuildContext context) {
return Hover(
effect: CoreHoverEffect.shadow,
child: Card(
cardStyle: const CoreCardStyle(boxShadow: CoreShadow.none),
child: const Text('Hover me'),
),
);
}
}
class HoverShadowExample extends StatelessWidget {
const HoverShadowExample({super.key});
@override
Widget build(BuildContext context) {
return Hover(
effect: CoreHoverEffect.shadow,
child: Card(
cardStyle: const CoreCardStyle(boxShadow: CoreShadow.none),
child: const Text('Hover me'),
),
);
}
}
class HoverChainExample extends StatelessComponent {
const HoverChainExample({super.key});
@override
Component build(BuildContext context) {
return Hover(
effect: CoreHoverEffect.scaleUp,
child: Card(child: const Text('Hover me')),
).withStyle(
const CoreHoverStyle(
scaleUpFactor: 1.15,
effectDuration: Duration(milliseconds: CoreDuration.moderate),
waitDuration: Duration(milliseconds: CoreDuration.fast),
shadowBoxShadow: CoreShadow.xl,
shadowBaseColor: CoreColor.token(CoreColors.primary),
),
);
}
}
class HoverChainExample extends StatelessWidget {
const HoverChainExample({super.key});
@override
Widget build(BuildContext context) {
return Hover(
effect: CoreHoverEffect.scaleUp,
child: Card(child: const Text('Hover me')),
).withStyle(
const CoreHoverStyle(
scaleUpFactor: 1.15,
effectDuration: Duration(milliseconds: CoreDuration.moderate),
waitDuration: Duration(milliseconds: CoreDuration.fast),
shadowBoxShadow: CoreShadow.xl,
shadowBaseColor: CoreColor.token(CoreColors.primary),
),
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 카드/이미지에 호버 시 확대·페이드·그림자 효과를 주고 싶을 때
- 포인터 호버 진입/이탈을 콜백으로 추적해야 할 때
대신 다른 컴포넌트를 사용하세요:
HoverCard: 호버 시 떠오르는 정보 패널이 필요할 때Tooltip: 호버 시 간단한 도움말 텍스트를 띄울 때
기본 사용법 (Basic Usage)#
Hover(
effect: CoreHoverEffect.scaleUp,
child: Card(child: const Text('Hover me')),
)
Hover(
effect: CoreHoverEffect.scaleUp,
child: Card(child: const Text('Hover me')),
)
빠른 오버라이드 (Chain)#
이미 만든 Hover 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
class HoverChainExample extends StatelessWidget {
const HoverChainExample({super.key});
@override
Widget build(BuildContext context) {
return Hover(
effect: CoreHoverEffect.scaleUp,
child: Card(child: const Text('Hover me')),
).withStyle(
const CoreHoverStyle(
scaleUpFactor: 1.15,
effectDuration: Duration(milliseconds: CoreDuration.moderate),
waitDuration: Duration(milliseconds: CoreDuration.fast),
shadowBoxShadow: CoreShadow.xl,
shadowBaseColor: CoreColor.token(CoreColors.primary),
),
);
}
}
class HoverChainExample extends StatelessComponent {
const HoverChainExample({super.key});
@override
Component build(BuildContext context) {
return Hover(
effect: CoreHoverEffect.scaleUp,
child: Card(child: const Text('Hover me')),
).withStyle(
const CoreHoverStyle(
scaleUpFactor: 1.15,
effectDuration: Duration(milliseconds: CoreDuration.moderate),
waitDuration: Duration(milliseconds: CoreDuration.fast),
shadowBoxShadow: CoreShadow.xl,
shadowBaseColor: CoreColor.token(CoreColors.primary),
),
);
}
}
Props / Parameters#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
child |
Widget? / Component? |
null |
호버 영역으로 감쌀 콘텐츠 |
effect |
CoreHoverEffect |
CoreHoverEffect.defaultEffect (none) |
호버 시각 효과 (none, scaleUp, scaleDown, fade, brighten, shadow) |
onHover |
ValueChanged<bool>? |
null |
해소된 호버 상태 변경 콜백 |
onEnter |
void Function()? |
null |
포인터 진입 즉시 호출 |
onExit |
void Function()? |
null |
포인터 이탈 즉시 호출 |
waitDuration |
Duration? |
null → 500ms (defaultWaitDuration) |
onHover(true) 발화 전 대기 시간 |
showDuration |
Duration? |
null → 200ms (defaultShowDuration) |
이탈 후 onHover(false) 발화 전 대기 시간 |
minDuration |
Duration? |
null → 0ms (defaultMinDuration) |
호버 상태 최소 유지 시간 |
hoverStyle |
CoreHoverStyle? |
null |
효과 배율·그림자·전환 duration·타이밍 단일 진입점 |
타이밍 세 값(waitDuration / showDuration / minDuration)은 위젯 파라미터로도,
hoverStyle 안으로도 넣을 수 있습니다 — hoverStyle 쪽이 뒤에 와서 이깁니다.
CoreHoverStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
scaleUpFactor |
double? |
Scale factor applied for the
scaleUp
effect override.
null
defers to [defaultScaleUpFactor].
|
scaleDownFactor |
double? |
Scale factor applied for the
scaleDown
effect override.
null
defers to [defaultScaleDownFactor].
|
fadeOpacity |
double? |
Opacity applied for the
fade
effect override.
null
defers to [defaultFadeOpacity].
|
brightenFactor |
double? |
Brightness multiplier applied for the
brighten
effect override.
null
defers to [defaultBrightenFactor].
|
shadowBoxShadow |
List<CoreShadowLayer>? |
Drop-shadow stack applied for the
shadow
effect override.
null
defers to [defaultShadowBoxShadow].
|
shadowBaseColor |
CoreColor? |
Base colour the
shadow
effect's layers are tinted with override.
null
defers to [defaultShadowBaseColor].
|
effectDuration |
Duration? |
Transition duration for the hover visual effect override.
null
defers to [defaultEffectDuration].
|
waitDuration |
Duration? |
Delay before the hover state is reported as active override.
null
defers to [defaultWaitDuration].
|
showDuration |
Duration? |
Delay before the hover state is reported as inactive after the pointer leaves override.
null
defers to [defaultShowDuration].
|
minDuration |
Duration? |
Minimum duration the hover state stays active once reported override.
null
defers to [defaultMinDuration].
|
Resolve chain#
CoreHoverStyle.defaultX (static const)
→ CoreHoverTheme.style // 프로젝트 공통
→ widget 타이밍 파라미터 (wait / show / min) // 인스턴스별
→ widget.hoverStyle // 인스턴스별 (전체 chrome)
동작 스펙 (Behavior)#
- 포인터가 진입하면
onEnter가 즉시 호출되고, 시각 효과가 즉시 적용됩니다. - 포인터가
waitDuration동안 머무르면onHover(true)가 발화합니다. - 포인터가 이탈하면
onExit가 즉시 호출되고, 시각 효과가 즉시 해제됩니다. -
이탈 후
showDuration뒤에onHover(false)가 발화합니다. 단minDuration보다 빨리 이탈하면 남은 시간만큼 활성 상태가 유지됩니다. -
scaleUp/scaleDown은 1.05 / 0.95 배율,fade는 0.8 불투명도,brighten은 1.1배 밝기,shadow는CoreShadow.lg그림자를 적용합니다.
사용 가이드라인 (Usage Guidelines)#
✅ Do#
즉각적 시각 피드백은 effect, 디바운스된 상태 반응은 onHover
Hover(
effect: CoreHoverEffect.shadow,
onHover: (active) => setState(() => _isHighlighted = active),
child: Card(
cardStyle: const CoreCardStyle(boxShadow: CoreShadow.none),
child: content,
),
)
effect는 포인터 진입 즉시 애니메이션되어 바로 반응하는 시각 피드백을 주고, onHover 콜백은 waitDuration(기본 500ms)이 지난 뒤에만 발화해 빠르게 지나가는 포인터로 상태가 흔들리는 것을 걸러줍니다.
Card의 boxShadow: CoreShadow.none 오버라이드는 effect: .shadow 전용 안전장치입니다 — 아래 "알려진 제약"의 표면 스타일 겹침 항목 참고.
❌ Don't#
키보드/터치 사용자도 같은 피드백을 받을 거라 가정하지 않기
// ❌ 포커스만으로 오는 사용자는 이 효과를 절대 보지 못함
Hover(
effect: CoreHoverEffect.scaleUp,
child: Button(onPressed: submit, child: const Text('Submit')),
)
Hover는 포인터 전용입니다 — 키보드 포커스나 터치로는 onHover/onEnter/onExit도, 시각 효과도 발생하지 않습니다. 포커스 시 동일한 피드백이 필요하면 별도로 구현해야 합니다.
접근성 (Accessibility)#
Hover 는 포인터 호버만 다루는 순수 래퍼입니다. 시맨틱을 아무것도 기여하지 않으므로, 리더가 읽는 내용은 전적으로 child 자신이 가진 것입니다.
역할 / Semantics#
없습니다. Flutter 는 효과 래퍼를 감싼 MouseRegion 만 반환하고 Semantics 를 붙이지 않으며, Web 은 role
도 aria-* 도 없는 <div> 를 렌더합니다(호출자가 직접 넘긴 attributes 만 통과합니다). 이 영역에 이름이나 역할이 필요하다면
child 쪽에서 붙여야 합니다.
키보드#
컴포넌트가 처리하는 키는 없습니다. Web 은 base 가 onKeyDown / onKeyUp 파라미터를 노출하지만 루트 <div>
에 tabindex 가 없어, 포커스된 자손에서 버블링된 이벤트에만 발화하는 순수 passthrough 입니다. Flutter 에는 대응 파라미터 자체가 없습니다.
포커스#
없습니다. 루트는 포커스 대상이 아니고 포커스 링도 없습니다. 호버 상태는 오직 포인터에서만 옵니다 — Flutter 는 MouseRegion 의 enter/exit, Web 은
mouseenter / mouseleave 입니다.
스크린 리더#
child 가 말하는 그대로입니다. 다만 effect: CoreHoverEffect.shadow 에서 Flutter 는 그림자 레이어마다 child 를 복제하고, 그 복제본이 시맨틱 트리에 그대로 남습니다. 기본 2-레이어 그림자에서는 같은 내용이
3번 읽힙니다. Web 은 같은 그림자를 CSS filter 한 번으로 그리므로 1번만 읽힙니다.
알려진 제약#
-
포인터 전용입니다.
onHover/onEnter/onExit는 키보드 포커스나 터치로는 발화하지 않고, 시각 효과도 적용되지 않습니다. 포커스에서 같은 반응이 필요하면 호출자가 직접 구현해야 합니다. -
동작 줄이기가 플랫폼별로 갈립니다. Flutter 는
effectDuration과waitDuration/showDuration/minDuration을 모두 모션 해석 지점에 통과시켜, "동작 줄이기"가 켜지면 효과와 500ms 호버 지연이 함께 0 이 됩니다. Web 은 transition duration 을 inline 리터럴 ms 로 내보내고 타이밍도 감축하지 않아prefers-reduced-motion이 닿지 않습니다 — 전역 접근성 축 에 적힌 "토큰을 거치지 않는 임의 duration" 한계에 해당합니다. -
.shadow의 시맨틱 중복(위)은 아직 수정되지 않았습니다. 그 효과를 쓰면서 중복 announce 를 피해야 한다면 다른 효과를 쓰거나child를 직접 시맨틱 처리해야 합니다. -
effect: .shadow는child가 이미 그리고 있는 그림자와 겹쳐 캐스트됩니다.filter: drop-shadow()(Web) / 레이어마다child를 다시 칠하는 방식(Flutter) 모두child를 통째로 다시 그림자로 캐스트하므로,child가 자기 자신의 box-shadow 를 이미 그리고 있으면(예:Card의 rest elevation, 혹은 neo-brutalism 처럼 그림자 없는 표면에 스타일이 자체적으로 cast 를 얹는 surface style 아래) 두 그림자가 겹쳐 보이고 rest 상태보다 훨씬 커 보입니다.child가 그 자체로 그림자를 그리지 않게 만드세요(위 예제의boxShadow: CoreShadow.none처럼) — 그림자가 없는child(이미지, 아바타, plain box)로 바꿔도 됩니다.
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | Hover | Hover |
| 효과 적용 |
AnimatedScale
/
AnimatedOpacity
/
ColorFiltered
/
AnimatedContainer
|
CSS
transform
/
opacity
/
filter
/
box-shadow
+
transition
|
| 이벤트 | MouseRegion enter/exit |
mouseenter / mouseleave event |