Agents (llms.txt)
Octocat

Drawer

A slide-in panel that overlays the main content.

import { Button } from '@/components/button';
import { Drawer } from '@/components/drawer';

const DrawerPreview = () => {
  return (
    <Drawer>
      <Drawer.Trigger asChild>
        <Button>Open Drawer</Button>
      </Drawer.Trigger>
      <Drawer.Content>
        <Drawer.Header>
          <Drawer.Title>Drawer Title</Drawer.Title>
        </Drawer.Header>
        <Drawer.Description>Drawer content goes here.</Drawer.Description>
        <Drawer.Actions className="flex gap-2">
          <Drawer.Close asChild>
            <Button variant="primary" className="grow">
              Submit
            </Button>
          </Drawer.Close>
          <Drawer.Close asChild>
            <Button className="grow" variant="outline">
              Close
            </Button>
          </Drawer.Close>
        </Drawer.Actions>
      </Drawer.Content>
    </Drawer>
  );
};

export default DrawerPreview;

Dependencies

Source Code

import { Modal } from '@/components/modal';
import { cn } from '@/lib/utils/classnames';

type DrawerProps = React.ComponentProps<typeof Modal>;

const Drawer = (props: DrawerProps) => {
  return <Modal {...props} />;
};

type DrawerContentProps = React.ComponentProps<typeof Modal.Content>;

const DrawerContent = ({
  className,
  children,
  ...props
}: DrawerContentProps) => {
  return (
    <Modal.Content
      className={cn(
        '[--drawer-p:--spacing(4)]',
        'overflow-x-hidden! mx-auto flex w-full max-w-screen flex-col overflow-y-auto border border-border bg-background-high p-(--drawer-p) *:shrink-0',
        'has-[[data-modal-focus-catcher]:first-child+[data-drawer-header],[data-drawer-header]:first-child]:pt-0 has-[[data-drawer-actions]:last-child]:pb-0',
        'backdrop:bg-black/20 not-data-[status=open]:backdrop:opacity-0 backdrop:backdrop-blur-sm',
        // desktop
        'md:mr-0 md:h-full md:max-h-screen md:w-full md:max-w-lg md:not-data-[status=open]:translate-x-full',
        // mobile
        'min-h-[50svh] max-md:mb-0 max-md:max-h-[calc(100svh-(--spacing(16)))] max-md:w-full max-md:not-data-[status=open]:translate-y-full max-md:rounded-t-xl',
        // animation props
        'transition-transform ease-emphasized-decelerate not-data-[status=open]:ease-emphasized-accelerate',
        'backdrop:transition-opacity backdrop:ease-in-out',
        'motion-reduce:transition-none motion-reduce:backdrop:transition-none',
        'duration-400 not-data-[status=open]:duration-250 not-data-[status=open]:backdrop:delay-150 not-data-[status=open]:backdrop:duration-200',
        className
      )}
      {...props}
    >
      {children}
    </Modal.Content>
  );
};

const DrawerTrigger = Modal.Trigger;

const DrawerClose = Modal.Close;

const DrawerTitle = ({
  children,
  className,
  ...props
}: React.ComponentProps<typeof Modal.Title>) => {
  return (
    <Modal.Title className={cn('font-semibold', className)} {...props}>
      {children}
    </Modal.Title>
  );
};

const DrawerDescription = ({
  children,
  className,
  ...props
}: React.ComponentProps<typeof Modal.Description>) => {
  return (
    <Modal.Description className={cn('pb-2', className)} {...props}>
      {children}
    </Modal.Description>
  );
};

const DrawerBleed = ({
  className,
  ...props
}: React.ComponentPropsWithRef<'div'>) => {
  return <div className={cn('-mx-(--drawer-p)', className)} {...props} />;
};

const DrawerHeader = ({
  className,
  children,
  ...props
}: React.ComponentPropsWithRef<'div'>) => {
  return (
    <DrawerBleed
      className={cn(
        'sticky top-0 z-10 mb-(--drawer-p) border-border border-b bg-background p-(--drawer-p)',
        className
      )}
      {...props}
      data-drawer-header=""
    >
      {children}
    </DrawerBleed>
  );
};

const DrawerActions = ({
  className,
  children,
  ...props
}: React.ComponentPropsWithRef<'div'>) => {
  return (
    <DrawerBleed
      className={cn(
        'sticky bottom-0 mt-auto flex gap-2 border-border border-t bg-background p-(--drawer-p)',
        className
      )}
      {...props}
      data-drawer-actions=""
    >
      {children}
    </DrawerBleed>
  );
};

const CompoundDrawer = Object.assign(Drawer, {
  Content: DrawerContent,
  Header: DrawerHeader,
  Title: DrawerTitle,
  Description: DrawerDescription,
  Actions: DrawerActions,
  Bleed: DrawerBleed,
  Trigger: DrawerTrigger,
  Close: DrawerClose,
});

