Skip to main content
ENKO

내비게이션

페이지와 섹션 사이를 오가는 컴포넌트.

Senco는 내비게이션을 두 단계까지만 둔다(Design principles 참고). 그래서 이 묶음은 일부러 작게 유지한다. 아래 컴포넌트가 다루는 이동은 세 가지뿐이다. 최상위 영역 사이, 한 방향으로 진행하는 플로우 안, 그리고 한 화면 안. 이 밖의 이동은 필요 없다. 위계가 세 번째 단계까지 필요해 보이면 내비게이션이 아니라 정보를 묶는 방식의 문제로 본다.

컴포넌트

ComponentPurposeLandmark
Tabs한 화면 안에서 콘텐츠 섹션을 바꾼다—
Bottom tab bar (iOS app mobile)화면 하단의 주요 모바일 내비게이션<nav>
Stepper여러 단계 플로우의 진행을 보여 준다—
Breadcrumbs페이지 위계에서 사용자가 어디에 있는지 보여 준다<nav>
Menubar드롭다운 메뉴를 가로로 늘어놓은 바—
Side navigation사이드바에 두는 섹션 단위 내비게이션<nav>
Tree view중첩 구조를 펼치고 접는 목록—
Pagination결과 목록의 페이지를 넘긴다<nav>
Sheet화면 가장자리 어디서든 밀려 나오는 패널—

무엇을 고를까

NeedUse
같은 페이지, 같은 데이터에서 보기만 바꾼다Tabs
앱의 최상위 영역 사이를 오간다Bottom tab bar (모바일) / Side navigation (데스크톱)
순서가 정해져 있고 되돌릴 수 없는 플로우를 진행한다Stepper
사용자가 따라 내려온 위계에서 현재 위치를 보여 준다Breadcrumbs
긴 결과 목록을 페이지 단위로 넘긴다Pagination
페이지를 떠나지 않고 보조 콘텐츠를 보여 준다Sheet

Tabs와 Stepper는 생김새는 비슷하지만 뜻은 정반대다. Tabs는 순서가 없어서 어느 탭이든 언제든 열 수 있다. Stepper는 순서가 있어서 앞 단계를 끝내야 다음 단계로 갈 수 있다. 송금 플로우를 탭으로 만들면, 금액을 넣기 전에 확인 단계로 가도 된다고 말하는 셈이 된다.

Tabs

페이지를 이동하지 않고 콘텐츠 섹션을 바꾼다. 전체 폭 레이아웃이고, 활성 탭에 밑줄을 긋는다.

  • 높이 48px로 최소 터치 타깃을 충족한다
  • 화살표 키로 이동할 수 있다
  • 여기 보이는 밑줄 스타일은 variant="line", size="lg"로 만든다

구조

┌──────────┬──────────┬──────────┐
│  Recent  │  Sent    │ Received │
├══════════┴──────────┴──────────┤   active: 2px --ds-primary underline
│                                │
│  panel content                 │
└────────────────────────────────┘

변형

VariantDimensionUse
Linestyle기본. 활성 탭에 밑줄
Scrollablelayout탭이 다섯 개 이상일 때. 한 줄로 스크롤하고, 두 줄로 넘기지 않는다

API

interface TabsProps {
  label: string;                    // accessible name for the tab list
  tabs: Array<{ id: string; label: string; disabled?: boolean }>;
  value: string;
  onValueChange: (id: string) => void;
  syncToUrl?: boolean;              // default: true — a tab state should be shareable
  children: React.ReactNode;        // the active panel
}

Tabs는 <nav>가 아니라 role="tablist"로 렌더링한다. 보기를 바꿀 뿐, 페이지를 이동하지 않기 때문이다.

상태

StateVisual
Active--ds-primary 라벨, 굵기 600, 2px 밑줄
Inactive--ds-muted 라벨, 굵기 500, 밑줄 없음
Hover--ds-heading 라벨, --ds-surface 배경
Focus-visible탭에 포커스 링, 밑줄은 그대로
Disabled--ds-disabled-text, 화살표 키 이동에서 건너뜀

