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#
코드 보기
class CollapsibleDefaultExample extends StatelessComponent {
const CollapsibleDefaultExample({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.',
),
);
}
}
class CollapsibleDefaultExample extends StatelessWidget {
const CollapsibleDefaultExample({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.',
),
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 보조 정보나 상세 내용을 기본적으로 숨겨두고 필요 시 펼쳐 보여줄 때
- 단일 섹션을 접기/펼치기로 제어할 때 (여러 섹션 관리는 Accordion 사용)
- 필터 패널, 추가 옵션, 긴 약관 내용 등을 토글로 보여줄 때
대신 다른 컴포넌트를 사용하세요:
- Accordion: 여러 접힘 패널의 상호배타 열림을 하나의 그룹으로 관리할 때
- Disclosure: Web 정적 렌더에서 native details의 독립 토글이 필요할 때
- Tree: 계층 구조의 항목·자식을 트리 형태로 표시할 때
Props / Parameters#
W 는 Flutter의 Widget, Web의 Jaspr Component 입니다.
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
trigger | W | 필수 | 클릭 가능한 헤더 |
content | W | 필수 | 펼쳤을 때 표시할 내용 |
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 도 없습니다.
키보드#
| 키 | Flutter | Web |
|---|---|---|
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는 그대로 두므로(Fluttercollapsible.dart:137, Webcollapsible.dart:136), 호출자가expanded:를 되먹이지 않으면aria-expanded/Semantics(expanded:)가 옛 값에 머물고 콘텐츠도inert인 채 남습니다. 화면은 열린 것처럼 보이는데 리더에는 닫혔다고 읽히는 상태가 되니, 콜백을 쓸 때는expanded를 함께 갱신하세요. -
트리거와 콘텐츠에 속성을 실을 진입점이 없습니다 — Web
Collapsible(attributes: …)는 루트<div>에만 닿고(collapsible.dart:171), FlutterCollapsible에는 그런 표면 자체가 없습니다. 위에서 말한aria-controls를 소비자가 밖에서 보강할 수 없다는 뜻입니다.
전역 축(동작 줄이기 · 고대비 · 색 강제 모드 · 시맨틱 역할 · 최소 터치 타겟)은 접근성 페이지가 한 곳에서 다룹니다.
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | Collapsible | Collapsible |
| 애니메이션 | SizeTransition + AnimationController |
CSS grid-template-rows transition |
| ARIA | Semantics(button:, expanded:) |
role="button", aria-expanded |
| 화살표 아이콘 | AnimatedBuilder + Transform.rotate |
CSS transform: rotate() |
| 접힌 내용 포커스 제외 | 닫힘 애니메이션 완료 후 ExcludeFocus |
상태 변경 즉시 inert |