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

Collapsible

클릭으로 내용을 접거나 펼칠 수 있는 토글 가능한 영역 컴포넌트

목차

트리거를 클릭하여 내용을 접거나 펼칠 수 있는 토글 영역 컴포넌트입니다.

기본 사용법 (Basic Usage)#

// 기본 접기/펼치기
Collapsible(
  trigger: Text('상세 정보'),
  content: Text('여기에 상세 내용이 표시됩니다.'),
)

// 초기 펼침 상태
Collapsible(
  trigger: Text('약관 내용'),
  content: TermsContent(),
  expanded: true,
)

// 외부 제어
Collapsible(
  trigger: Text('필터 옵션'),
  content: FilterPanel(),
  expanded: isFilterExpanded,
  onExpandedChanged: (value) => setState(() => isFilterExpanded = value),
)

Live Preview#

사용 시기 (When to Use)#

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

  • 보조 정보나 상세 내용을 기본적으로 숨겨두고 필요 시 펼쳐 보여줄 때
  • 단일 섹션을 접기/펼치기로 제어할 때 (여러 섹션 관리는 Accordion 사용)
  • 필터 패널, 추가 옵션, 긴 약관 내용 등을 토글로 보여줄 때

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

  • Accordion: 여러 접힘 패널의 상호배타 열림을 하나의 그룹으로 관리할 때
  • Disclosure: Web 정적 렌더에서 native details의 독립 토글이 필요할 때
  • Tree: 계층 구조의 항목·자식을 트리 형태로 표시할 때

Props / Parameters#

W 는 Flutter의 Widget, Web의 Jaspr Component 입니다.

속성타입기본값설명
triggerW필수클릭 가능한 헤더
contentW필수펼쳤을 때 표시할 내용
expanded bool false 초기 상태 및 호출부에서 갱신할 펼침 값
onExpandedChanged void Function(bool)? null 상태 변경 콜백
collapsibleStyle CoreCollapsibleStyle? null chrome 단일 진입점 (아래 표 참고)

CoreCollapsibleStyle 필드#

필드타입설명
contentInset CoreEdgeInsets? Inner inset around the trigger and the content area (pre-scaling).
duration Duration? Expand/collapse animation duration.
triggerTextStyle CoreTextStyle? Trigger row text style override.
contentTextStyle CoreTextStyle? Content body text style override. Layered over the bodyMedium typography role + on-surface colour the widget composes for the expanded content area (single static site, no state branch).
iconLabelGapStyle CoreGapStyle? Horizontal gap slot between the trigger text and the chevron icon. Forwarded straight to Gap(gapStyle: …) (Flutter) and to the gap <div> width (Web).
iconStyle CoreIconStyle? Chevron / disclosure icon style.
clickableStyle CoreClickableStyle? Nested [CoreClickableStyle] slot for the composed trigger Clickable (press scale / focus ring / transition durations). Merged on top of [defaultClickableStyle] and raw-forwarded — the Clickable's own resolver fills the remaining defaults.

빠른 오버라이드 (Chain)#

이미 만든 Collapsible 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.

class CollapsibleChainExample extends StatelessWidget {
  const CollapsibleChainExample({super.key});

  @override
  Widget build(BuildContext context) {
    return const Collapsible(
      trigger: Text('Toggle Content'),
      content: Text(
        'This is the collapsible content that can be expanded or collapsed.',
      ),
      expanded: true,
    ).withStyle(
      const CoreCollapsibleStyle(
        contentInset: CoreEdgeInsets.all(CoreSpace.space24),
        duration: Duration(milliseconds: CoreDuration.moderate),
        triggerTextStyle: CoreTextStyle.token(
          CoreTextStyles.titleSmall,
          color: CoreColor.token(CoreColors.primary),
        ),
        iconLabelGapStyle: CoreGapStyle(size: CoreSpace.space12),
        iconStyle: CoreIconStyle(
          size: CoreSize.size20,
          color: CoreColor.token(CoreColors.primary),
        ),
      ),
    );
  }
}
class CollapsibleChainExample extends StatelessComponent {
  const CollapsibleChainExample({super.key});