동작

  • 탭 콘텐츠는 처음 열 때 불러오고, 그 뒤로는 마운트된 채 둔다. 다시 돌아와도 데이터를 새로 가져오지 않는다.
  • 탭이 다섯 개 이상이면 줄을 가로로 스크롤하고, 활성 탭이 화면에 보이게 맞춘다. 두 줄로 넘기지 않는다.
  • 활성 탭을 URL에 반영해서, 탭 상태를 공유하거나 복원할 수 있게 한다.

콘텐츠

탭 라벨은 한두 단어로, 형태를 맞추고, 액션이 아니라 콘텐츠를 가리키는 이름을 쓴다.

접근성

  • 탭 목록 전체가 Tab 정지점 하나다. 탭 사이는 화살표 키로 이동한다.
  • role="tablist" / role="tab" / role="tabpanel"을 쓰고, aria-controls와 aria-selected를 단다.
  • 패널에 tabindex="0"을 줘서, 키보드 사용자가 탭을 고른 뒤 패널을 스크롤할 수 있게 한다.
  • Tabs는 <nav>로 감싸지 않는다. 보기를 바꿀 뿐, 페이지를 이동하지 않는다.
KeyAction
← →이전 / 다음 탭 (끝에 닿으면 처음으로)
Home / End첫 / 마지막 탭
Tab탭 목록에서 패널로 이동

토큰

--ds-primary · --ds-muted · --ds-surface · --ds-border · --ds-focus-ring

Tabs
Sent transfers shown here.

Bottom tab bar (iOS app mobile)

화면 하단에 고정한다. 스크롤하는 동안에도 항상 보인다.

  • 탭은 최대 4–5개. 그보다 많으면 너무 빽빽하다
  • 아이콘에는 항상 라벨을 붙인다. 아이콘만으로는 뜻이 충분히 전달되지 않는다.
  • 알림 배지(숫자 또는 점)를 지원한다

구조

PartTokenValue
바 높이--ds-size-bottomnav52px
배경--ds-bottomtab-bg--ds-white
위 테두리--ds-bottomtab-border--ds-gray-200
활성 아이콘 + 라벨--ds-bottomtab-active--ds-accent-600
비활성 아이콘 + 라벨--ds-bottomtab-inactive--ds-gray-500
배지 배경 / 텍스트--ds-bottomtab-badge-bg / -text--ds-red-600 / 흰색
아이콘 크기--ds-size-icon20px
라벨 크기--ds-text-2xs10px

변형

VariantDimensionUse
Four itemslayout기본
Five itemslayout최대치. 여섯 번째 목적지가 필요하다면 정보 구조가 잘못된 것이다

API

interface BottomTabBarProps {
  items: Array<{
    id: string;
    label: string;                  // one word — 10px leaves no room for two
    icon: React.ReactNode;
    href: string;
    badge?: number | "dot";
  }>;                               // 4–5 items
  activeId: string;
  onActiveReselect?: (id: string) => void;  // tapping the active tab scrolls its view to top
}

배지는 숫자만 따로 읽히게 두지 않고, 항목의 접근 가능한 이름에 합친다("Activity, 3 new").

상태

StateRendering
Inactive--ds-bottomtab-inactive 아이콘과 라벨
Active--ds-bottomtab-active, aria-current="page"
With badge--ds-bottomtab-badge-bg 위에 숫자나 점. 접근 가능한 이름에 합친다
Active reselected그 화면을 맨 위로 스크롤한다. 페이지 이동은 없다

동작

  • 바는 env(safe-area-inset-bottom) 위에 둔다. inset을 빼먹으면 가장 오른쪽 탭이 홈 인디케이터 아래로 들어가, 탭 입력의 상당수를 OS가 가로챈다.
  • 바는 sticky가 아니라 fixed다. 스크롤해도 숨기지 않는다. 긴 거래 목록을 내려 본 사용자가 다른 곳으로 가려고 다시 위로 올라가게 만들면 안 된다.
  • 활성 탭을 다시 누르면 그 화면이 맨 위로 스크롤된다.
  • 바는 라벨까지 52px이지만 각 탭의 터치 영역은 48px 높이다. 타깃은 바 높이 전체에 걸친다.

