Zum Inhalt springen

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.json
npx shadcn@latest add https://neobrutalism.com/r/base/drawer.json
yarn dlx shadcn@latest add https://neobrutalism.com/r/base/drawer.json
bunx --bun shadcn@latest add https://neobrutalism.com/r/base/drawer.json

Installiere die folgenden Abhängigkeiten:

pnpm add vaul
npm install vaul
yarn add vaul
bun add vaul

Kopiere den folgenden Code und füge ihn in dein Projekt ein.

components/ui/drawer.tsx
"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
    └── DrawerFooter

Beispiele

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:

TasteAktion
Space / EnterÖffnet den Drawer vom fokussierten Trigger aus
Tab / Shift + TabBewegt den Fokus zyklisch durch den Inhalt des Drawers (der Fokus bleibt gefangen)
EscapeSchließ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.