Drawer | CoUI

Drawer

드로어/사이드바 컴포넌트

Drawer#

화면 측면에서 슬라이드하여 나타나는 모달 패널 컴포넌트입니다. 명령형 API (openCoDrawer) 로 호출하여 표시하고, 사용자가 닫으면 Future 가 결과와 함께 완료됩니다.

Live Preview#

Web
Flutter
Loading Flutter...
class DrawerDefaultExample extends StatelessComponent {
  const DrawerDefaultExample({super.key});

  @override
  Component build(BuildContext context) {
    return CoButton(
      variant: CoreButtonVariant.outline,
      onPressed: () {
        openCoDrawer<void>(
          context: context,
          side: CoreDrawerSide.left,
          builder: (close) => CoDrawer(
            title: 'Navigation',
            content: div(
              [
                const Text('Dashboard'),
                const Text('Settings'),
                const Text('Profile'),
              ],
              classes: 'flex flex-col gap-${CoreSpace.scale.space8}',
            ),
          ),
        );
      },
      child: const Text('Open drawer'),
    );
  }
}
class DrawerDefaultExample extends StatelessWidget {
  const DrawerDefaultExample({super.key});

  @override
  Widget build(BuildContext context) {
    return CoButton(
      variant: CoreButtonVariant.outline,
      onPressed: () {
        openCoDrawer<void>(
          context: context,
          side: CoreDrawerSide.left,
          builder: (ctx) => CoDrawer(
            title: 'Navigation',
            content: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: const [
                Text('Dashboard'),
                Text('Settings'),
                Text('Profile'),
              ],
            ),
          ),
        );
      },
      child: const Text('Open drawer'),
    );
  }
}

사용 시기 (When to Use)#

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

  • 보조 콘텐츠(필터, 설정, 상세 정보)를 화면 옆에서 슬라이드로 보여줄 때
  • 메인 콘텐츠를 가리지 않고 추가 작업 공간이 필요할 때
  • 모바일 네비게이션 메뉴를 구현할 때

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

  • Dialog: 사용자 확인이 필요한 짧은 메시지일 때
  • Popover: 특정 요소에 붙는 작은 팝업일 때
  • Tabs: 콘텐츠를 탭으로 전환하는 것이 더 적합할 때

기본 사용법 (Basic Usage)#

CoUI 는 Flutter 와 Web 에서 동일한 명령형 API 를 제공합니다. 아래 예제 코드는 양쪽 플랫폼에서 그대로 사용할 수 있습니다.

// 트리거 버튼 클릭 시 드로어 열기
CoButton(
  variant: CoreButtonVariant.outline,
  onPressed: () {
    openCoDrawer<void>(
      context: context,
      side: CoreDrawerSide.left,
      builder: (close) => CoDrawer(
        title: 'Navigation',
        content: Column(  // Web 은 div(...)
          crossAxisAlignment: CrossAxisAlignment.start,
          children: const [
            Text('Dashboard'),
            Text('Settings'),
            Text('Profile'),
          ],
        ),
      ),
    );
  },
  child: const Text('Open drawer'),
)

openCoDrawer<T>Future<T?> 를 반환합니다 — 사용자가 패널 안에서 close(value) 를 호출하면 그 값으로, 배리어/드래그/Escape/× 버튼으로 닫으면 null 로 완료됩니다.

// 결과를 받아오는 패턴
final picked = await openCoDrawer<String>(
  context: context,
  side: CoreDrawerSide.right,
  builder: (close) => CoDrawer(
    title: 'Pick a colour',
    content: ColorList(onPick: (c) => close(c.name)),
  ),
);

API#

openCoDrawer<T>(...)#

인자타입기본값설명
context BuildContext 필수 가장 가까운 CoUIWeb / CoUIApp 또는 CoOverlayHost ancestor 를 찾는 데 사용
builder WidgetBuilder (Flutter) / CoDrawer Function(close) (Web) 필수 드로어 panel 을 빌드하는 함수. Web 빌더는 close([T?]) 콜백을 받아 panel 안에서 결과와 함께 닫을 수 있음
side CoreDrawerSide 필수 슬라이드 인 방향 (left / right / top / bottom)