콘텐츠

라벨은 한 단어로, 액션이 아니라 장소를 가리킨다. "Home", "Send", "Activity", "Profile" 같은 식이다. 10px 라벨에는 두 단어가 들어갈 자리가 없고, 그렇다고 아이콘만으로는 부족하다. 아이콘만 있는 하단 내비게이션은 사용성 테스트에서 번번이 잘못 읽히고, 특히 "Activity"와 "Home"을 헷갈린다.

접근성

  • <nav aria-label="Main">으로 감싼다.
  • 활성 탭에 aria-current="page"를 단다.
  • 배지는 별도 요소로 읽히지 않고, 탭의 접근 가능한 이름에 포함해 읽힌다("Activity, 3 new").
  • 화면에 보이는 라벨이 그대로 접근 가능한 이름이 된다. 라벨과 다른 내용의 aria-label을 붙이지 않는다.

키보드

터치용 UI다. 키보드를 연결하면 각 항목은 탭 순서에 들어가는 링크가 된다. 바는 복합 컨트롤이 아니라 목적지 모음이라서 화살표 키 이동은 없다.

KeyAction
Tab항목마다 정지점이 하나씩
Enter이동

토큰

--ds-bottomtab-bg · --ds-bottomtab-border · --ds-bottomtab-active · --ds-bottomtab-inactive · --ds-bottomtab-badge-bg · --ds-bottomtab-badge-text · --ds-size-bottomnav · --ds-size-icon · --ds-text-2xs

Bottom tab bar
Home screen

Stepper

여러 단계로 된 플로우에서 어디까지 왔는지 보여 준다. 변형은 세 가지다. 긴 플로우(4단계 이상)에는 circle, 짧은 플로우(2–3단계)에는 line, 시각을 함께 보여 주는 타임라인에는 vertical line을 쓴다.

구조

단계마다 점, 연결선, 라벨, 시각(선택)이 들어간다. 완료된 점은 체크가 든 24px, 현재 단계와 남은 단계의 점은 12px이다.

변형

VariantStepsUse
Circle4+인증, 온보딩
Line2–3송금 플로우
Vertical line제한 없음시각을 함께 보여 주는 상태 타임라인

API

interface StepperProps {
  variant?: "circle" | "line" | "vertical";  // default: "line" for 2–3 steps, "circle" for 4+
  steps: Array<{ id: string; label: string; timestamp?: Date }>;
  currentIndex: number;
  completedIndices: number[];
  errorIndex?: number;
  onStepSelect?: (index: number) => void;    // completed steps only
}

상태

Step stateVisualAnnounced as
Completed채운 원, 체크 아이콘"Step 1, Amount, completed"
Current--ds-primary로 채운 원, 번호"Step 2, Recipient, current step"
Upcoming--ds-gray-300 외곽선, 번호"Step 3, Confirm"
Error--ds-red-600, 경고 아이콘"Step 2, Recipient, needs attention"

동작

  • 완료된 단계는 누를 수 있고, 입력한 데이터가 그대로 남은 채 그 단계로 돌아간다. 남은 단계는 누를 수 없다.
  • 뒤로 가도 진행 상황은 사라지지 않는다. 3단계에서 1단계로 돌아가도 2단계 입력은 그대로 남아 있다.
  • 모바일에서 스테퍼는 플로우 맨 위, 스크롤 영역 바깥에 항상 보인다.

콘텐츠

단계 라벨은 화면이 아니라 할 일을 가리키는 한두 단어로 쓴다. "Amount", "Recipient", "Confirm" 같은 식이다.

