# Bottom Sheet URL: /lynx/components/bottom-sheet Source: https://github.com/daangn/seed-design/blob/dev/docs/content/lynx/components/bottom-sheet.mdx 화면 하단에서 올라오는 시트 컴포넌트로, 드래그·snap·dismiss 상호작용을 제공합니다. Lynx Engine 최소 버전: 3.6 사용 XElement: 사용 가능 버전: @seed-design/lynx-react@0.1.0, @seed-design/lynx-css@0.1.0 ## Preview ```tsx import "./styles"; import { BottomSheetBody, BottomSheetContent, BottomSheetFooter, BottomSheetRoot, BottomSheetTrigger, } from "@/components/ui/bottom-sheet"; import { ActionButton, useSeedClassName, VStack } from "@seed-design/lynx-react"; export default function Example() { const seedClassName = useSeedClassName({ colorMode: "system" }); return ( Open Content 확인 ); } ``` ## Installation - npm: npx @seed-design/cli add ui:bottom-sheet - pnpm: pnpm dlx @seed-design/cli add ui:bottom-sheet - yarn: yarn dlx @seed-design/cli add ui:bottom-sheet - bun: bun x @seed-design/cli add ui:bottom-sheet `@seed-design/lynx-react`의 `BottomSheet`는 [`@lynx-js/lynx-ui-sheet`](https://github.com/lynx-family/lynx-ui/tree/main/packages/lynx-ui-sheet)를 래핑합니다. 드래그, spring, snap, presence 동작은 lynx-ui-sheet가 담당합니다. SEED는 공개 API와 recipe 슬롯 스타일을 제공합니다. ## Usage ```tsx import { BottomSheetBody, BottomSheetContent, BottomSheetFooter, BottomSheetRoot, BottomSheetTrigger, } from "@/components/ui/bottom-sheet"; export function App() { return ( 시트 열기 본문 콘텐츠 하단 액션 영역 ); } ``` Registry의 `BottomSheetContent`는 Positioner, Backdrop, Header를 내부에서 조립합니다. `BottomSheetTrigger`는 Content 밖에 두어야 탭으로 시트를 열 수 있습니다. 저수준 package API에서 `BottomSheet.Positioner`에 `container`를 지정하면 `` XElement를 사용합니다. Registry의 기본 경로는 ``를 사용합니다. `container`가 필요하다면 앱의 Lynx Engine이 ``를 지원하는지 확인하세요. ### Imperative 제어 `BottomSheetRoot`에 `ref`를 전달하면 `open`, `close`, `snapTo`, `expand`, `collapse` 메서드를 호출할 수 있습니다. ```tsx import { useRef } from "@lynx-js/react"; import { BottomSheetRoot, type BottomSheetRootRef } from "@/components/ui/bottom-sheet"; export function App() { const ref = useRef(null); return ( {/* ... */} ); } ``` ## Props ### `BottomSheetRoot` ### `BottomSheetTrigger` ### `BottomSheetContent` ### `BottomSheetBody` ### `BottomSheetFooter` ## Examples ### Trigger ``를 탭하면 Bottom Sheet가 열립니다. Lynx는 `asChild`를 지원하지 않습니다. Trigger는 자식 요소를 감싸는 ``를 렌더링합니다. ```tsx import "./styles"; import { BottomSheetBody, BottomSheetContent, BottomSheetFooter, BottomSheetRoot, BottomSheetTrigger, } from "@/components/ui/bottom-sheet"; import { ActionButton, useSeedClassName, VStack } from "@seed-design/lynx-react"; export default function Example() { const seedClassName = useSeedClassName({ colorMode: "system" }); return ( Open Content 확인 ); } ``` ### Controlled Trigger 외의 방식으로 Bottom Sheet를 열고 닫으려면 `open`과 `onOpenChange`로 상태를 제어합니다. 다음 예제는 버튼을 탭한 뒤 1초 후 Bottom Sheet를 엽니다. ```tsx import "./styles"; import { BottomSheetBody, BottomSheetContent, BottomSheetFooter, BottomSheetRoot, } from "@/components/ui/bottom-sheet"; import { useState } from "@lynx-js/react"; import { ActionButton, useSeedClassName, VStack } from "@seed-design/lynx-react"; export default function Example() { const seedClassName = useSeedClassName({ colorMode: "system" }); const [open, setOpen] = useState(false); function scheduleOpen() { "background only"; setTimeout(() => { setOpen(true); }, 1000); } return ( 1초 후 열기 Content 확인 ); } ``` ### `onOpenChange` Details Lynx의 `onOpenChange`는 변경된 `open` 값만 전달합니다. React 버전의 `details.reason`은 지원하지 않습니다. 열고 닫는 원인이 필요하면 앱이 소유한 버튼과 Trigger의 이벤트 핸들러에서 원인을 별도 상태로 기록하세요. 배경 탭과 드래그는 콜백 값만으로 구분할 수 없습니다. ### Header Align ``의 `headerAlign`으로 title과 description을 왼쪽 또는 가운데에 정렬할 수 있습니다. ```tsx import "./styles"; import { BottomSheetBody, BottomSheetContent, BottomSheetFooter, BottomSheetRoot, BottomSheetTrigger, } from "@/components/ui/bottom-sheet"; import { ActionButton, HStack, useSeedClassName } from "@seed-design/lynx-react"; export default function Example() { const seedClassName = useSeedClassName({ colorMode: "system" }); return ( Left (기본값) Content 확인 Center Content 확인 ); } ``` ### Max Height `BottomSheetBody`는 native ``를 렌더링합니다. 최대 300px인 스크롤 영역을 만들려면 Body 안에 ``를 배치하고 `height`, `maxHeight`, `flex: "none"`을 함께 지정하세요. Lynx의 `scroll-view`에 `maxHeight`만 지정하면 콘텐츠 크기를 높이로 계산하지 못해 영역이 0px로 축소될 수 있습니다. ```tsx import "./styles"; import { BottomSheetBody, BottomSheetContent, BottomSheetFooter, BottomSheetRoot, BottomSheetTrigger, } from "@/components/ui/bottom-sheet"; import { ActionButton, useSeedClassName, VStack } from "@seed-design/lynx-react"; export default function Example() { const seedClassName = useSeedClassName({ colorMode: "system" }); return ( Open 확인 ); } ``` ### Snap Points `snapPoints`에는 픽셀 숫자, 화면 높이 기준 백분율, 콘텐츠 크기에 맞추는 `"fit"`을 사용할 수 있습니다. 배열 순서가 snap index가 되며, `initialSnap`으로 처음 열릴 높이를 정합니다. `onSnapChange`에서는 현재 index와 픽셀로 계산된 높이를 받을 수 있습니다. ref의 `snapTo` 메서드를 사용하면 원하는 높이로 이동할 수 있습니다. ```tsx import "./styles"; import { BottomSheetBody, BottomSheetContent, BottomSheetFooter, BottomSheetRoot, type BottomSheetRootRef, BottomSheetTrigger, } from "@/components/ui/bottom-sheet"; import { useRef, useState } from "@lynx-js/react"; import { ActionButton, HStack, useSeedClassName, VStack } from "@seed-design/lynx-react"; const snapPoints = ["45%", "80%"]; export default function Example() { const seedClassName = useSeedClassName({ colorMode: "system" }); const sheetRef = useRef(null); const [snapIndex, setSnapIndex] = useState(0); function handleSnapToFirst() { "background only"; sheetRef.current?.snapTo(0); } function handleSnapToSecond() { "background only"; sheetRef.current?.snapTo(1); } function handleClose() { "background only"; sheetRef.current?.close(); } return ( snap index: {JSON.stringify(snapIndex)} setSnapIndex(index)} > Snap Points 열기 현재 snap index: {JSON.stringify(snapIndex)} 45% 80% 닫기 ); } ``` 드래그와 snap 애니메이션의 최종 동작은 QR 코드 탭에서 Lynx Explorer로 확인하세요. 문서 미리보기에서는 기기와 제스처 처리 결과가 다를 수 있습니다. ### Fade From Index Lynx는 `fadeFromIndex`를 지원하지 않습니다. Registry가 제공하는 기본 Backdrop을 사용하세요. snap index별 dim 변화가 꼭 필요하면 저수준 package API로 Backdrop을 직접 조립하고 `onSnapChange` 상태에 따라 투명도를 제어해야 합니다. ### Show Handle ``에 `showHandle`을 전달하면 Handle을 표시합니다. 기본값은 `false`입니다. ```tsx import "./styles"; import { BottomSheetBody, BottomSheetContent, BottomSheetFooter, BottomSheetRoot, BottomSheetTrigger, } from "@/components/ui/bottom-sheet"; import { useState } from "@lynx-js/react"; import { ActionButton, useSeedClassName, VStack } from "@seed-design/lynx-react"; export default function Example() { const seedClassName = useSeedClassName({ colorMode: "system" }); const [open, setOpen] = useState(false); function handleClose() { "background only"; setOpen(false); } return ( Open Content 닫기 ); } ``` ### Show Close Button Lynx의 `BottomSheetContent`는 `showCloseButton`과 CloseButton을 지원하지 않습니다. 닫기 버튼이 필요하면 controlled 상태를 사용하고 `BottomSheetFooter`의 버튼에서 `open`을 `false`로 바꾸세요. ### Dismissible Lynx에는 배경 탭, 드래그, ESC 닫기를 한 번에 제어하는 `dismissible` prop이 없습니다. 드래그는 `BottomSheetRoot`의 `enableDragToClose={false}`로 막을 수 있습니다. 배경 탭까지 막아야 한다면 저수준 package API로 조립하고 `BottomSheet.Backdrop`에 `clickToClose={false}`를 전달하세요. Lynx에는 웹의 ESC 키 닫기 동작이 없습니다. 닫기 동작을 막았다면 Footer 등에 명시적인 닫기 버튼을 제공하세요. ### With Scroll Fog `BottomSheetBody`는 스크롤 방식을 정하지 않는 native ``입니다. 스크롤 힌트가 필요하면 Body 안에 `ScrollFog`를 직접 배치하세요. 이 구조에서는 `size`, `placement`, `hideScrollBar`와 native scroll-view prop을 사용하는 곳에서 직접 제어할 수 있습니다. 네이티브 `fading-edge-length`는 스크롤할 콘텐츠가 남아 있는 edge에서만 fog를 표시합니다. 처음에는 아래쪽, 끝까지 스크롤하면 위쪽에 표시됩니다. ```tsx import "./styles"; import { BottomSheetBody, BottomSheetContent, BottomSheetFooter, BottomSheetRoot, BottomSheetTrigger, } from "@/components/ui/bottom-sheet"; import { ActionButton, ScrollFog, useSeedClassName, VStack } from "@seed-design/lynx-react"; export default function Example() { const seedClassName = useSeedClassName({ colorMode: "system" }); return ( Open 확인 ); } ``` ### Bottom Inset Lynx의 `BottomSheetContent`는 기기의 `safeAreaInsetBottom`을 내부 padding으로 적용합니다. React 예제처럼 `--seed-safe-area-bottom`을 직접 전달할 필요가 없습니다. ### Handle Only ``에 `handleOnly`를 전달하면 Bottom Sheet를 움직이는 세로 제스처 영역이 Handle로 제한됩니다. 이 옵션은 ``과 함께 사용하세요. 본문이 길 때는 Body 안의 스크롤 영역과 시트 드래그가 겹치지 않도록 이 조합을 권장합니다. ```tsx import "./styles"; import { BottomSheetBody, BottomSheetContent, BottomSheetFooter, BottomSheetRoot, BottomSheetTrigger, } from "@/components/ui/bottom-sheet"; import { ActionButton, useSeedClassName, VStack } from "@seed-design/lynx-react"; export default function Example() { const seedClassName = useSeedClassName({ colorMode: "system" }); return ( Open Content 확인 ); } ``` ### Skip Animation `skipAnimation`을 전달하면 Bottom Sheet의 enter, exit, snap 애니메이션을 건너뜁니다. ```tsx import "./styles"; import { BottomSheetBody, BottomSheetContent, BottomSheetFooter, BottomSheetRoot, BottomSheetTrigger, } from "@/components/ui/bottom-sheet"; import { ActionButton, useSeedClassName, VStack } from "@seed-design/lynx-react"; export default function Example() { const seedClassName = useSeedClassName({ colorMode: "system" }); return ( Open Content 확인 ); } ``` ## 웹 버전과의 차이 | 항목 | 웹 | Lynx | | ------------------ | ------------------------------------- | -------------------------------------------------------------- | | 사용자 이벤트 | `onClick` | `bindtap` | | 렌더링 요소 | HTML 요소 | 네이티브 ``, ``, `` | | 상태 prop | `open`, `defaultOpen`, `onOpenChange` | 동일. 내부에서 lynx-ui-sheet의 상태 API로 연결 | | 마운트 제어 | `lazyMount`, `unmountOnExit` | Registry에서 미지원. 저수준 `BottomSheet.Positioner`의 `forceMount`로 제어 | | Trigger 합성 | `asChild` 지원 | 미지원. `` 렌더링 | | imperative ref API | 없음 | `open`, `close`, `snapTo`, `expand`, `collapse` |