Sidebar
사이드바 — 앱의 신원 · 검색 · 메뉴 · 계정을 담는 옆 내비게이션
목차
Sidebar 는 화면의 읽는 방향 시작 쪽에 붙는 고정 폭 내비게이션입니다. 위에 앱의 신원(워크스페이스)과 검색, 가운데에 메뉴 행, 아래에 도움말과 로그인한 사람을 둡니다. 사이드바는 영역만 나누고 행을 직접 그리지 않습니다 — 행은 부품 컴포넌트를 조합해 넣습니다.
| 부품 | 역할 |
|---|---|
SidebarItem | 메뉴 한 줄 — 아이콘 · 이름 · 점 · 개수 · 뒤 아이콘 |
SidebarSection | 행 묶음 위의 조용한 제목 |
SidebarGroup | 하위 행을 접고 펴는 행 |
SidebarHeader |
워크스페이스 또는 사람의 신원 행 (plain · card) |
SidebarSearch | 검색을 여는 입력 모양의 버튼, 단축키는 Kbd |
SidebarRailItem | 아이콘 레일의 정사각형 목적지 |
기본 사용법 (Basic Usage)#
Sidebar(
header: SidebarHeader(
leading: const Avatar(initials: 'NA'),
title: const Text('나음 병원'),
subtitle: const Text('본원 · 관리자'),
trailing: const Icon(LucideIcons.chevronsUpDown),
onPressed: () {},
),
search: SidebarSearch(
placeholder: '검색',
shortcut: const ['⌘', 'K'],
onPressed: () {},
),
children: [
SidebarItem(
leading: const Icon(LucideIcons.house),
label: const Text('홈'),
selected: true,
onPressed: () {},
),
const SidebarSection(label: Text('업무')),
SidebarGroup(
leading: const Icon(LucideIcons.folder),
label: const Text('프로젝트'),
children: [
SidebarItem(
level: CoreSidebarItemLevel.nested,
label: const Text('디자인 시스템'),
onPressed: () {},
),
],
),
],
)
Sidebar(
header: SidebarHeader(
leading: Avatar(initials: 'NA'),
title: Text('나음 병원'),
subtitle: Text('본원 · 관리자'),
trailing: Icon(LucideIcons.chevronsUpDown),
onPressed: () {},
),
search: SidebarSearch(
placeholder: '검색',
shortcut: ['⌘', 'K'],
onPressed: () {},
),
children: [
SidebarItem(
leading: Icon(LucideIcons.house),
label: Text('홈'),
selected: true,
onPressed: () {},
),
SidebarSection(label: Text('업무')),
SidebarGroup(
leading: Icon(LucideIcons.folder),
label: Text('프로젝트'),
children: [
SidebarItem(
level: CoreSidebarItemLevel.nested,
label: Text('디자인 시스템'),
onPressed: () {},
),
],
),
],
)
Live Preview#
코드 보기
class SidebarDefaultExample extends StatelessComponent {
const SidebarDefaultExample({super.key});
@override
Component build(BuildContext context) {
// A standard sidebar — identity, search, the rows, help and the person.
return div(classes: 'h-${CoreLayout.scale.layout640}', [
Sidebar(
semanticLabel: '주 메뉴',
header: SidebarHeader(
leading: Avatar(initials: 'NA'),
title: Text('나음 병원'),
subtitle: Text('본원 · 관리자'),
trailing: Icon(LucideIcons.chevronsUpDown),
onPressed: () {},
),
search: SidebarSearch(
placeholder: '검색',
shortcut: ['⌘', 'K'],
onPressed: () {},
),
help: SidebarItem(
leading: Icon(LucideIcons.lifeBuoy),
label: Text('도움말'),
onPressed: () {},
),
user: SidebarHeader(
variant: CoreSidebarHeaderVariant.card,
leading: Avatar(initials: 'KH'),
title: Text('김하나'),
subtitle: Text('hana@example.com'),
),
children: [
SidebarItem(
leading: Icon(LucideIcons.house),
label: Text('홈'),
selected: true,
onPressed: () {},
),
SidebarItem(
leading: Icon(LucideIcons.inbox),
label: Text('받은 편지함'),
dot: true,
onPressed: () {},
),
SidebarSection(label: Text('업무')),
SidebarGroup(
leading: Icon(LucideIcons.folder),
label: Text('프로젝트'),
children: [
SidebarItem(
level: CoreSidebarItemLevel.nested,
label: Text('디자인 시스템'),
onPressed: () {},
),
SidebarItem(
level: CoreSidebarItemLevel.nested,
label: Text('웹사이트'),
onPressed: () {},
),
],
),
SidebarItem(
leading: Icon(LucideIcons.calendar),
label: Text('일정'),
onPressed: () {},
),
],
),
]);
}
}
class SidebarDefaultExample extends StatelessWidget {
const SidebarDefaultExample({super.key});
@override
Widget build(BuildContext context) {
// A standard sidebar — identity, search, the rows, help and the person.
return SizedBox(
height: CoreLayout.layout640,
child: Sidebar(
semanticLabel: '주 메뉴',
header: SidebarHeader(
leading: const Avatar(initials: 'NA'),
title: const Text('나음 병원'),
subtitle: const Text('본원 · 관리자'),
trailing: const Icon(LucideIcons.chevronsUpDown),
onPressed: () {},
),
search: SidebarSearch(
placeholder: '검색',
shortcut: const ['⌘', 'K'],
onPressed: () {},
),
help: SidebarItem(
leading: const Icon(LucideIcons.lifeBuoy),
label: const Text('도움말'),
onPressed: () {},
),
user: const SidebarHeader(
variant: CoreSidebarHeaderVariant.card,
leading: Avatar(initials: 'KH'),
title: Text('김하나'),
subtitle: Text('hana@example.com'),
),
children: [
SidebarItem(
leading: const Icon(LucideIcons.house),
label: const Text('홈'),
selected: true,
onPressed: () {},
),
SidebarItem(
leading: const Icon(LucideIcons.inbox),
label: const Text('받은 편지함'),
dot: true,
onPressed: () {},
),
const SidebarSection(label: Text('업무')),
SidebarGroup(
leading: const Icon(LucideIcons.folder),
label: const Text('프로젝트'),
children: [
SidebarItem(
level: CoreSidebarItemLevel.nested,
label: const Text('디자인 시스템'),
onPressed: () {},
),
SidebarItem(
level: CoreSidebarItemLevel.nested,
label: const Text('웹사이트'),
onPressed: () {},
),
],
),
SidebarItem(
leading: const Icon(LucideIcons.calendar),
label: const Text('일정'),
onPressed: () {},
),
],
),
);
}
}
구조 (Anatomy)#
standard 패널 top header · search
body children (스크롤)
bottom footer · help · user
twoPane 레일 railHeader · railItems · railFooter
패널 top panelTitle · panelAction · search
body children (스크롤)
bottom footer · help · user
collapsed 레일 railHeader · railItems · railFooter
- 패널의 위아래 영역은 고정되고 가운데 본문만 스크롤됩니다.
- 페이지와 맞닿는 끝쪽 경계선은 가장 바깥 열이 그립니다 —
collapsed는 레일, 나머지는 패널.
Figma: CoUI 라이브러리
❖ Sidebar 페이지.
사용 시기 (When to Use)#
- 메뉴가 많고 구역 · 그룹 · 2단 메뉴가 필요한 관리자 · 업무 화면.
- 화면 왼쪽(오른쪽에서 왼쪽으로 쓰는 언어는 오른쪽)에 늘 머무는 내비게이션.
모바일의 아래 탭 바는 NavigationBar 를, 화면 일부의 탭 전환은 Tabs 를 씁니다.
Props / Parameters#
Sidebar#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
type |
CoreSidebarType |
standard |
standard · twoPane · collapsed |
header |
W? |
null |
패널 맨 위 신원 행 — 보통 SidebarHeader |
search |
W? |
null |
검색 트리거 — 보통 SidebarSearch |
panelTitle |
W? |
null |
두 칸 패널의 제목 |
panelAction |
W? |
null |
패널 제목 끝의 동작 |
children |
List<W> |
[] |
패널 본문의 행 |
footer |
W? |
null |
도움말 위의 내용(사용량 카드 같은 것) |
help | W? | null | 도움말 행 |
user | W? | null | 맨 아래 로그인한 사람 |
railHeader |
W? |
null |
레일 맨 위 표식(로고) |
railItems |
List<W> |
[] |
레일의 목적지 — 보통 SidebarRailItem |
railFooter |
W? |
null |
레일 맨 아래(설정 · 아바타) |
semanticLabel |
String? |
null |
내비게이션 랜드마크의 접근성 이름 |
sidebarStyle |
CoreSidebarStyle? |
null |
인스턴스 스타일 |
SidebarItem#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
label | W | 필수 | 목적지 이름 |
leading | W? | null | 이름 앞 아이콘 |
count |
W? |
null |
끝의 개수 — 보통 Badge |
trailing | W? | null | 맨 끝 아이콘 |
dot | bool | false | 안 읽음 점 |
level |
CoreSidebarItemLevel |
top |
top(36) · nested(32) |
selected | bool | false | 지금 있는 곳 |
enabled |
bool? |
null |
null 이면 onPressed 가 있을 때 켜짐 |
onPressed |
VoidCallback? |
null |
눌렀을 때 |
sidebarItemStyle |
CoreSidebarItemStyle? |
null |
인스턴스 스타일 |
나머지 부품#
| 컴포넌트 | 속성 |
|---|---|
SidebarSection |
label (필수) · action · sidebarSectionStyle |
SidebarGroup |
label
(필수) ·
children
(필수) ·
leading
·
expanded
·
initiallyExpanded
(
true
) ·
onExpandedChanged
·
sidebarGroupStyle
|
SidebarHeader |
title
(필수) ·
variant
(
plain
) ·
leading
·
subtitle
·
trailing
·
onPressed
·
sidebarHeaderStyle
|
SidebarSearch |
placeholder
(필수) ·
shortcut
·
onPressed
·
sidebarSearchStyle
|
SidebarRailItem |
icon
(필수) ·
label
(필수) ·
dot
·
selected
·
enabled
·
onPressed
·
sidebarRailItemStyle
|
CoreSidebarStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
backgroundColor |
CoreColor? |
The panel's face. null defers to [defaultBackgroundColor]. |
borderColor |
CoreColor? |
The line on the edge that meets the page. null defers to [defaultBorderColor]. |
borderWidth |
double? |
Width of the edge line (logical px). null defers to [defaultBorderWidth]. |
panelWidth |
double? |
Width of the panel (logical px). null defers to the type's row in [defaultsByType]. |
railWidth |
double? |
Width of the icon rail (logical px). null defers to [defaultRailWidth]. |
railBackgroundColor |
CoreColor? |
The rail's face. null defers to the type's row in [defaultsByType]. |
railPadding |
CoreEdgeInsets? |
The rail's insets. null defers to [defaultRailPadding]. |
railSpacing |
double? |
Spacing between the rail's items. null defers to [defaultRailSpacing]. |
railHeaderGapStyle |
CoreGapStyle? |
Extra room after the rail's header. null defers to [defaultRailHeaderGapStyle]. |
topPadding |
CoreEdgeInsets? |
The top region's insets (header, search, panel title).
null
defers to the type's row in [defaultsByType].
|
topSpacing |
double? |
Spacing between the top region's parts. null defers to [defaultTopSpacing]. |
bodyPadding |
CoreEdgeInsets? |
The body's insets. null defers to the type's row in [defaultsByType]. |
bodySpacing |
double? |
Spacing between the body's rows. null defers to [defaultBodySpacing]. |
bottomPadding |
CoreEdgeInsets? |
The bottom region's insets. null defers to [defaultBottomPadding]. |
bottomSpacing |
double? |
Spacing between the bottom region's parts. null defers to [defaultBottomSpacing]. |
panelTitleStyle |
CoreTextStyle? |
The two-pane panel title's text. null defers to [defaultPanelTitleStyle]. |
panelTitlePadding |
CoreEdgeInsets? |
Insets around the panel title row. null defers to [defaultPanelTitlePadding]. |
panelTitleSpacing |
double? |
Spacing between the panel title and its action.
null
defers to [defaultPanelTitleSpacing].
|
panelActionIconStyle |
CoreIconStyle? |
The panel title's action glyph. null defers to [defaultPanelActionIconStyle]. |
스타일 시스템 (Style System)#
모든 chrome 은 각 컴포넌트의 단일 Style 슬롯으로만 들어갑니다. 사이드바는 틀(면 · 경계 · 폭 · 영역 여백)만, 행은 자기 Style 을 가집니다.
Resolve chain#
CoreSidebarStyle.defaultX → defaultsByType[type]
→ CoreSidebarTheme.style
→ CoreSidebarTheme.variantStyles[type]
→ widget.sidebarStyle
CoreSidebarItemStyle.defaultX → defaultsByLevel[level]
→ CoreSidebarItemTheme.style
→ CoreSidebarItemTheme.variantStyles[level]
→ widget.sidebarItemStyle
테마로 덮어쓰기#
CoreComponentTheme(
sidebar: CoreSidebarTheme(
style: CoreSidebarStyle(panelWidth: CoreLayout.layout280),
),
sidebarItem: CoreSidebarItemTheme(
variantStyles: {
CoreSidebarItemLevel.nested: CoreSidebarItemStyle(
minHeight: CoreSize.size28,
),
},
),
)
변형 (Variants)#
type: standard#
한 패널. 폭 248, 바탕 surfaceContainerLow, 끝쪽 경계 outlineVariant.
type: twoPane#
64 레일 옆에 240 패널. 레일은 한 단 깊은 surfaceContainer 로 칠해져 두 열로 읽힙니다. 패널 위에는 레일에서 고른 영역의 제목(panelTitle)이 옵니다.
type: collapsed#
레일만 남깁니다. 각 목적지의 이름은 툴팁과 접근성 이름이 말합니다.
동작 스펙 (Behavior)#
인터랙션#
-
SidebarItem·SidebarRailItem·SidebarSearch와onPressed가 있는SidebarHeader는 누를 수 있습니다. hover · focus · press 는onSurface상태 레이어(3 %)로 표시합니다. SidebarGroup의 행을 누르면 하위 행이 열리고 닫힙니다.
상태 전환#
-
미선택 행의 글자와 아이콘은
onSurfaceSubtle, 지금 있는 곳은surfaceContainer바탕에onSurface600 입니다. - 비활성 행은
disabledContent로 칠해지고, 선택돼 있어도 선택으로 그리지 않습니다.
애니메이션#
- 행 바탕 전환은
Clickable의 시계를 따릅니다. - 그룹이 열고 닫히는 시간은
CoreDuration.normal(200ms) 이며 동작 줄이기에서 0 이 됩니다.
사용 가이드라인 (Usage Guidelines)#
Do#
- 한 사이드바에서
selected는 한 행에만 둡니다. - 하위 행에는
level: CoreSidebarItemLevel.nested를 줍니다.
Don't#
- 화면 아래 탭 바 자리에 사이드바를 쓰지 않습니다 —
NavigationBar를 씁니다. - 사용량 카드 같은 제품 콘텐츠를 사이드바 부품으로 만들지 않고
footer슬롯에Card·Progress를 조합해 넣습니다.
접근성 (Accessibility)#
역할#
-
Sidebar는 내비게이션 랜드마크입니다 (Web<nav>, Flutter navigation 시맨틱).semanticLabel로 이름을 붙입니다. - 지금 있는 곳은 Web 에서
aria-current="page", Flutter 에서 selected 로 알립니다. -
SidebarGroup의 행은aria-expanded로 열림을 알립니다. 닫힌 하위 행은 Web 에서inert라 키보드와 보조 기술에 닿지 않습니다. SidebarSection은 제목(heading)입니다.
키보드#
- Tab 으로 누를 수 있는 부품을 차례로 지나고, 위 · 아래 화살표로 행 사이를 옮깁니다.
- Enter · Space 로 행을 누릅니다.
스크린 리더#
SidebarRailItem은 보이는 이름이 없어label이 접근성 이름입니다.-
SidebarSearch는placeholder가 접근성 이름이고 단축키는 Web 에서aria-keyshortcuts로 알립니다.
포커스 관리#
포커스 링은 각 행의 모서리를 따릅니다(Clickable).
알려진 제약#
-
미선택 행의
onSurfaceSubtle은 라이트 테마의surfaceContainerLow위에서 4.5 : 1 아래로 내려갑니다.Tabs의 미선택 글자와 같이 받아들인 예외입니다. 지금 있는 곳은onSurface라 이 제약 밖입니다.
전역 축(동작 줄이기 · 고대비 · 색 강제 모드 · 시맨틱 역할 · 최소 터치 타겟)은 접근성 페이지가 한 곳에서 다룹니다.
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 화살표 이동 | Shortcuts → NextFocusIntent / PreviousFocusIntent |
keydown 에서 다음 · 이전 role="button" 에 포커스 |
| 상태 레이어 | 위젯이 hover · focus · press 를 감지해 겹침 색을 고름 |
범위
<style>
규칙이
:hover
·
:focus-visible
·
:active
로 칠함
|
| 그룹 열기 | AnimatedSize | grid-template-rows 전환 |
추가 차이 없음 — 이름 · 기본값 · 순서는 두 플랫폼이 같습니다.
관련 컴포넌트 (Related Components)#
- NavigationBar: 화면 아래 탭 바와 레일
- Tabs: 화면 일부의 탭 전환
- Kbd:
SidebarSearch의 단축키
조합 예제#
Sidebar(
footer: Card(
child: Progress(value: 0.6),
),
children: [...],
)