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 à
Dialogpour 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.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
Installez les dépendances suivantes :
pnpm add vaulnpm install vaulyarn add vaulbun add vaul
Copiez-collez le code suivant dans votre projet.
"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
└── DrawerFooterExemples#
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 :
| Touche | Action |
|---|---|
Space / Enter | Ouvre le tiroir depuis le déclencheur ayant le focus |
Tab / Shift + Tab | Fait circuler le focus dans le contenu du tiroir (le focus est piégé) |
Escape | Ferme 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.