접근성

  • 순서 있는 목록으로 렌더링해서, 전체 개수와 현재 위치가 마크업 구조로 전달되게 한다.
  • 현재 단계에 aria-current="step"을 단다.
  • 상태는 색만으로 전하지 않고 아이콘과 텍스트를 함께 쓴다. 완료된 단계는 색만 채우지 않고 체크도 넣는다.
  • 남은 단계는 탭 순서에서 빼지 않고 aria-disabled="true"로 표시한다.

키보드

KeyAction
Tab스테퍼 전체가 정지점 하나
← →이동 가능한 단계 사이 이동
Enter포커스된 단계로 이동 (완료된 단계만)

토큰

--ds-timeline-completed-dot · --ds-timeline-active-dot · --ds-timeline-future-dot · --ds-timeline-line-completed · --ds-red-600

Circle
✓
Account
✓
Details
3
Review
4
Confirm
Line (horizontal)
Line (vertical)

페이지 위계에서 사용자가 어디에 있는지 보여 준다. 마지막 항목은 현재 페이지라서 링크가 아니라 일반 텍스트로 둔다.

  • 경로가 깊어지면 모바일에서는 말줄임표로 접는다
  • 항목 사이에 구분자(화살표 또는 슬래시)를 둔다

구조

aria-hidden 화살표로 구분한 링크가 한 줄로 이어지고, 맨 끝은 현재 페이지 이름을 일반 텍스트로 둔다.

변형

VariantDimensionUse
Fulllayout세 단계 이하
Collapsedlayout가운데 단계를 펼침 컨트롤 안에 접어 둔다

API

interface BreadcrumbsProps {
  items: Array<{ label: string; href?: string }>;  // the last item omits href — it is the current page
  collapseAfter?: number;           // default: 3 — collapses the middle into an expandable button
}

구분자는 장식이므로 aria-hidden이다. 스크린 리더가 "Home slash Settings"라고 읽는다면 문장 부호를 내용으로 읽고 있는 것이다.

상태

StateRendering
Full모든 단계를 보여 준다
Collapsed--ds-bp-mobile 미만이거나 collapseAfter를 넘으면 가운데 단계를 펼침 버튼 안에 접어 둔다
Current마지막 항목, 일반 텍스트, aria-current="page"

동작

내비게이션을 두 단계까지만 두므로, 브레드크럼은 제품보다 주로 문서 사이트에서 쓴다. 앱에서는 App bar의 뒤로 가기 버튼이 더 적은 UI로 같은 역할을 한다.

구분자는 장식이므로 aria-hidden이다. 스크린 리더가 "Home slash Settings slash Security"라고 읽는다면 문장 부호를 내용으로 읽고 있는 것이다.

콘텐츠

각 항목에는 목적지 페이지의 제목을 그대로 쓴다. 페이지 제목을 다르게 바꿔 쓰면 경로를 믿을 수 없게 된다.

접근성

  • <nav aria-label="Breadcrumb">으로 감싸고, 안에 순서 있는 목록을 둔다.
  • 현재 페이지에는 aria-current="page"를 달고 링크로 만들지 않는다.
  • 접힌 말줄임표는 그냥 문자가 아니라, 누르면 전체 경로를 펼치는 버튼이다.

키보드

KeyAction
Tab링크와 접기 컨트롤을 차례로 이동
Enter링크로 이동하거나, 접힌 구간을 펼친다

토큰

--ds-link · --ds-muted · --ds-heading · --ds-text-sm

Breadcrumb

드롭다운 메뉴를 가로로 늘어놓은 줄로, 보통 화면 맨 위에 둔다. 각 항목을 누르면 관련 액션을 모은 메뉴가 열린다.

  • 활성 메뉴는 Primary 색에 은은한 배경 색을 깐다
  • 메뉴 안에 체크 항목과 라디오 그룹을 넣을 수 있다

구조

메뉴 트리거를 가로로 늘어놓는다. 트리거마다 항목 패널이 열리고, 패널에는 구분선, 체크박스 항목, 라디오 그룹이 들어갈 수 있다.

변형

하나뿐이다. 데스크톱용 UI이고, 터치 환경에서는 같은 액션을 Sheet로 연다.

API

