Ngăn kéo
Một giao diện điều khiển bằng cử chỉ, có thể trượt vào từ bất kỳ cạnh nào, dành cho các menu trên thiết bị di động, bộ lọc và biểu mẫu nhanh — với các đường viền và hiệu ứng bóng theo phong cách tân thô mộc.
"use client"
import * as React from "react"Drawer là một panel trượt vào từ một cạnh của màn hình và có thể kéo để đóng lại. Được xây dựng trên Vaul của Emil Kowalski và tạo kiểu theo công thức neobrutalist: viền dày, bóng đổ đậm và kiểu chữ đậm.
Nên dùng khi:
- Action sheet trên mobile — menu chia sẻ, tùy chọn sắp xếp, xác nhận nhanh. Cử chỉ kéo để đóng dễ chịu hơn nhiều so với việc nhắm vào một nút đóng bé xíu khi dùng cảm ứng.
- Panel bộ lọc và cài đặt — bộ lọc thương mại điện tử hay các tùy chọn cần nhiều chỗ hơn một popover nhưng chưa đến mức phải có trang riêng.
- Dialog đáp ứng — kết hợp với
Dialogđể hiện modal trên desktop và drawer trên mobile (xem ví dụ bên dưới).
Giới thiệu#
Drawer được xây dựng dựa trên Vaul của emilkowalski.
Cài đặt#
pnpm dlx shadcn@latest add https://neobrutalism.com/r/base/drawer.jsonnpx shadcn@latest add https://neobrutalism.com/r/base/drawer.jsonyarn dlx shadcn@latest add https://neobrutalism.com/r/base/drawer.jsonbunx --bun shadcn@latest add https://neobrutalism.com/r/base/drawer.json
Cài đặt các phụ thuộc sau:
pnpm add vaulnpm install vaulyarn add vaulbun add vaul
Sao chép và dán đoạn mã sau vào dự án của bạn.
"use client"
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@/lib/utils"
function Drawer({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
}
function DrawerTrigger({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
}
function DrawerPortal({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
}
function DrawerClose({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
}
function DrawerOverlay({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
return (
<DrawerPrimitive.Overlay
data-slot="drawer-overlay"
className={cn(
"fixed inset-0 z-50 bg-foreground/20 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
className
)}
{...props}
/>
)
}
function DrawerContent({
className,
children,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
return (
<DrawerPortal data-slot="drawer-portal">
<DrawerOverlay />
<DrawerPrimitive.Content
data-slot="drawer-content"
className={cn(
"group/drawer-content fixed z-50 flex h-auto flex-col bg-popover text-sm text-popover-foreground shadow-md data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-xl data-[vaul-drawer-direction=bottom]:border-t-2 data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:rounded-r-xl data-[vaul-drawer-direction=left]:border-r-2 data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:rounded-l-xl data-[vaul-drawer-direction=right]:border-l-2 data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-xl data-[vaul-drawer-direction=top]:border-b-2 data-[vaul-drawer-direction=left]:sm:max-w-sm data-[vaul-drawer-direction=right]:sm:max-w-sm",
className
)}
{...props}
>
<div className="mx-auto mt-4 hidden h-1 w-[100px] shrink-0 rounded-full bg-muted group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
)
}
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-header"
className={cn(
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-0.5 md:text-left",
className
)}
{...props}
/>
)
}
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
}
function DrawerTitle({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
return (
<DrawerPrimitive.Title
data-slot="drawer-title"
className={cn(
"font-head text-base font-medium text-foreground",
className
)}
{...props}
/>
)
}
function DrawerDescription({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
return (
<DrawerPrimitive.Description
data-slot="drawer-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}
Cập nhật các đường dẫn import cho phù hợp với thiết lập dự án của bạn.
Cách dùng#
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer"<Drawer>
<DrawerTrigger>Open</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Are you absolutely sure?</DrawerTitle>
<DrawerDescription>This action cannot be undone.</DrawerDescription>
</DrawerHeader>
<DrawerFooter>
<Button>Submit</Button>
<DrawerClose>
<Button variant="outline">Cancel</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>Kết hợp#
Dùng cách kết hợp sau để xây dựng một Drawer:
Drawer
├── DrawerTrigger
└── DrawerContent
├── DrawerHeader
│ ├── DrawerTitle
│ └── DrawerDescription
└── DrawerFooterVí dụ#
Nội dung có thể cuộn#
Giữ các hành động hiển thị trong khi nội dung cuộn.
import { Button } from "@/components/ui/button"
import {
Drawer,Các cạnh#
Dùng prop direction để thiết lập cạnh của drawer. Các tùy chọn khả dụng là top, right, bottom và left.
import { Button } from "@/components/ui/button"
import {
Drawer,Dialog đáp ứng#
Bạn có thể kết hợp các thành phần Dialog và Drawer để tạo một dialog đáp ứng. Nó render một thành phần Dialog trên máy tính để bàn và một Drawer trên di động.
"use client"
import * as React from "react"RTL#
Để bật hỗ trợ RTL trong Neobrutalism, hãy xem hướng dẫn cấu hình RTL.
"use client"
import * as React from "react"Khả năng truy cập#
Drawer tuân theo mẫu Dialog (Modal) của WAI-ARIA: Vaul được xây trên primitive Radix Dialog, nên panel render với role="dialog" cùng aria-modal, giữ focus bên trong khi đang mở, được gắn nhãn bởi DrawerTitle và mô tả bởi DrawerDescription. Cử chỉ kéo để đóng chỉ dành cho con trỏ — người dùng bàn phím đóng bằng Escape hoặc bằng một nút DrawerClose, nên hãy luôn để sẵn một nút như vậy.
Tương tác bàn phím:
| Phím | Hành động |
|---|---|
Space / Enter | Mở drawer từ trigger đang được focus |
Tab / Shift + Tab | Luân chuyển focus qua nội dung của drawer (focus bị giữ lại) |
Escape | Đóng drawer và trả focus về trigger |
Tham chiếu API#
Xem tài liệu Vaul để biết tham chiếu API đầy đủ.