BottomSheet
화면 하단에서 올라오는 모바일 선택 표면 — 포커스 트랩 · Escape/배경 닫기 · projection 기반 핸들 드래그 release · 스크롤 잠금을 내장한 제어 컴포넌트입니다. Select/DatePicker의 mobile 모드가 이 컴포넌트를 합성합니다 (DK 사양 승격).
마지막 업데이트 2026-07-01
한눈에#
화면 하단에서 올라오는 모바일 선택 표면입니다 — 포커스 트랩·Escape/배경 닫기·핸들 드래그 release·스크롤 잠금을 내장한 제어 컴포넌트입니다.
- 하단 슬라이드 업
- projection release
- auto·half·full 높이
- Select·DatePicker mobile 합성
트리거를 눌러 핸들 드래그 다운·Escape·배경 닫기를 직접 확인하세요:
사용 시점#
모바일·터치 환경에서 하단에서 올라오는 선택·작업 표면이면 BottomSheet — 측면·적응·중앙 결정은 다른 컴포넌트입니다.
권장 — 이렇게 쓰세요
지양 — 이러지 마세요
쓴다 — 엄지로 닿는 하단 영역에 옵션을 펼치고 핸들 드래그로 가볍게 닫는 모바일 선택 표면
대신 SideSheet — 넓은 화면에선 측면 패널로 적응시킬 때
대신 Drawer — 뷰포트와 무관하게 항상 측면 고정 패널일 때
대신 Modal — 데스크톱 중앙 결정 다이얼로그일 때
플레이그라운드#
컨트롤로 props를 조작하면 미리보기와 코드가 실시간 갱신됩니다.
BottomSheet를 직접 조작해 보세요
속성·테마·토큰을 바꾸고 React·Flutter 코드를 확인하는 풀스크린 빌더로 엽니다.
플레이그라운드는 넓은 작업 영역이 필요해 웹·태블릿에서 편집할 수 있어요.
변형#
footer 슬롯이 변형 축입니다 — 취소/확인 같은 액션이 필요한 흐름은 footer를 채우고(버튼은 균등 폭으로 늘어나는 모바일 관례), 단순 정보·즉시 확정 흐름(Select mobile처럼 항목 클릭이 곧 확정)은 footer 없이 본문만 씁니다.
DK 프로젝트의 모바일 재활용 표준 표면입니다 — 선택형 오버레이 컴포넌트는
responsive='auto'의 compact branch에서 팝업 대신 이 시트로 선택 UI를 렌더합니다
(mobile prop은 하위호환 alias).
Select, MultiSelect,
DatePicker, DateRangePicker,
TimePicker, ColorPicker,
DropdownMenu의
## 모바일 섹션을 참고하세요.
시트 내부에서 Calendar·list·짧은 form panel은 별도 카드처럼 다시 가두지 않습니다.
BottomSheet 자체가 이미 모바일 표면이므로 내부 표면은 border·border-radius·shadow·raised
background를 제거하고 본문 폭을 채웁니다. 검색창·텍스트 필드처럼 컨트롤 자체가 입력 표면인 경우만
테두리를 유지합니다.
하위 단계로 들어가는 모바일 시트는 headerLeading에 뒤로가기 액션을 둡니다. 본문 리스트에
이전 행을 추가하지 않아 좁은 화면의 세로 공간을 보존합니다.
크기#
auto도 상한 85dvh를 넘지 않아 상단 컨텍스트가 항상 일부 노출됩니다. 본문이
넘치면 body 영역만 내부 스크롤되고, footer는 safe-area-inset-bottom을
반영해 홈 인디케이터를 피합니다.
표시 방식 (ADR-012 B-P2)#
variant가 표면의 성격을 가릅니다 — 기본 'modal'은 스크림으로 배경을 가리고
포커스를 가두며 배경 스크롤을 잠그는 차단형입니다(기존 동작). 'standard'는
스크림·포커스 트랩·스크롤 잠금이 없어 본문과 공존하며, 배경을 그대로 클릭할 수
있습니다(M3 standard sheet). standard는 배경 클릭으로 닫히지 않으므로 닫기 버튼이나
핸들 드래그로 닫습니다.
expandable을 켜면 핸들을 위로 끌어 half ↔ full 디텐트를 토글합니다 — half
상태에서 아래로 release하면 닫힘 후보가 됩니다. release 판정은 손을 뗀 위치만 보지 않고
속도로 투영한 예상 착지점을 기준으로 하며, 결과에 따라 full 확장·half 축소·닫힘·원래
디텐트 복귀 중 하나를 선택합니다. 드래그 중에는 패널이 손을 즉시 따라오도록 transition을 끄고,
하단 경계에 가까워질수록 visual offset에 러버밴드 저항을 적용합니다. release 뒤에는
spring.spatial 계열 정착 전환으로 원래 디텐트 또는 새 디텐트에 복귀합니다.
height는 디텐트 초기값으로만 쓰입니다.
상태#
제어 컴포넌트입니다 — 열림/닫힘은 소비자의 open prop이 결정하고, 닫힘
요청(Escape·배경·닫기 버튼·핸들 드래그)은 전부 onClose 콜백으로 전달됩니다.
핸들 release는 raw 거리와 포인터 속도를 projectMomentum으로 투영해 판정합니다.
빠른 아래 플릭은 짧게 끌어도 닫힘 후보가 될 수 있고, 천천히 많이 끌다 멈추면 투영된
착지점 기준으로 닫힘 또는 원래 디텐트 복귀가 결정됩니다. 모달 스크림은 드래그 진행도에 맞춰
분리된 pseudo-element opacity로 줄어들어, 패널 자체가 닫힘 중에 갑자기 희미해지지 않습니다. 패널은
data-wds-bottom-sheet-state, data-wds-bottom-sheet-detent,
data-wds-bottom-sheet-dragging 상태를 노출해 열림/닫힘 exit, 드래그 추적, release
정착 전환을 분리합니다. 닫힘 요청 뒤에는 closing 상태로 남은 이동 거리와 release velocity를
반영한 하단 exit 모션을 완료한 뒤
미마운트합니다. 열림과 closing 동안 배경 body 스크롤이 잠깁니다.
titleVisuallyHidden은 제목을 접근성 이름으로만 남기고 화면에서는 숨깁니다. showCloseButton={false}는
닫기 버튼을 숨기지만 Escape·배경 클릭·핸들 드래그 닫기 계약은 유지합니다.
Props#
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
open | boolean | — | 표시 여부 — 제어 prop |
onClose | () => void | — | Escape·배경·닫기 버튼·핸들 드래그 다운의 닫힘 요청 콜백 |
title | ReactNode | — | 시트 제목 — aria-labelledby 대상이라 필수 |
headerLeading | ReactNode | — | 제목 앞 헤더 영역 — 단계형 시트의 뒤로가기/상위 이동 같은 내비게이션 액션 |
footer | ReactNode | — | 하단 액션 영역 (취소/확인 등) |
height | 'auto' | 'half' | 'full' | auto | auto: 내용 높이(상한 85dvh) · half: 50dvh · full: 85dvh. expandable이면 디텐트 초기값으로만 쓰임 |
variant | 'standard' | 'modal' | modal | 표시 방식 (ADR-012 B-P2) — modal(기본): 스크림+포커스 트랩+스크롤 잠금 / standard: 본문 공존 |
expandable | boolean | false | true면 핸들을 위로 끌어 half↔full 디텐트 토글 (half에서 아래로 끌면 닫힘) |
closeLabel | string | 닫기 | 닫기 버튼 aria-label |
titleVisuallyHidden | boolean | false | true면 제목은 접근성 이름으로만 남기고 화면에서는 숨긴다. |
showCloseButton | boolean | true | false면 닫기 버튼을 숨긴다. Escape·배경 클릭·드래그 닫기는 유지된다. |
className | string | — | 추가 CSS 클래스 — 루트 요소에 병합됩니다. |
ref | Ref<HTMLDivElement> | — | 패널(role=dialog)로 병합 전달되는 ref — 닫힘(미마운트) 동안 null (React 19 ref-as-prop). 내부 panelRef(포커스 트랩·진입 포커스·드래그 로직용)와 useMergedRefs로 합성한다. |
접근성#
| 계약 | 구현 |
|---|---|
| 역할 | role="dialog" + aria-modal="true" |
| 이름 | title이 aria-labelledby로 연결. 시각 제목을 숨겨도 접근성 이름은 유지 |
| 포커스 진입 | 열리면 패널로 이동 |
| 포커스 트랩 | Tab/Shift+Tab이 시트 안에서 순환 |
| 포커스 복원 | 닫히면 이전 포커스 요소로 복귀 |
| 키보드 닫기 | Escape — 포커스 위치 무관(document 캡처) |
| 드래그 핸들 | 포인터 전용 보조 경로(aria-hidden) — 키보드는 Escape·닫기 버튼 |
| 스크롤 | 열림 동안 배경 body 스크롤 잠금 |
| 모션 | 슬라이드 업·release 정착은 transform 중심 + prefers-reduced-motion 존중 |
토큰#
component 토큰 없이 semantic을 직접 소비합니다(신설 기준 §4 미충족 — Modal과 같은 오버레이 어휘).
| 속성 | 토큰 |
|---|---|
| 오버레이 | color.overlay · z.modal |
| 패널 | color.surface-raised · radius.xl(상단 모서리) · shadow.xl |
| 핸들 | color.border-strong · radius.full |
| 푸터 구분선 | color.border |
| 닫기 버튼 | hover color.surface-hover · pressed color.surface-pressed |
| 등장 모션 | duration.fast/normal · 패널 ease.emphasized-decelerate · 스크림 ease.out |
| release 판정 | projectMomentum 기반 projected offset |
| 드래그 추적 | resolveBottomSheetInteractiveOffset 러버밴드 · progress 기반 스크림 opacity |
| release 정착 | spring.spatial.fast/default |
| 닫힘 exit | release 위치·속도·남은 거리 기반 exit duration을 런타임 인라인 오버라이드(폴백 --wds-duration-normal) |