interface MenubarProps {
  label: string;
  menus: Array<{
    id: string;
    label: string;
    items: Array<
      | { type: "action"; label: string; onSelect: () => void; disabled?: boolean }
      | { type: "checkbox"; label: string; checked: boolean; onChange: (v: boolean) => void }
      | { type: "radio"; label: string; value: string; groupId: string }
      | { type: "separator" }
    >;
  }>;
}

데스크톱 패턴이다. 터치 환경에서는 같은 액션을 Sheet에 담아 보여 준다.

상태

StateRendering
Closed모든 메뉴가 접혀 있다
Open메뉴가 하나만 열린다. 하나를 열면 나머지는 닫힌다
Active item--ds-primary에 옅은 색 배경

동작

메뉴 하나를 열면 다른 메뉴는 닫힌다. 메뉴가 열린 상태에서 포인터를 다른 트리거로 옮기면 다시 클릭하지 않아도 그 메뉴로 바뀐다.

콘텐츠

트리거 라벨은 한 단어로 쓴다. 항목 라벨은 동사로 시작한다.

접근성

  • role="menubar" 안에 role="menuitem" 자식을 둔다. 바 전체가 Tab 정지점 하나다.
  • ← →로 메뉴 사이를 이동하고, ↓로 메뉴를 열고, Esc로 메뉴를 닫으면 포커스가 트리거로 돌아간다.
  • 체크할 수 있는 항목은 role="menuitemcheckbox", 하나만 고르는 선택지는 role="menuitemradio"를 쓴다.

Menubar는 데스크톱 패턴이다. 모바일 제품에서는 쓰지 않고, 같은 액션을 시트에 담아 보여 준다.

키보드

KeyAction
Tab메뉴 바 전체가 정지점 하나
← →메뉴 사이 이동
↓포커스된 메뉴 열기
↑ ↓열린 메뉴 안에서 이동
Esc닫고 트리거로 포커스를 돌려준다

토큰

--ds-primary · --ds-primary-light · --ds-white · --ds-border · --ds-shadow-lg · --ds-z-dropdown

Menubar

Side navigation

페이지나 앱 안의 섹션을 오가는 사이드바 내비게이션. 활성 항목 왼쪽에 Primary 색 테두리를 둔다.

  • 들여쓰기로 중첩 섹션을 표현한다
  • 데스크톱에서는 고정하고, 모바일에서는 접는다

구조

섹션을 세로로 쌓는다. 섹션마다 제목과 항목이 있다. 활성 항목에는 --ds-sidebar-active 색으로 왼쪽 테두리를 단다.

변형

VariantDimensionUse
Fixedlayout1024px 이상
Collapsedlayout1024px 미만. Sheet로 연다

API

interface SideNavProps {
  label: string;                    // distinct from the main navigation landmark
  sections: Array<{
    title: string;
    items: Array<{ label: string; href: string }>;
  }>;
  activeHref: string;
  collapsedBelow?: number;          // default: 1024 — becomes a Sheet below this width
}

상태

StateVisualToken
Default--ds-fg 라벨--ds-sidebar-bg
Hover--ds-sidebar-hover 배경—
Active--ds-sidebar-active 왼쪽 테두리와 라벨, 옅은 색 배경--ds-sidebar-active-bg
Focus-visible안쪽으로 3px 실선 외곽선--ds-focus

동작

  • 1024px 미만에서는 사이드바를 트리거 버튼 안으로 접고, 누르면 시트로 연다.
  • 페이지를 이동해도 활성 섹션은 펼친 채 두고, 다른 섹션은 각자의 열림/닫힘 상태를 유지한다.
  • 사이드바 안의 스크롤 위치는 페이지를 바꿔도 유지한다.

콘텐츠

섹션 제목은 명사로 쓴다. 항목 라벨은 목적지 페이지의 제목과 똑같이 쓴다.

접근성

  • <nav aria-label="Sections">로 감싸서 주요 내비게이션 랜드마크와 구분한다.
  • 활성 항목에 aria-current="page"를 단다.
  • 중첩 그룹에는 tree 역할을 중첩하지 않고, aria-expanded를 쓰는 펼침 패턴을 쓴다.