  @override
  Component build(BuildContext context) {
    return Collapsible(
      trigger: Text('Toggle Content'),
      content: Text(
        'This is the collapsible content that can be expanded or collapsed.',
      ),
      expanded: true,
    ).withStyle(
      const CoreCollapsibleStyle(
        contentInset: CoreEdgeInsets.all(CoreSpace.space24),
        duration: Duration(milliseconds: CoreDuration.moderate),
        triggerTextStyle: CoreTextStyle.token(
          CoreTextStyles.titleSmall,
          color: CoreColor.token(CoreColors.primary),
        ),
        iconLabelGapStyle: CoreGapStyle(size: CoreSpace.space12),
        iconStyle: CoreIconStyle(
          size: CoreSize.size20,
          color: CoreColor.token(CoreColors.primary),
        ),
      ),
    );
  }
}

변형 (Variants)#

기본 (닫힘 상태로 시작)#

Collapsible(
  trigger: Text('추가 옵션'),
  content: AdditionalOptions(),
)

기본 펼침#

Collapsible(
  trigger: Text('기본 정보'),
  content: UserInfoPanel(),
  expanded: true,
)

동작 스펙 (Behavior)#

인터랙션#

  • 클릭/탭: 트리거 영역 클릭 시 펼침/접힘 토글, onExpandedChanged 호출
  • 호버: 트리거 텍스트 밑줄 표시
  • 포커스: 트리거에 포커스 시 Enter/Space로 토글 가능

상태 전환#

  • onExpandedChanged 가 있으면 다음 값만 콜백으로 요청하고, 부모가 expanded 를 갱신해야 펼침 상태가 바뀝니다. 콜백이 없으면 내부에서 토글합니다. 별도 비활성 플래그는 없습니다.
  • collapsed → expanded: 트리거 클릭 시 내용 영역 펼침
  • expanded → collapsed: 트리거 재클릭 시 내용 영역 접음
  • 화살표 아이콘이 펼침/닫힘 상태에 따라 180도 회전

애니메이션#

  • 높이 전환: 200ms ease-in-out (CoreDuration.normal)
  • 화살표 아이콘 회전: 200ms ease-in-out

사용 가이드라인 (Usage Guidelines)#

✅ Do#

보조적인 내용을 기본 숨김 상태로 시작

Collapsible(
  trigger: Text('고급 설정'),
  content: AdvancedSettingsPanel(),
  // expanded 기본값 false
)

대부분의 사용자에게 불필요한 정보를 숨겨 페이지를 깔끔하게 유지한다.


❌ Don't#

필수 정보를 Collapsible 안에 숨기지 않기

// ❌ 중요한 오류 메시지를 접힌 상태로 숨김
Collapsible(
  trigger: Text('결제 오류'),
  content: Text('카드 정보가 유효하지 않습니다.'),
)

사용자가 중요한 정보를 놓칠 수 있으며, Alert나 Dialog로 즉시 표시해야 한다.

✅ Do#

여러 독립 섹션은 각각 별도 Collapsible로 관리

Column(
  children: [
    Collapsible(
      trigger: Text('기본 정보'),
      content: BasicInfoForm(),
      expanded: isBasicExpanded,
      onExpandedChanged: (v) => setState(() => isBasicExpanded = v),
    ),
    Collapsible(
      trigger: Text('추가 정보'),
      content: ExtraInfoForm(),
      expanded: isExtraExpanded,
      onExpandedChanged: (v) => setState(() => isExtraExpanded = v),
    ),
  ],
)

섹션들이 서로 독립적으로 열리고 닫혀 사용자가 원하는 섹션만 볼 수 있다.


❌ Don't#

너무 짧은 내용을 Collapsible로 감싸지 않기

// ❌ 한 줄 텍스트를 굳이 Collapsible로
Collapsible(
  trigger: Text('작성자'),
  content: Text('홍길동'),
)

짧은 내용은 바로 표시하는 것이 클릭 비용을 줄여 UX를 개선한다.

접근성 (Accessibility)#

역할#

  • Flutter — 트리거는 Clickable 합성이라 Semantics(button: true, enabled: …) 를 받고(clickable.dart:505), 그 안쪽에 Semantics(button: true, expanded: …) 가 한 번 더 얹혀 펼침 상태를 전달합니다(collapsible.dart:280).
  • Web — 트리거는 Clickable 의 <div> 라 role="button" + tabindex="0" 이 붙고(clickable.dart:436), 그 위에 aria-expanded 가 caller attributes 로 실립니다(collapsible.dart:188). 루트 <div> 는 configureAttributes 를 override 하지 않아 호출자가 넘긴 attributes 외에는 role 이 붙지 않습니다(collapsible.dart:164).

