컴포넌트
Drawer
화면 가장자리에서 패널을 엽니다.
모션
아래에서 300ms 동안 나타나고 200ms 동안 닫힙니다. 스와이프 중에는 지연 없이 손가락을 따라갑니다. 움직임 감소 설정에서는 전환을 생략합니다. 공통 모션 규칙을 따릅니다.
설치
pnpm dlx ply-ui add drawer공통 설치 설정을 먼저 적용합니다.
사용법
Portal → Viewport → Popup → Content 순서로 조합합니다.
Title/Description을 제공하고 Close로 닫습니다. 스와이프 동작은 Base UI가 처리합니다.
구성
| 구성 요소 | 역할 |
|---|---|
Root | 열림 상태와 스와이프 동작을 관리합니다. |
Trigger | 패널을 여는 버튼입니다. |
Portal / Backdrop | 패널을 별도 영역에 렌더하고 배경을 덮습니다. |
Viewport | 패널의 위치와 스와이프 영역을 구성합니다. |
Popup | 아래쪽 패널의 표면입니다. |
Content | 패널 안의 내용 영역입니다. |
Title / Description | 패널 이름과 설명을 연결합니다. |
Close | 패널을 닫습니다. |
구현 코드
components/ui/drawer.tsx
"use client";
import type { ComponentProps } from "react";
import { Drawer as Primitive } from "@base-ui/react/drawer";
import { withClassName } from "../../lib/cx";
import "./drawer.css";
// 동작과 접근성은 Base UI가 담당합니다. 이 파일은 스타일 연결만 담당합니다.
function DrawerBackdrop({
className,
...props
}: ComponentProps<typeof Primitive.Backdrop>) {
return (
<Primitive.Backdrop
{...props}
className={withClassName("rbx-backdrop", className)}
/>
);
}
function DrawerPopup({
className,
...props
}: ComponentProps<typeof Primitive.Popup>) {
return (
<Primitive.Popup
{...props}
className={withClassName("rbx-drawer", className)}
/>
);
}
function DrawerTitle({
className,
...props
}: ComponentProps<typeof Primitive.Title>) {
return (
<Primitive.Title
{...props}
className={withClassName("rbx-dialog-title", className)}
/>
);
}
function DrawerDescription({
className,
...props
}: ComponentProps<typeof Primitive.Description>) {
return (
<Primitive.Description
{...props}
className={withClassName("rbx-description", className)}
/>
);
}
function DrawerTrigger({
className,
...props
}: ComponentProps<typeof Primitive.Trigger>) {
return (
<Primitive.Trigger
{...props}
className={withClassName("rbx-button", className)}
/>
);
}
function DrawerClose({
className,
...props
}: ComponentProps<typeof Primitive.Close>) {
return (
<Primitive.Close
{...props}
className={withClassName("rbx-button", className)}
/>
);
}
function DrawerContent({
className,
...props
}: ComponentProps<typeof Primitive.Content>) {
return (
<Primitive.Content
{...props}
className={withClassName("rbx-drawer-content", className)}
/>
);
}
// Root/Portal 등 스타일 없는 파트와 제네릭 API는 원본을 그대로 보존합니다.
export const Drawer = {
...Primitive,
Backdrop: DrawerBackdrop,
Popup: DrawerPopup,
Title: DrawerTitle,
Description: DrawerDescription,
Trigger: DrawerTrigger,
Close: DrawerClose,
Content: DrawerContent,
};
components/ui/drawer.css
@import "./button.css";
/* Drawer: 공통 구조는 theme.css, 컴포넌트 전용 조정은 아래에 작성합니다. */
.rbx-drawer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 101;
max-height: 85dvh;
overflow: auto;
background: var(--rbx-color-surface-100);
color: var(--rbx-color-content-emphasis);
border-radius: 16px 16px 0 0;
padding: 32px;
transform: translateY(var(--drawer-swipe-movement-y, 0));
}
.rbx-drawer-content {
max-width: 600px;
margin: auto;
}
/* 스와이프 중에는 손가락을 바로 따라가고, 놓으면 전환합니다. */
.rbx-drawer {
transition: transform var(--rbx-motion-duration-panel)
var(--rbx-motion-ease-enter);
}
.rbx-drawer:is([data-starting-style], [data-ending-style]) {
transform: translateY(100%);
}
.rbx-drawer[data-ending-style] {
transition-duration: var(--rbx-motion-duration-panel-exit);
transition-timing-function: var(--rbx-motion-ease-exit);
}
.rbx-drawer[data-swiping] {
transition: none;
}