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,062 sites in The UI Index have a drawer (10 sites).
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/ caseroom/ r/ drawer.json - Astro
- npx shadcn@
latest add https:// www.themes.dev/ caseroom/ r/ astro/ drawer.json
Or copy the files below into your project. They need clsx, tailwind-merge, and the Caseroom theme. The HTML needs only the compiled CSS.
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>
);
}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,
};---
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>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,
};<div class="flex flex-wrap justify-center gap-3">
<button type="button" class="inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,background-color,border-color,transform] select-none active:scale-[0.97] aria-busy:cursor-progress disabled:pointer-events-none disabled:opacity-50 aria-busy:disabled:opacity-80 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border border-input bg-background hover:bg-accent hover:text-accent-foreground active:bg-pressed h-9 px-4" command="show-modal" commandfor="ex-drawer">
Project settings
</button>
<button type="button" class="inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,background-color,border-color,transform] select-none active:scale-[0.97] aria-busy:cursor-progress disabled:pointer-events-none disabled:opacity-50 aria-busy:disabled:opacity-80 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 hover:bg-accent hover:text-accent-foreground active:bg-pressed h-9 px-4" command="show-modal" commandfor="ex-drawer-left">
Browse
</button>
</div>
<dialog id="ex-drawer" data-dialog data-side="right" aria-labelledby="ex-drawer-title" aria-describedby="ex-drawer-description" class="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-0 left-auto translate-x-full border-l open:translate-x-0 starting:open:translate-x-full">
<div class="flex h-full flex-col">
<div class="flex items-start justify-between gap-4 px-5 pt-5 pb-4 border-b">
<div class="min-w-0">
<h2 id="ex-drawer-title" class="text-base font-semibold tracking-[-0.01em]">Project settings</h2>
<p id="ex-drawer-description" class="mt-1 text-sm text-pretty text-muted-foreground">Northwind site</p>
</div>
<form method="dialog" class="-mt-1.5 -mr-2 shrink-0">
<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="M4 4l8 8M12 4l-8 8"></path></svg>
</button>
</form>
</div>
<div data-dialog-body class="flex-1 overflow-y-auto px-5 py-4 text-sm">
<div class="grid gap-4">
<label class="flex items-center gap-2 text-sm leading-none font-medium has-[input]:items-start has-[input]:leading-snug has-[input]:[&>*:first-child]:mt-px select-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50">
<input type="checkbox" role="switch" class="relative inline-flex h-5 w-9 shrink-0 cursor-pointer appearance-none items-center rounded-full border border-input bg-muted transition-colors before:absolute before:left-0.5 before:size-3.5 before:rounded-full before:bg-foreground/70 before:transition-[transform,width] before:content-[''] checked:border-primary checked:bg-primary checked:before:translate-x-4 checked:before:bg-primary-foreground enabled:not-checked:hover:border-foreground enabled:not-checked:hover:before:bg-foreground checked:enabled:hover:border-[color-mix(in_oklab,var(--primary)_var(--hover-amount),var(--background))] checked:enabled:hover:bg-[color-mix(in_oklab,var(--primary)_var(--hover-amount),var(--background))] checked:enabled:active:border-[color-mix(in_oklab,var(--primary)_var(--press-amount),var(--press-mix))] checked:enabled:active:bg-[color-mix(in_oklab,var(--primary)_var(--press-amount),var(--press-mix))] checked:enabled:hover:before:bg-primary-foreground enabled:active:before:w-4.5 checked:enabled:active:before:translate-x-3 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50" checked>
Deploy on every push
</label>
<label class="flex items-center gap-2 text-sm leading-none font-medium has-[input]:items-start has-[input]:leading-snug has-[input]:[&>*:first-child]:mt-px select-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50">
<input type="checkbox" role="switch" class="relative inline-flex h-5 w-9 shrink-0 cursor-pointer appearance-none items-center rounded-full border border-input bg-muted transition-colors before:absolute before:left-0.5 before:size-3.5 before:rounded-full before:bg-foreground/70 before:transition-[transform,width] before:content-[''] checked:border-primary checked:bg-primary checked:before:translate-x-4 checked:before:bg-primary-foreground enabled:not-checked:hover:border-foreground enabled:not-checked:hover:before:bg-foreground checked:enabled:hover:border-[color-mix(in_oklab,var(--primary)_var(--hover-amount),var(--background))] checked:enabled:hover:bg-[color-mix(in_oklab,var(--primary)_var(--hover-amount),var(--background))] checked:enabled:active:border-[color-mix(in_oklab,var(--primary)_var(--press-amount),var(--press-mix))] checked:enabled:active:bg-[color-mix(in_oklab,var(--primary)_var(--press-amount),var(--press-mix))] checked:enabled:hover:before:bg-primary-foreground enabled:active:before:w-4.5 checked:enabled:active:before:translate-x-3 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50">
Preview links for pull requests
</label>
</div>
</div>
<form method="dialog" data-dialog-footer class="flex flex-wrap items-center justify-end gap-2 border-t bg-muted/50 px-5 py-3">
<button type="submit" class="inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,background-color,border-color,transform] select-none active:scale-[0.97] aria-busy:cursor-progress disabled:pointer-events-none disabled:opacity-50 aria-busy:disabled:opacity-80 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 hover:bg-accent hover:text-accent-foreground active:bg-pressed h-9 px-4" value="cancel">
Cancel
</button>
<button type="submit" class="inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,background-color,border-color,transform] select-none active:scale-[0.97] aria-busy:cursor-progress disabled:pointer-events-none disabled:opacity-50 aria-busy:disabled:opacity-80 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-[color-mix(in_oklab,var(--primary)_var(--hover-amount),var(--background))] active:bg-[color-mix(in_oklab,var(--primary)_var(--press-amount),var(--press-mix))] h-9 px-4" value="save">
Save
</button>
</form>
</div>
</dialog>
<dialog id="ex-drawer-left" data-dialog data-side="left" aria-labelledby="ex-drawer-left-title" class="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 left-0 right-auto -translate-x-full border-r open:translate-x-0 starting:open:-translate-x-full">
<div class="flex h-full flex-col">
<div class="flex items-start justify-between gap-4 px-5 pt-5 pb-4 border-b">
<div class="min-w-0">
<h2 id="ex-drawer-left-title" class="text-base font-semibold tracking-[-0.01em]">Browse</h2>
</div>
<form method="dialog" class="-mt-1.5 -mr-2 shrink-0">
<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="M4 4l8 8M12 4l-8 8"></path></svg>
</button>
</form>
</div>
<div data-dialog-body class="flex-1 overflow-y-auto px-5 py-4 text-sm">
<p class="text-muted-foreground">Projects, domains and team settings live here.</p>
</div>
</div>
</dialog>