양 플랫폼 모두 트리거와 콘텐츠를 잇는 aria-controls 도, 콘텐츠의 region role 도 없습니다.

키보드#

키FlutterWeb
Enter / Space트리거 토글 (펼침/접힘)트리거 토글 (펼침/접힘)
Tab / Shift+Tab 트리거가 tab stop, 펼친 콘텐츠는 그 다음 트리거가 tab stop, 펼친 콘텐츠는 그 다음
Escape 로 접기없음없음

컴포넌트가 바인딩하는 키는 Enter / Space 뿐입니다(clickable.dart:14).

Flutter 에서 화살표로 포커스가 움직이는 것은 프레임워크의 방향성 순회이지 이 컴포넌트의 동작이 아닙니다 — Flutter Web 에서는 같은 키가 스크롤로 갑니다.

스크린 리더#

접근 가능한 이름은 전적으로 trigger 슬롯에 넣은 것에서 옵니다 — 양 플랫폼 모두 trigger 가 필수 슬롯이고 컴포넌트가 이름을 따로 만들지 않습니다. 아이콘만 넣으면 이름 없는 버튼이 되므로, 그때는 호출자가 트리거 쪽에 이름을 붙여야 합니다.

펼침 상태는 트리거에 실려 함께 읽히지만, 토글 결과를 알리는 live region 은 양쪽 다 없습니다.

접힌 콘텐츠는 리더에 노출되지 않습니다. Flutter 는 높이 0 으로 클립되어 시맨틱 트리에 올라가지 않고, Web 은 inert 로 접근성 트리·탭 순서·hit-test 에서 함께 빠집니다(collapsible.dart:234).

포커스 관리#

  • 진입 / 이탈 — 없음. 오버레이가 아니라 인라인 섹션이라 열고 닫을 때 포커스를 옮기지 않습니다.
  • 트랩 — 없음. Tab 은 컴포넌트 밖으로 그대로 빠져나갑니다.
  • 펼친 콘텐츠 안의 포커스 가능 요소는 탭 순서에 들어오고, 접으면 빠집니다. Flutter 는 닫힘 애니메이션이 끝난 시점(_controller.isDismissed)에 ExcludeFocus 로 제외하고(collapsible.dart:203), Web 은 상태가 뒤집히는 즉시 inert 를 붙입니다.
  • 콘텐츠 안에 포커스가 있는 채로 접으면 포커스를 트리거로 되돌리는 코드가 없습니다.

알려진 제약#

  • onExpandedChanged 를 넘기면 컴포넌트가 자기 상태를 바꾸지 않습니다 — 콜백만 호출하고 내부 _isExpanded 는 그대로 두므로(Flutter collapsible.dart:137, Web collapsible.dart:136), 호출자가 expanded: 를 되먹이지 않으면 aria-expanded / Semantics(expanded:) 가 옛 값에 머물고 콘텐츠도 inert 인 채 남습니다. 화면은 열린 것처럼 보이는데 리더에는 닫혔다고 읽히는 상태가 되니, 콜백을 쓸 때는 expanded 를 함께 갱신하세요.
  • 트리거와 콘텐츠에 속성을 실을 진입점이 없습니다 — Web Collapsible(attributes: …) 는 루트 <div> 에만 닿고(collapsible.dart:171), Flutter Collapsible 에는 그런 표면 자체가 없습니다. 위에서 말한 aria-controls 를 소비자가 밖에서 보강할 수 없다는 뜻입니다.

전역 축(동작 줄이기 · 고대비 · 색 강제 모드 · 시맨틱 역할 · 최소 터치 타겟)은 접근성 페이지가 한 곳에서 다룹니다.

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

항목FlutterWeb
클래스명CollapsibleCollapsible
애니메이션 SizeTransition + AnimationController CSS grid-template-rows transition
ARIA Semantics(button:, expanded:) role="button", aria-expanded
화살표 아이콘 AnimatedBuilder + Transform.rotate CSS transform: rotate()
접힌 내용 포커스 제외 닫힘 애니메이션 완료 후 ExcludeFocus 상태 변경 즉시 inert
  • Accordion: 여러 Collapsible 패널을 그룹으로 관리할 때 사용
  • Tree: 계층적 데이터를 중첩 접기/펼치기로 표시할 때 사용