키보드

KeyAction
Tab화면에 보이는 순서대로 항목을 이동
Enter링크로 이동
EscSheet로 열려 있을 때 닫는다

토큰

--ds-sidebar-bg · --ds-sidebar-border · --ds-sidebar-active · --ds-sidebar-active-bg · --ds-sidebar-hover

Tree view

펼치고 접을 수 있는 중첩 목록. 파일 탐색기, 계정 구조, 범주 트리에 알맞다.

  • 화살표 아이콘으로 펼칠 수 있는 노드를 표시한다
  • 들여쓰기로 깊이를 보여 준다

구조

깊이에 따라 들여쓰기가 늘어나는 행으로 이뤄진다. 펼칠 수 있는 행은 inline-start에 화살표를 단다.

변형

VariantDimensionUse
Flatlayout한 단계, 화살표 없음
Nestedlayout펼치고 접을 수 있는 여러 단계

API

interface TreeNode {
  id: string;
  label: string;
  children?: TreeNode[];
}

interface TreeViewProps {
  label: string;
  nodes: TreeNode[];
  expandedIds: string[];
  onExpandedChange: (ids: string[]) => void;
  selectedId?: string;
  onSelect?: (id: string) => void;
}

깊이는 들여쓰기가 아니라 aria-level로 전한다. 스크린 리더는 들여쓰기를 볼 수 없다.

상태

StateRendering
Collapsedinline-start 화살표, aria-expanded="false"
Expandedaria-expanded="true"
Leaf화살표 없음, aria-expanded 없음
Selectedaria-selected="true"

동작

펼침 상태는 controlled로 관리하고, 다시 렌더링해도 유지한다. 부모를 선택해도 자식은 선택되지 않는다.

콘텐츠

노드 라벨은 명사로 쓴다. 부모와의 관계는 깊이로 드러나므로 라벨에 부모 이름을 반복하지 않는다.

접근성

  • role="tree" / role="treeitem"을 쓰고, 펼칠 수 있는 노드에는 aria-expanded를, 모든 항목에는 aria-level을 단다.
  • Tab 정지점은 하나다. ↑ ↓로 보이는 노드 사이를 이동하고, →로 펼치거나 한 단계 내려가고, ←로 접거나 한 단계 올라간다.
  • 들여쓰기는 눈으로만 보인다. 스크린 리더는 들여쓰기를 알 수 없으니 깊이는 aria-level로 전해야 한다.

Tree view는 Senco 제품에서 쓰지 않는다. 문서 사이트와, 나중에 생길 수 있는 관리자 화면용이다. 내비게이션을 두 단계까지만 두는 원칙 때문에 앱에는 들이지 않는다.

키보드

KeyAction
Tab트리 전체가 정지점 하나
↑ ↓보이는 노드 사이 이동
→펼치거나, 첫 자식으로 이동
←접거나, 부모로 이동
Enter선택

토큰

--ds-fg · --ds-surface · --ds-primary-light · --ds-focus-ring · --ds-size-icon-sm

Tree view
Passport.pdf
Proof of address.pdf

Pagination

긴 결과 목록을 페이지 단위로 넘긴다. 양 끝에 이전/다음 버튼을 두고, 페이지가 많으면 중간을 말줄임표로 줄인다.

구조

이전 버튼, 말줄임표로 줄인 페이지 번호 구간, 다음 버튼으로 이뤄진다.

변형

VariantDimensionUse
Numberedlayout포인터 위주 화면, 전체 페이지 수를 알 때
Prev/Next onlylayout전체 개수를 모를 때

API

interface PaginationProps {
  page: number;                     // 1-based
  pageCount: number;
  onPageChange: (page: number) => void;
  siblingCount?: number;            // default: 1
}

페이지 컨트롤의 접근 가능한 이름은 "3"이 아니라 "Page 3"이다. 첫 페이지에서 이전 버튼은 숨기지 않고 비활성으로 둔다. 그래야 컨트롤 줄이 옆으로 밀리지 않는다.

