Hidden | CoUI
LogoCoUI

Hidden

자식을 fade + collapse 애니메이션으로 토글하는 래퍼

Hidden#

hidden bool 하나로 자식을 opacity + 축소 애니메이션과 함께 토글합니다. 접히는 축을 direction (vertical/horizontal)으로 지정하고, 공간을 유지하려면 keepSpace: true, 역방향 접힘은 reverse: true.

Live Preview#

Web
Toggleable content
Flutter
Loading Flutter...
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#

속성타입기본값설명
childW필수토글되는 콘텐츠
hidden bool 필수 true면 opacity 0 + 축소, false면 원상태
direction CoreHiddenDirection? vertical vertical (height 접힘) / horizontal (width 접힘)
durationDuration?200ms전환 시간
keepSpace bool? false true면 크기 유지, opacity만 애니메이션
reverse bool? false true면 trailing edge(right/bottom)부터 접힘
curve CoreCubicBezier? CoreEasing.move 전환 easing. 양 플랫폼이 같은 토큰을 읽습니다
semanticLabelString?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-hiddenhidden 값을 그대로 따라 항상 "true" 또는 "false" 로 나가고, data-state 도 함께 나갑니다. semanticLabelaria-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 이 만드는 Web aria-label 은 role 없는 <div> 위라 사실상 동작하지 않습니다.
  • aria-hidden 은 전환이 시작되는 프레임에 곧바로 뒤집히므로, 페이드가 끝나기 전에 서브트리가 접근성 트리에서 빠집니다.

크로스 플랫폼 차이점 (Platform Differences)#

항목FlutterWeb
클래스명HiddenHidden
child 타입WidgetComponent
렌더링 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 CoreCubicBezierCubic 같은 제어점 → cubic-bezier(...) timing function
  • Collapsible: 트리거가 있는 접기/펼치기
  • Swap: 두 콘텐츠 전환