# Drawer (Caseroom component)

A side sheet on the native dialog: slides in from its edge, keeps focus inside, Escape and the backdrop close it. Black and white, dark mode, shadcn-compatible; React, Astro and plain HTML.

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

## Install

- React: `npx shadcn@latest add https://www.themes.dev/caseroom/r/drawer.json`
- Astro: `npx shadcn@latest add https://www.themes.dev/caseroom/r/astro/drawer.json`
- HTML: https://www.themes.dev/caseroom/r/html/drawer.html (with https://www.themes.dev/caseroom/r/html/caseroom.css)

Page: https://www.themes.dev/caseroom/components/drawer/ · Rules for agents: https://www.themes.dev/caseroom/AGENT.md

## Code

### drawer.astro

```astro
---
import { cn } from '@/lib/utils';
import { CLOSE_PATH, descriptionId, titleId } from '../dialog/dialog.variants';
import { drawer } from './drawer.variants';

interface Props {
  /** Unique on the page. A trigger opens it with command="show-modal" commandfor={id}. */
  id: string;
  title: string;
  description?: string;
  side?: 'right' | 'left';
  class?: string;
}

const { id, title, description, side = 'right', class: className } = Astro.props;
const hasFooter = Astro.slots.has('footer');
---

<dialog id={id} data-dialog data-side={side} aria-labelledby={titleId(id)} aria-describedby={description ? descriptionId(id) : undefined} class={cn(drawer.root, drawer[side], className)}>
  <div class={drawer.inner}>
    <div class={drawer.header}>
      <div class="min-w-0">
        <h2 id={titleId(id)} class={drawer.title}>{title}</h2>
        {description && <p id={descriptionId(id)} class={drawer.description}>{description}</p>}
      </div>
      <form method="dialog" class={drawer.close}>
        <button type="submit" aria-label="Close" class="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" stroke-width="1.5" stroke-linecap="round" aria-hidden="true" class="size-4"><path d={CLOSE_PATH} /></svg>
        </button>
      </form>
    </div>
    <div data-dialog-body class={drawer.body}><slot /></div>
    {hasFooter && <form method="dialog" data-dialog-footer class={drawer.footer}><slot name="footer" /></form>}
  </div>
</dialog>

<script>
  import '../dialog/dialog.client';
</script>
```

### drawer.tsx

```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

```tsx
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,
};
```
