Aller au contenu

Tiroir

Une feuille contrôlée par gestes qui s'affiche en glissant depuis n'importe quel bord, destinée aux menus mobiles, aux filtres et aux formulaires rapides — avec des bordures et des ombres de style néobrutaliste.

"use client"

import * as React from "react"

Le tiroir est un panneau qui glisse depuis un bord de l’écran et se referme d’un glissement de doigt. Construit sur Vaul d’Emil Kowalski et habillé selon la recette néobrutaliste : bordures épaisses, ombres franches et typographie affirmée.

À privilégier pour :

  • Feuilles d’actions mobiles — menus de partage, options de tri, confirmations rapides. Le geste glisser-pour-fermer vaut mieux que de traquer un minuscule bouton de fermeture au tactile.
  • Panneaux de filtres et de réglages — filtres e-commerce ou préférences qui demandent plus de place qu’un popover sans justifier une page entière.
  • Boîtes de dialogue responsives — associez-le à Dialog pour afficher une fenêtre modale sur ordinateur et un tiroir sur mobile (voir l’exemple ci-dessous).

À propos

Drawer repose sur Vaul, signé 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

Installez les dépendances suivantes :

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

Copiez-collez le code suivant dans votre projet.

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

Adaptez les chemins d’import à la structure de votre projet.

Utilisation

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>

Composition

Utilisez la composition suivante pour construire un Drawer :

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

Exemples

Contenu défilant

Gardez les actions visibles pendant que le contenu défile.

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

Côtés

Utilisez la prop direction pour définir le côté d’ouverture du tiroir. Les valeurs possibles sont top, right, bottom et left.

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

Boîte de dialogue responsive

Vous pouvez combiner les composants Dialog et Drawer pour créer une boîte de dialogue responsive. Elle affiche un composant Dialog sur ordinateur et un Drawer sur mobile.

"use client"

import * as React from "react"

RTL

Pour activer le RTL dans Neobrutalism, consultez le guide de configuration RTL.

"use client"

import * as React from "react"

Accessibilité

Le tiroir suit le modèle WAI-ARIA Dialog (Modal) : Vaul s’appuie sur la primitive Radix Dialog, le panneau est donc rendu avec role="dialog" et aria-modal, piège le focus tant qu’il est ouvert, et il est intitulé par DrawerTitle et décrit par DrawerDescription. La fermeture par glissement est réservée au pointeur — au clavier, on ferme avec Escape ou un bouton DrawerClose : prévoyez-en donc toujours un.

Interactions clavier :

ToucheAction
Space / EnterOuvre le tiroir depuis le déclencheur ayant le focus
Tab / Shift + TabFait circuler le focus dans le contenu du tiroir (le focus est piégé)
EscapeFerme le tiroir et renvoie le focus au déclencheur

Référence API

Consultez la documentation Vaul pour la référence API complète.