Hidden#
hidden bool 하나로 자식을 opacity + 축소 애니메이션과 함께 토글합니다. 접히는 축을 direction
(vertical/horizontal)으로 지정하고, 공간을 유지하려면 keepSpace: true, 역방향 접힘은
reverse: true.
Live Preview#
class HiddenDefaultExample extends StatefulComponent {
const HiddenDefaultExample({super.key});
@override
State<HiddenDefaultExample> createState() => _HiddenDefaultExampleState();
}
class _HiddenDefaultExampleState extends State<HiddenDefaultExample> {
bool _hidden = false;
@override
Component build(BuildContext context) {
final cs = context.theme.colorScheme;
return div(
[
Button(
variant: CoreButtonVariant.primary,
onPressed: () => setState(() => _hidden = !_hidden),
child: Text(_hidden ? 'Show' : 'Hide'),
),
Gap.space12(),
Hidden(
hidden: _hidden,
child: div(
[
div(
[Text('Toggleable content')],
classes: 'text-${CoreTextStyles.bodyMedium.name} text-${cs.onSurface}',
),
],
classes:
'bg-${cs.surfaceContainer} rounded-${CoreRadius.scale.radius16} p-${CoreSpace.scale.space16}',
),
),
],
classes: 'flex flex-col items-start',
);
}
}
class HiddenDefaultExample extends StatefulWidget {
const HiddenDefaultExample({super.key});
@override
State<HiddenDefaultExample> createState() => _HiddenDefaultExampleState();
}
class _HiddenDefaultExampleState extends State<HiddenDefaultExample> {
bool _hidden = false;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Button(
variant: CoreButtonVariant.primary,
onPressed: () => setState(() => _hidden = !_hidden),
child: Text(_hidden ? 'Show' : 'Hide'),
),
Gap.space12(),
Hidden(
hidden: _hidden,
child: Container(
padding: EdgeInsets.all(CoreSpace.space16),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainer.toValue(),
borderRadius: BorderRadius.circular(CoreRadius.radius16),
),
child: Text(
'Toggleable content',
style: theme.typography.bodyMedium.toValue(theme: theme).copyWith(
color: theme.colorScheme.onSurface.toValue(),
),
),
),
),
],
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 단일 영역을 부드럽게 보였다 숨겼다 해야 할 때 (알림·오류 메시지, 확장 드로어)
- 접힘 애니메이션이 필요하지만 헤더/트리거가 없는 경우
대신 다른 컴포넌트를 사용하세요:
Collapsible: 트리거(헤더)가 있는 접기/펼치기Swap: 두 콘텐츠를 교차 전환
기본 사용법 (Basic Usage)#
bool _hidden = false;
Hidden(
hidden: _hidden,
direction: CoreHiddenDirection.vertical,
child: Text('Toggleable content'),
)
Props / Parameters#
Hidden#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
child | W | 필수 | 토글되는 콘텐츠 |
hidden |
bool |
필수 | true면 opacity 0 + 축소, false면 원상태 |
direction |
CoreHiddenDirection? |
vertical |
vertical (height 접힘) / horizontal (width 접힘) |
duration | Duration? | 200ms | 전환 시간 |
keepSpace |
bool? |
false | true면 크기 유지, opacity만 애니메이션 |
reverse |
bool? |
false | true면 trailing edge(right/bottom)부터 접힘 |
curve |
CoreCubicBezier? |
CoreEasing.move |
전환 easing. 양 플랫폼이 같은 토큰을 읽습니다 |
semanticLabel | String? | null | 스크린리더 라벨 |
curve 는 플랫폼 중립인 cubic-bezier 제어점 4개(CoreEasing.*)를 받습니다 —
Flutter 는 이것을 Curve 로, Web 은 같은 제어점을 cubic-bezier(...)
timing function 으로 변환하므로 한 토큰이 양쪽 곡선을 정합니다. Flutter
Curves.* 를 직접 넘길 수는 없습니다 (Web 이 미러할 수 없는 타입입니다).
전환 시간과 easing 의 디자인 시스템 기본값은 CoreHiddenStyle
(defaultTransitionDuration / defaultTransitionCurve) 에 있고,
프로젝트 단위로는 CoreHiddenTheme.style 로 덮습니다.
CoreHiddenStyle.defaultTransitionCurve // 디자인 시스템 기본값
→ CoreHiddenTheme.style.transitionCurve // 프로젝트 공통
→ CoreHiddenTheme.curve // 프로젝트 fast-path
→ widget.curve // 인스턴스별
스타일 시스템 (Style System)#
CoreHiddenStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
transitionDuration |
Duration? |
Collapse / expand transition duration override. null → [defaultTransitionDuration]. |
transitionCurve |
CoreCubicBezier? |
Collapse / expand transition easing override. null → [defaultTransitionCurve]. |
사용 가이드라인 (Usage Guidelines)#
✅ Do#
주변 레이아웃이 움직이면 안 될 때 keepSpace: true
Hidden(
hidden: _hidden,
keepSpace: true,
child: errorMessage,
)
기본값(keepSpace: false)은 opacity뿐 아니라 공간까지 접어서(축소) 주변 레이아웃을 움직입니다. 인라인 알림처럼 자리를 고정해야 하면 keepSpace: true로 opacity만 애니메이션하세요.
❌ Don't#
숨긴 콘텐츠 안의 인터랙티브 요소가 안전하게 차단될 거라 기대하지 않기
// ❌ 숨겨진 버튼이 탭/리더에서 제외될 거라 가정
Hidden(
hidden: true,
child: Button(onPressed: submit, child: const Text('Submit')),
)
Hidden은 시각적으로만 숨길 뿐 inert/ExcludeFocus를 적용하지 않습니다 — 숨겨진 버튼도 Tab으로 포커스되고(Flutter는) 스크린리더에 계속 읽힙니다. 키보드/스크린리더 접근을 실제로 차단하려면 별도 처리가 필요합니다.
접근성 (Accessibility)#
Hidden 은 시각적으로만 숨기는 래퍼입니다. 접근성 관점의 숨김(리더에서 제거 + 포커스에서 제외)은 두 플랫폼 모두 완결되어 있지 않으므로, 아래 제약을 읽고 필요한 부분은 직접 채워야 합니다.
역할 / Semantics#
-
Flutter:
role을 내보내지 않습니다.semanticLabel을 준 경우에만Semantics(container: true, label: ...)로 라벨이 붙은 컨테이너 노드 하나가 추가되고, 기본값(null)에서는 아무 노드도 추가되지 않습니다. -
Web: 루트
<div>에role을 설정하지 않습니다.aria-hidden은hidden값을 그대로 따라 항상"true"또는"false"로 나가고,data-state도 함께 나갑니다.semanticLabel은aria-label로 나가지만 role 이 없는 generic<div>위에 얹히므로 대부분의 리더가 무시합니다.
키보드#
없습니다. 이 컴포넌트는 어떤 키도 처리하지 않습니다.
포커스#
포커스를 전혀 관리하지 않으며, 숨김 상태에서도 마찬가지입니다.
-
Flutter:
IgnorePointer(ignoring: hidden)로 포인터 입력만 차단합니다.ExcludeFocus/ExcludeSemantics/BlockSemantics가 없어,hidden: true인 동안에도 자식은 포커스 traversal 안에 남아 있습니다. -
Web: 숨김 상태는
opacity: 0,pointer-events: none, (keepSpace가 아니면)max-height/max-width: 0+overflow: hidden으로만 표현됩니다.display: none,visibility: hidden,inert,tabindex="-1"중 어느 것도 적용되지 않아 포커스 가능한 자손은 계속 tab 으로 들어갑니다.
스크린 리더#
-
Web: 숨김 동안
aria-hidden="true"가 서브트리를 접근성 트리에서 제거하고, 보이는 동안에는aria-hidden="false"를 명시적으로 내보냅니다. - Flutter: 숨김은 순수하게 시각 처리라, 투명해지고 접힌 콘텐츠도 계속 읽힙니다.
즉 같은 hidden: true 가 Web 에서는 "사라짐", Flutter 에서는 "여전히 읽힘"으로 갈립니다.
알려진 제약#
- Flutter — 숨긴 콘텐츠가 읽히고 tab 으로 들어갑니다. 키보드 사용자가 보이지 않는 높이 0 영역에 포커스하게 됩니다.
-
Web —
aria-hidden="true"조상 안에 포커스 가능한 자손이 남습니다. 포커스는 이동했는데 리더는 아무것도 보고하지 않는 전형적인 focusable-in-hidden-subtree 위반입니다. -
두 플랫폼 모두
inert/ExcludeFocus에스케이프 해치도, 그것을 켜는 파라미터도 제공하지 않습니다. 숨기는 동안 접근성까지 함께 숨겨야 한다면 호출자가 직접 감싸야 합니다. -
semanticLabel이 만드는 Webaria-label은 role 없는<div>위라 사실상 동작하지 않습니다. aria-hidden은 전환이 시작되는 프레임에 곧바로 뒤집히므로, 페이드가 끝나기 전에 서브트리가 접근성 트리에서 빠집니다.
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | Hidden | Hidden |
| child 타입 | Widget | Component |
| 렌더링 | AnimatedOpacity + _HiddenLayout (custom RenderBox) |
CSS transition: opacity, max-height, max-width |
| pointer 차단 | IgnorePointer(ignoring: hidden) |
pointer-events: none |
| reverse | layout offset 이동 | CSS transform-origin (bottom/right) |
| curve | CoreCubicBezier → Cubic |
같은 제어점 → cubic-bezier(...) timing function |
관련 컴포넌트 (Related Components)#
- Collapsible: 트리거가 있는 접기/펼치기
- Swap: 두 콘텐츠 전환