Schublade
Ein gestengesteuertes Fenster, das von jeder Kante her eingerutscht wird – für mobile Menüs, Filter und Schnellformulare – mit neobrutalistischen Rahmen und Schatten.
"use client"
import * as React from "react"Der Drawer ist ein Panel, das von einem Bildschirmrand hereingleitet und sich per Ziehen wieder schließen lässt. Basiert auf Vaul von Emil Kowalski und ist nach dem neobrutalistischen Rezept gestaltet: dicke Rahmen, harte Schatten und kräftige Typografie.
Typische Anwendungsfälle:
- Action-Sheets auf Mobilgeräten – Teilen-Menüs, Sortieroptionen, schnelle Bestätigungen. Die Zuzieh-Geste schlägt auf Touchscreens die Jagd nach einem winzigen Schließen-Button.
- Filter- und Einstellungs-Panels – E-Commerce-Filter oder Einstellungen, die mehr Platz brauchen als ein Popover, aber keine eigene Seite rechtfertigen.
- Responsive Dialoge – kombiniere ihn mit
Dialog, um auf dem Desktop ein Modal und auf Mobilgeräten einen Drawer zu rendern (siehe das Beispiel unten).
Über#
Drawer basiert auf Vaul von emilkowalski.
Installation#
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
Installiere die folgenden Abhängigkeiten:
pnpm add vaulnpm install vaulyarn add vaulbun add vaul
Kopiere den folgenden Code und füge ihn in dein Projekt ein.
"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,
}
Passe die Importpfade an deine Projektkonfiguration an.
Verwendung#
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>Komposition#
Verwende die folgende Komposition, um ein Drawer zu erstellen:
Drawer
├── DrawerTrigger
└── DrawerContent
├── DrawerHeader
│ ├── DrawerTitle
│ └── DrawerDescription
└── DrawerFooterBeispiele#
Scrollbarer Inhalt#
Halte Aktionen sichtbar, während der Inhalt scrollt.
import { Button } from "@/components/ui/button"
import {
Drawer,Seiten#
Verwende die Prop direction, um die Seite des Drawers festzulegen. Verfügbare Optionen sind top, right, bottom und left.
import { Button } from "@/components/ui/button"
import {
Drawer,Responsiver Dialog#
Du kannst die Komponenten Dialog und Drawer kombinieren, um einen responsiven Dialog zu erstellen. Dabei wird auf dem Desktop eine Dialog-Komponente und auf Mobilgeräten ein Drawer gerendert.
"use client"
import * as React from "react"RTL#
Um RTL-Unterstützung in Neobrutalism zu aktivieren, siehe die RTL-Konfigurationsanleitung.
"use client"
import * as React from "react"Barrierefreiheit#
Der Drawer folgt dem WAI-ARIA-Pattern „Dialog (Modal)“: Vaul baut auf dem Radix-Dialog-Primitive auf, das Panel wird also mit role="dialog" und aria-modal gerendert, hält den Fokus gefangen, solange es offen ist, und wird über DrawerTitle beschriftet und über DrawerDescription beschrieben. Das Schließen per Ziehen funktioniert nur mit dem Zeiger – Tastaturnutzer schließen mit Escape oder einem DrawerClose-Button, also baue immer einen ein.
Tastaturinteraktionen:
| Taste | Aktion |
|---|---|
Space / Enter | Öffnet den Drawer vom fokussierten Trigger aus |
Tab / Shift + Tab | Bewegt den Fokus zyklisch durch den Inhalt des Drawers (der Fokus bleibt gefangen) |
Escape | Schließt den Drawer und gibt den Fokus an den Trigger zurück |
API-Referenz#
Die vollständige API-Referenz findest du in der Vaul-Dokumentation.