Layout

Drawer.

A bottom sheet with a grab handle that can be dragged closed. Built on vaul.

Installation#

With the CLI

$npx lanterncn add drawer

This also installs the Lantern theme and any Lantern UI components it depends on. The shadcn CLI works directly too: npx shadcn@latest add httptim/lantern-ui/drawer. See Installation if your project is not set up yet.

Manually

  1. Install the dependencies

    npm install vaul
  2. Copy the source into your 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-[#080b0a]/75 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-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-popover-foreground outline-none",
              "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-lg data-[vaul-drawer-direction=top]:border-b",
              "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-lg data-[vaul-drawer-direction=bottom]:border-t",
              "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]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
              "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]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
              className,
            )}
            {...props}
          >
            <div
              aria-hidden="true"
              data-slot="drawer-handle"
              className="mx-auto mt-3 hidden h-1.5 w-12 shrink-0 rounded-full bg-input transition-colors group-hover/drawer-content:bg-muted-foreground 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-1 p-5 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.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-5", className)} {...props} />;
    }
    
    function DrawerTitle({ className, ...props }: React.ComponentProps<typeof DrawerPrimitive.Title>) {
      return (
        <DrawerPrimitive.Title
          data-slot="drawer-title"
          className={cn("font-display text-lg leading-tight font-medium tracking-tight", 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,
    };
  3. Update the import paths to match your project

    The source imports cn from @/lib/utils.

Usage#

import {
  Drawer,
  DrawerPortal,
  DrawerOverlay,
  DrawerTrigger,
  DrawerClose,
  DrawerContent,
  DrawerHeader,
  DrawerFooter,
  DrawerTitle,
  DrawerDescription,
} from "@/components/ui/drawer";
<Drawer>
  <DrawerTrigger asChild>
    <Button>Open</Button>
  </DrawerTrigger>
  <DrawerContent>
    <DrawerHeader>
      <DrawerTitle>Refuel miner-02</DrawerTitle>
      <DrawerDescription>Pick how much coal to load.</DrawerDescription>
    </DrawerHeader>
    <DrawerFooter>
      <DrawerClose asChild>
        <Button variant="secondary">Cancel</Button>
      </DrawerClose>
    </DrawerFooter>
  </DrawerContent>
</Drawer>

Pass direction="top", "left" or "right" to Drawer to slide from another edge. The grab handle only shows on bottom drawers.

Examples#

Dialog on desktop, drawer on mobile