export { CompoundDrawer as Drawer };

Features

  • Modal Overlay: Creates an accessible modal dialog with backdrop
  • Focus Management: Automatically traps focus within the dialog
  • Flexible Positioning: Center or top alignment options
  • Controlled & Uncontrolled: Supports both controlled and uncontrolled modes
  • Customizable Actions: Built-in support for common dialog actions

Anatomy


          <Drawer>
  <Drawer.Trigger />
  <Drawer.Content>
    <Drawer.Header>
      <Drawer.Title />
    </Drawer.Header>
    <Drawer.Description />
    <Drawer.Actions>
      <Drawer.Close />
    </Drawer.Actions>
  </Drawer.Content>
</Drawer>
        

API Reference

Drawer

Extends the Modal component.

Drawer.Content

Extends the Modal.Content component.

Drawer.Trigger

Extends the Modal.Trigger component.

Drawer.Close

Extends the Modal.Close component.

Drawer.Title

Extends the Modal.Title component.

Drawer.Description

Extends the Modal.Description component.

Drawer.Header

Extends the div element.

Drawer.Actions

Extends the div element.

Examples

Simple

import { Button } from '@/components/button';
import { Drawer } from '@/components/drawer';

const DrawerPreview = () => {
  return (
    <Drawer>
      <Drawer.Trigger asChild>
        <Button>Open Drawer</Button>
      </Drawer.Trigger>
      <Drawer.Content>
        <Drawer.Header>
          <Drawer.Title>Drawer Title</Drawer.Title>
        </Drawer.Header>
        <Drawer.Description>Drawer content goes here.</Drawer.Description>
        <Drawer.Actions className="flex gap-2">
          <Drawer.Close asChild>
            <Button variant="primary" className="grow">
              Submit
            </Button>
          </Drawer.Close>
          <Drawer.Close asChild>
            <Button className="grow" variant="outline">
              Close
            </Button>
          </Drawer.Close>
        </Drawer.Actions>
      </Drawer.Content>
    </Drawer>
  );
};

export default DrawerPreview;

Tall Content

import { Button } from '@/components/button';
import { Drawer } from '@/components/drawer';

const DrawerTallContent = () => {
  return (
    <Drawer>
      <Drawer.Trigger asChild>
        <Button>Open Drawer</Button>
      </Drawer.Trigger>
      <Drawer.Content>
        <Drawer.Header>
          <Drawer.Title>Drawer Title</Drawer.Title>
        </Drawer.Header>
        {Array(24)
          .fill(null)
          .map((_, index) => (
            <p key={index} className="mb-4">
              This is paragraph {index + 1}.
            </p>
          ))}
        <Drawer.Actions className="flex gap-2">
          <Drawer.Close asChild>
            <Button className="grow">Submit</Button>
          </Drawer.Close>
          <Drawer.Close asChild>
            <Button className="grow" variant="outline">
              Close
            </Button>
          </Drawer.Close>
        </Drawer.Actions>
      </Drawer.Content>
    </Drawer>
  );
};

export default DrawerTallContent;

Controlled

import { useState } from 'react';

import { Button } from '@/components/button';
import { Drawer } from '@/components/drawer';

const DrawerControlled = () => {
  const [open, setOpen] = useState(false);
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleSubmit = () => {
    setIsSubmitting(true);

    setTimeout(() => {
      setIsSubmitting(false);
      setOpen(false);
    }, 3000);
  };

  const handleCancel = () => {
    setOpen(false);
  };

  return (
    <Drawer
      open={open}
      onOpenChange={(isOpen) => setOpen(isSubmitting ? true : isOpen)}
    >
      <Button onClick={() => setOpen(true)}>Open Drawer</Button>
      <Drawer.Content inert={isSubmitting}>
        <Drawer.Header>
          <Drawer.Title>Drawer Title</Drawer.Title>
        </Drawer.Header>
        <p>Drawer dangerous content goes here.</p>
        <Drawer.Actions className="flex gap-2">
          <Button
            className="grow"
            variant="destructive"
            isLoading={isSubmitting}
            onClick={handleSubmit}
            disabled={isSubmitting}
          >
            Delete Everything
          </Button>
          <Button
            className="grow"
            variant="outline"
            onClick={handleCancel}
            disabled={isSubmitting}
          >
            I&apos;m not sure
          </Button>
        </Drawer.Actions>
      </Drawer.Content>
    </Drawer>
  );
};

export default DrawerControlled;

Best Practices

  1. Content Structure:

    • Always include a clear title that describes the purpose
    • Keep content concise and focused
    • Use appropriate action labels (avoid “OK/Cancel”)
  2. Performance:

    • Lazy load dialog content if needed
    • Consider using dynamic imports for heavy content
    • Clean up resources when dialog closes

Previous

Divider

Next

Field