컴포넌트

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
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
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;
}

목차