상태

StateRendering
First page이전 버튼은 숨기지 않고 비활성. 컨트롤 줄이 밀리면 안 된다
Last page다음 버튼은 비활성
Current링크가 아니라 텍스트, aria-current="page"
Truncated말줄임표는 aria-hidden. 건너뛴 범위는 앞뒤 링크 이름으로 알 수 있다

동작

  • 거래 내역은 모바일에서는 무한 스크롤, 데스크톱에서는 페이지네이션을 쓴다. 페이지네이션이면 키보드 사용자도 페이지 끝에 닿을 수 있지만, 무한 스크롤은 그렇지 않다.
  • 첫 페이지에서 이전 버튼은 숨기지 않고 비활성으로 둬서 컨트롤 줄이 밀리지 않게 한다.
  • 현재 페이지는 링크가 아니라 텍스트로 보여 준다.

콘텐츠

컨트롤 이름은 "3"이 아니라 "Page 3"으로 쓴다. 현재 위치는 "Page 3 of 10"처럼 적는다.

접근성

  • <nav aria-label="Pagination">.
  • 현재 페이지에 aria-current="page"를 단다.
  • 각 페이지 링크의 접근 가능한 이름은 "3"이 아니라 "Page 3"이다.
  • 말줄임표는 aria-hidden이다. 건너뛴 범위는 앞뒤 링크의 접근 가능한 이름으로 알 수 있다.

키보드

KeyAction
Tab컨트롤을 차례로 이동
Enter그 페이지로 이동

토큰

--ds-primary · --ds-muted · --ds-border · --ds-disabled-text · --ds-focus-ring

Sheet

화면 가장자리 어디서든 밀려 나오는 패널. 바텀 시트는 Senco 모바일의 핵심 패턴으로, 내비게이션 서랍, 상세 패널, 필터에 쓴다.

  • 배경을 탭하면 시트가 닫힌다
  • 크기: sm, default, lg, full

구조

배경, 화면 안쪽을 향한 가장자리에 radius를 준 패널, 제목과 닫기 컨트롤이 있는 헤더, 스크롤되는 본문, 푸터(선택)로 이뤄진다.

변형

VariantDimensionUse
Bottomlayout모바일 기본
Start / endlayout데스크톱 사이드 패널
Fulllayout화면을 꽉 채우는 작업

API

interface SheetProps {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  side?: "bottom" | "start" | "end";  // default: "bottom"
  size?: "sm" | "default" | "lg" | "full";  // default: "default"
  title: string;                    // required — a modal surface needs an accessible name
  children: React.ReactNode;
}

시트는 겹쳐 띄우지 않는다. 두 번째 시트를 열면 첫 번째를 대신한다. 스와이프로만 닫게 하지 않는다. 배경 탭, Escape, 눈에 보이는 닫기 컨트롤이 모두 동작해야 한다(SC 2.5.7).

상태

StateRendering
Closed렌더링하지 않는다
Entering--ds-duration-slow, --ds-ease-out
Open포커스를 가두고, 뒤의 콘텐츠는 inert, body 스크롤은 잠긴다
Dragging바텀 시트만. 포인터를 따라 움직이고, 닫힘 기준에 못 미치면 제자리로 돌아간다
Exiting--ds-duration-normal, --ds-ease-in

동작

