Naar inhoud

Lade

Een gebarenbestuurd venster dat vanaf elke rand naar binnen schuift voor mobiele menu’s, filters en snelle formulieren — met neobrutalistische randen en schaduwen.

"use client"

import * as React from "react"

De drawer is een paneel dat vanaf een rand van het scherm naar binnen schuift en dichtgesleept kan worden. Gebouwd op Vaul van Emil Kowalski en vormgegeven volgens het neobrutalistische recept: dikke randen, scherpe schaduwen en vette typografie.

Ideaal voor:

  • Mobiele actiemenu’s — deelmenu’s, sorteeropties, snelle bevestigingen. Het sleep-om-te-sluiten-gebaar werkt op touch prettiger dan zoeken naar een piepklein sluitknopje.
  • Filters en instellingenpanelen — e-commercefilters of voorkeuren die meer ruimte nodig hebben dan een popover, maar geen eigen pagina rechtvaardigen.
  • Responsieve dialogen — combineer hem met Dialog om op desktop een modaal venster te tonen en op mobiel een drawer (zie het voorbeeld hieronder).

Over

Drawer is gebouwd bovenop Vaul van emilkowalski.

Installatie

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

Installeer de volgende afhankelijkheden:

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

Kopieer en plak de volgende code in je project.

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

Werk de importpaden bij zodat ze overeenkomen met je projectconfiguratie.

Gebruik

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>

Compositie

Gebruik de volgende compositie om een Drawer te bouwen:

Drawer
├── DrawerTrigger
└── DrawerContent
    ├── DrawerHeader
    │   ├── DrawerTitle
    │   └── DrawerDescription
    └── DrawerFooter

Voorbeelden

Scrollbare inhoud

Houd acties zichtbaar terwijl de inhoud scrolt.

import { Button } from "@/components/ui/button"
import {
  Drawer,

Zijden

Gebruik de direction-prop om de zijde van de drawer in te stellen. Beschikbare opties zijn top, right, bottom en left.

import { Button } from "@/components/ui/button"
import {
  Drawer,

Responsief dialoogvenster

Je kunt de componenten Dialog en Drawer combineren om een responsief dialoogvenster te maken. Dit rendert een Dialog-component op desktop en een Drawer op mobiel.

"use client"

import * as React from "react"

RTL

Om RTL-ondersteuning in Neobrutalism in te schakelen, zie de RTL-configuratiegids.

"use client"

import * as React from "react"

Toegankelijkheid

De drawer volgt het WAI-ARIA Dialog (Modal)-patroon: Vaul bouwt voort op de Radix Dialog-primitive, dus het paneel wordt gerenderd met role="dialog" en aria-modal, houdt de focus vast zolang het open is, en krijgt een label via DrawerTitle en een beschrijving via DrawerDescription. Slepen om te sluiten werkt alleen met een aanwijzer — toetsenbordgebruikers sluiten met Escape of een DrawerClose-knop, dus neem er altijd een op.

Toetsenbordinteracties:

ToetsActie
Space / EnterOpent de drawer vanaf de trigger met focus
Tab / Shift + TabLaat de focus door de inhoud van de drawer rouleren (de focus blijft binnen)
EscapeSluit de drawer en geeft de focus terug aan de trigger

API-referentie

Zie de Vaul-documentatie voor de volledige API-referentie.