Skip to content
    Style: Wireframe

    Components · Overlays

    Drawer

    A side sheet on the native dialog: slides in from its edge, keeps focus inside, Escape and the backdrop close it.

    0.48% of the 2,073 sites in The UI Index have a drawer (10 sites).

    Project settings

    Northwind site

    Browse

    Projects, domains and team settings live here.

    Both previews render the same markup; the HTML tab below is that markup without a framework.

    Install

    React
    npx shadcn@latest add https://www.themes.dev/ui/r/drawer.json
    Astro
    npx shadcn@latest add https://www.themes.dev/ui/r/astro/drawer.json

    Or copy the files below into your project. They need clsx, tailwind-merge, and the themes.dev UI theme. The HTML needs only the compiled CSS.

    drawer.tsx
    import type { ReactNode } from 'react';
    import { cn } from '@/lib/utils';
    import '../dialog/dialog.client';
    import { CLOSE_PATH, descriptionId, titleId } from '../dialog/dialog.variants';
    import { drawer } from './drawer.variants';
    
    export interface DrawerProps {
      /** Unique on the page. A trigger opens it with command="show-modal" commandfor={id}. */
      id: string;
      title: string;
      description?: string;
      side?: 'right' | 'left';
      footer?: ReactNode;
      className?: string;
      children?: ReactNode;
    }
    
    /** A side sheet on the native dialog: slides in from its edge, keeps focus inside, closes on Escape or the backdrop. */
    export function Drawer({ id, title, description, side = 'right', footer, className, children }: DrawerProps) {
      return (
        <dialog id={id} data-dialog="" data-side={side} aria-labelledby={titleId(id)} aria-describedby={description ? descriptionId(id) : undefined} className={cn(drawer.root, drawer[side], className)}>
          <div className={drawer.inner}>
            <div className={drawer.header}>
              <div className="min-w-0">
                <h2 id={titleId(id)} className={drawer.title}>{title}</h2>
                {description && <p id={descriptionId(id)} className={drawer.description}>{description}</p>}
              </div>
              <form method="dialog" className={drawer.close}>
                <button type="submit" aria-label="Close" className="inline-flex size-8 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground active:bg-pressed">
                  <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" aria-hidden="true" className="size-4"><path d={CLOSE_PATH} /></svg>
                </button>
              </form>
            </div>
            <div data-dialog-body="" className={drawer.body}>{children}</div>
            {footer && <form method="dialog" data-dialog-footer="" className={drawer.footer}>{footer}</form>}
          </div>
        </dialog>
      );
    }
    drawer.variants.ts
    import { dialog } from '../dialog/dialog.variants';
    
    /** A side sheet on the native <dialog> (the Dialog's behaviour: focus kept inside, Escape and the backdrop close
     *  it, focus returns to the trigger). It slides in from its edge and back out; with reduced motion it only fades. */
    export const drawer = {
      root: 'fixed inset-y-0 m-0 h-dvh max-h-none w-[min(24rem,calc(100%-2.5rem))] max-w-none border-0 bg-popover p-0 text-popover-foreground shadow-2xl dark:shadow-[0_24px_64px_rgb(0_0_0/0.7)] backdrop:bg-black/50 backdrop:backdrop-blur-[2px] transition-[translate,opacity,display,overlay] transition-discrete duration-200 ease-out motion-reduce:translate-x-0 motion-reduce:opacity-0 motion-reduce:open:opacity-100 motion-reduce:starting:open:opacity-0',
      right: 'right-0 left-auto translate-x-full border-l open:translate-x-0 starting:open:translate-x-full',
      left: 'left-0 right-auto -translate-x-full border-r open:translate-x-0 starting:open:-translate-x-full',
      inner: 'flex h-full flex-col',
      header: `${dialog.header} pb-4 border-b`,
      title: dialog.title,
      description: dialog.description,
      close: dialog.close,
      body: 'flex-1 overflow-y-auto px-5 py-4 text-sm',
      footer: dialog.footer,
    };