PropertyValue
등장--ds-duration-slow (300ms), --ds-ease-out
퇴장--ds-duration-normal (180ms), --ds-ease-in
Radius화면 안쪽을 향한 가장자리에만 --ds-radius-2xl
배경--ds-overlay
Z-index--ds-z-modal
SizeBottom sheet heightSide sheet width
sm40vh320px
default60vh400px
lg85vh560px
full100dvh, 위쪽 radius 없음100vw
PartSpecification
헤더제목 --ds-text-lg 600, --ds-space-5 패딩, inline-end에 닫기 컨트롤(X 아이콘, 48px 타깃)
본문페이지와 따로 스크롤된다. 가로 패딩 --ds-space-5
드래그 핸들바텀 시트만. 36×4px, --ds-border, --ds-radius-full, 위 가장자리에서 --ds-space-2 아래
스와이프로 닫기시트 높이의 30% 이상 끌어내리고 놓거나, 아래로 0.5px/ms보다 빠르게 밀 때 닫힌다. 그 외에는 --ds-duration-normal 동안 제자리로 돌아간다
배경--ds-overlay. 라이트 테마에서 gray-900 10%, 다크 테마에서 30%
포털현재 테마 범위(data-ds-theme)를 지정한 포털에 렌더링한다. 그래야 페이지 범위 밖에서도 semantic 토큰 값이 제대로 잡힌다
  • 시트가 열려 있는 동안 body 스크롤을 잠근다.
  • 바텀 시트는 아래로 스와이프해서 닫을 수 있지만, 스와이프로만 닫게 하지 않는다. 배경 탭, Esc, 눈에 보이는 닫기 컨트롤이 모두 동작해야 한다(SC 2.5.7).
  • 시트는 겹쳐 띄우지 않는다. 두 번째 시트를 열면 첫 번째를 대신한다.

콘텐츠

제목에는 할 일을 적는다. 제목이 없으면 접근 가능한 이름 없는 모달이 된다.

접근성

  • role="dialog"에 aria-modal="true"를 주고, 제목으로 이름을 붙인다.
  • 열리면 포커스가 시트 안으로 들어가고, 닫히면 트리거로 돌아간다.
  • 열려 있는 동안 포커스를 가둔다. Utilities 참고.
  • Esc로 닫는다.

토큰

--ds-overlay · --ds-z-modal · --ds-radius-2xl · --ds-duration-slow · --ds-ease-out

Sheet

디자인과 접근성

모든 내비게이션 컴포넌트는 키보드로 조작할 수 있어야 한다. 항목 사이는 화살표 키로 이동하고, Enter나 Space로 선택한다. 현재 페이지나 섹션은 aria-current로 표시해서 스크린 리더가 읽어 줄 수 있게 한다. Tabs, Bottom tab bar, Breadcrumbs에는 <nav> 랜드마크를 쓰고 무엇인지 알 수 있는 aria-label을 붙인다. DOM의 내비게이션 순서는 화면에 보이는 순서와 같아야 한다.

여기 모든 컴포넌트에 적용되는 규칙

  • 복합 컴포넌트는 Tab 정지점이 하나다. 안에서는 화살표 키로 이동한다.
  • 모든 <nav>에는 서로 다른 aria-label을 붙인다. 라벨 없는 내비게이션 랜드마크가 한 페이지에 두 개 있으면 스크린 리더 사용자는 둘을 구분할 수 없다.
  • aria-current에는 맞는 값을 쓴다. 내비게이션은 page, 스테퍼는 step이다.
  • 현재 상태를 색만으로 전하지 않는다. 굵기, 밑줄, 테두리, 아이콘 중 하나를 함께 쓴다.
  • DOM 순서는 화면 순서와 같게 둔다. 내비게이션에 order나 row-reverse를 쓰지 않는다.

권장과 금지

DoDon't
Use a stepper for the transfer flowUse tabs for a flow with required order
Show icon and label in the bottom tab barShip icon-only bottom navigation
Let a completed step be revisitedLet an upcoming step be skipped ahead to
Respect the bottom safe-area insetPin the tab bar to bottom: 0
Give each <nav> an aria-labelShip three unlabelled navigation landmarks

알려진 공백

  • 같은 내비게이션 맥락에서, 데스크톱 전용인 Hover card 상호작용을 모바일에서 어떻게 대체할지 문서가 없다.
  • Tree view와 Menubar는 스펙은 있지만 제품에서는 쓰지 않는다. 문서 사이트용 컴포넌트다.
  • 시트 겹쳐 띄우기는 규칙으로 금지했지만 컴포넌트 차원에서 막지는 않는다.