CoDrawer#

속성타입기본값설명
content Widget / Component 필수 드로어 내부에 표시할 콘텐츠
side CoreDrawerSide left 슬라이드 인 방향 (위젯 파라미터)
title String? null 드로어 상단 제목 텍스트
barrierDismissible bool true 배리어(스크림) 클릭 시 드로어 닫기
draggable bool true 가장자리를 50% 이상 드래그하면 닫힘
dismissOnEscape bool true Escape 키로 닫기 (Web 전용)
drawerStyle CoreDrawerStyle? null panel chrome / barrier / animation / nested titleStyle 묶음

스타일 시스템 — drawerStyle (Epic #1302)#

CoDrawer 의 panel chrome / barrier / animation / 슬롯 미세 조정은 단일 drawerStyle (CoreDrawerStyle) 으로 흐릅니다. 시맨틱 enum (side) 과 동작 정책 (barrierDismissible, draggable, dismissOnEscape) 은 위젯 파라미터 그대로.

openCoDrawer(
  context: context,
  side: CoreDrawerSide.right,
  builder: (ctx) => CoDrawer(
    title: 'Settings',
    content: ...,
    drawerStyle: CoreDrawerStyle(
      panelBackgroundColor: cs.surface,
      panelBorderRadius: 16,
      panelWidth: 360,
      barrierColor: cs.scrim.withOpacity(.4),
      openAnimationDuration: Duration(milliseconds: 350),
      closeAnimationDuration: Duration(milliseconds: 200),
      titleStyle: CoreTextStyle(
        fontSize: 18,
        fontWeight: CoreFontWeight.semiBold,
      ),
    ),
  ),
)

CoreDrawerStyle 필드

필드타입설명
panelBackgroundColor / panelBorderColor Color? / String? panel 배경 / 보더 색
panelBorderRadiusdouble?panel 보더 반경
panelWidthdouble?left/right 드로어 너비
panelHeightdouble?top/bottom 드로어 높이
barrierColor Color? / String? scrim 색
openAnimationDuration Duration? 열기 애니메이션 (기본 350ms)
closeAnimationDurationDuration?닫기 애니메이션
titleStyleCoreTextStyle?제목 텍스트

Resolve chain

design system default
  → CoreDrawerTheme.style                       // 프로젝트 공통
  → 부모 컴포넌트 슬롯 오버라이드
  → widget.drawerStyle                          // 인스턴스별

closeCoDrawer<T>(context, [result]) (Flutter)#

명령형으로 가장 위 드로어를 닫습니다 (Navigator.pop 과 동일한 의미). Web 은 builderclose([T?]) 콜백을 사용하세요.

변형 (Variants)#

위치 (Side)#

openCoDrawer(side: CoreDrawerSide.left, ...)    // 좌측
openCoDrawer(side: CoreDrawerSide.right, ...)   // 우측
openCoDrawer(side: CoreDrawerSide.top, ...)     // 상단
openCoDrawer(side: CoreDrawerSide.bottom, ...)  // 하단

좌/우 드로어는 기본 너비 256 px (CoreDrawerTokens.defaultWidth), 상/하 드로어는 기본 높이 192 px (CoreDrawerTokens.defaultHeight).

동작 스펙 (Behavior)#

열기/닫기#

CoDrawer명령형(imperative) 컴포넌트 입니다 (shadcn_flutter / shadcn-ui 와 동일한 모델). 상태 boolean 으로 제어하지 않고, openCoDrawer 를 호출해 표시하고 다음 중 하나로 닫힙니다:

  1. 배리어(스크림) 탭 — barrierDismissible: true (default)
  2. 드로어 가장자리 드래그 — draggable: true (default), 50 % threshold
  3. Escape 키 — dismissOnEscape: true (default, Web)
  4. × 버튼 클릭 — title 이 있을 때 헤더에 표시
  5. 빌더의 close([result]) 콜백 (Web) / closeCoDrawer(context, result) (Flutter)

애니메이션#

  • 슬라이드: 350 ms ease-out (열기) / ease-out-cubic (닫기) — CoreDrawerTokens.animationDurationMs
  • 배리어 페이드: 350 ms ease-out (양쪽 동일)
  • 드래그 중: transition 일시 정지 (panel 이 손가락을 즉시 따라감)

레이아웃#

  • 좌/우 드로어: 너비 CoreDrawerTokens.defaultWidth (256 px) × 화면 전체 높이
  • 상/하 드로어: 화면 전체 너비 × 높이 CoreDrawerTokens.defaultHeight (192 px)
  • CoreDrawerTheme.width / .height 로 오버라이드 가능

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

✅ Do#

드로어에 명확한 제목과 닫기 방법을 제공하세요.

openCoDrawer(
  context: context,
  side: CoreDrawerSide.right,
  builder: (close) => CoDrawer(
    title: '필터 설정',
    content: FilterPanel(onApply: (f) => close(f)),
  ),
)

title 이 있으면 자동으로 헤더 + × 버튼이 그려집니다. 사용자가 어떤 패널인지 파악하고 쉽게 닫을 수 있습니다.


❌ Don't#

짧은 확인 메시지에 Drawer 를 사용하지 마세요.

openCoDrawer(
  context: context,
  side: CoreDrawerSide.bottom,
  builder: (close) => CoDrawer(
    content: Text('정말 삭제하시겠습니까?'),
  ),
)

간단한 확인은 CoDialog 가 더 적합합니다. CoDrawer 는 복잡한 콘텐츠/네비게이션용입니다.

✅ Do#

콘텐츠에 맞는 side 를 선택하세요.

  • 네비게이션 메뉴 → left
  • 보조 상세 / 필터 패널 → right
  • 모바일 시트 / 액션 → bottom
  • 알림 / 이벤트 시트 → top

사용자 기대에 맞는 방향이어야 직관적입니다.

접근성 (Accessibility)#

키보드 인터랙션#

동작
Escape드로어 닫기 (dismissOnEscape: true 일 때)
Tab드로어 내 요소 간 포커스 이동
Shift+Tab이전 요소로 포커스 이동

시맨틱#

  • Flutter: Navigator.push 로 별도 라우트 — 시스템 백 버튼 / PopScope 가 자동 처리. Semantics(container: true, label: title)
  • Web: role="dialog" + aria-modal="true" + aria-label={title}

닫기 접근성#

  • × 버튼 (role="button" + aria-label="Close") 이 헤더에 표시되어 키보드/스크린 리더 접근 가능
  • 배리어 탭 + 드래그 + Escape 키 — 다양한 dismiss 경로 제공

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

CoDrawer 는 Flutter / Web 에서 동일한 명령형 API (openCoDrawer<T>, CoDrawer panel) 를 공유합니다. 아래는 플랫폼 내부 구현 차이만 나열합니다.

항목FlutterWeb
오버레이 마운트 Navigator.push(_CoDrawerRoute<T>) CoOverlayHost.dialog 레이어 portal
애니메이션 SlideTransition + Tween<Offset> 350 ms Curves.easeOut CSS transform: translateX 350 ms ease-out
배리어 PopupRoute.barrierColor <div class="absolute inset-0"> + click handler
드래그 dismiss GestureDetector + onPanUpdate/End mousedown/move/up listener
닫기 API closeCoDrawer(context, result) 또는 Navigator.pop(ctx, result) builder 의 close([result]) 콜백
시스템 backPopScope 자동(없음)
  • Dialog: 모달 대화상자. 짧은 확인/입력에 적합 (Drawer 는 복잡한 콘텐츠용)
  • Navigation: 네비게이션 컴포넌트. Drawer 안에 배치하여 모바일 메뉴 구현
  • Popover: 요소에 붙는 팝업. Drawer 보다 작고 특정 컨텍스트에 연결

조합 예제#

// 모바일 필터 드로어 패턴
final filter = await openCoDrawer<FilterValue>(
  context: context,
  side: CoreDrawerSide.right,
  builder: (close) => CoDrawer(
    title: '필터',
    content: FilterPanel(
      onApply: (value) => close(value),
    ),
  ),
);
if (filter != null) applyFilter(filter);