Drawer

free

Drawer brings supporting panels into the current page without a route change.

@floating-ui/reactmotion
Preview
Dark
import { useState } from 'react';
import Button from '@/components/ui/Button';
import Drawer from '@/components/ui/Drawer';

export default function UsageDemo() {
  const [open, setOpen] = useState(false);

  return (
    <div className="flex justify-center">
      <Button variant="solid" onClick={() => setOpen(true)}>
        Open Drawer
      </Button>

      <Drawer title="Drawer Title" isOpen={open} onClose={() => setOpen(false)}>
        <p className="text-muted-foreground">
          Render any content here — forms, details, filters, or supplementary
          actions.
        </p>
      </Drawer>
    </div>
  );
}

Installation

Add this component with the NateUI CLI.

npx nateui@latest add Drawer

Examples

Basic

Preview
Dark
import { useState } from 'react';
import Button from '@/components/ui/Button';
import Drawer from '@/components/ui/Drawer';

export default function BasicDemo() {
  const [open, setOpen] = useState(false);

  return (
    <div className="flex justify-center">
      <Button variant="solid" onClick={() => setOpen(true)}>
        Open Drawer
      </Button>

      <Drawer title="Drawer Title" isOpen={open} onClose={() => setOpen(false)}>
        <p className="text-muted-foreground">
          Render any content here — forms, details, filters, or supplementary
          actions.
        </p>
      </Drawer>
    </div>
  );
}

Placement

Preview
Dark
import { useState } from 'react';
import Button from '@/components/ui/Button';
import Drawer from '@/components/ui/Drawer';
import Radio from '@/components/ui/Radio';

type Placement = 'top' | 'right' | 'bottom' | 'left';

const placements: { label: string; value: Placement }[] = [
  { label: 'Top', value: 'top' },
  { label: 'Right', value: 'right' },
  { label: 'Bottom', value: 'bottom' },
  { label: 'Left', value: 'left' },
];

export default function PlacementDemo() {
  const [open, setOpen] = useState(false);
  const [placement, setPlacement] = useState<Placement>('right');

  return (
    <div className="flex flex-wrap items-center justify-center gap-4">
      <Radio.Group value={placement} onChange={(value) => setPlacement(value)}>
        {placements.map((item) => (
          <Radio key={item.value} value={item.value}>
            {item.label}
          </Radio>
        ))}
      </Radio.Group>

      <Button variant="solid" onClick={() => setOpen(true)}>
        Open Drawer
      </Button>

      <Drawer
        title="Drawer Title"
        isOpen={open}
        placement={placement}
        onClose={() => setOpen(false)}
      >
        <p className="text-muted-foreground">
          Drag the handle to dismiss this drawer when it opens from the top or
          bottom edge.
        </p>
      </Drawer>
    </div>
  );
}

Width & Height

Preview
Dark
import { useState } from 'react';
import Button from '@/components/ui/Button';
import Drawer from '@/components/ui/Drawer';

export default function WidthHeightDemo() {
  const [wideOpen, setWideOpen] = useState(false);
  const [tallOpen, setTallOpen] = useState(false);

  return (
    <div className="flex flex-wrap justify-center gap-2">
      <Button onClick={() => setWideOpen(true)}>Wide Drawer</Button>
      <Button onClick={() => setTallOpen(true)}>Tall Drawer</Button>

      <Drawer
        title="Wide Drawer"
        isOpen={wideOpen}
        placement="right"
        width={600}
        onClose={() => setWideOpen(false)}
      >
        <p className="text-muted-foreground">
          This drawer sets <code>width</code> to 600px.
        </p>
      </Drawer>

      <Drawer
        title="Tall Drawer"
        isOpen={tallOpen}
        placement="bottom"
        height={320}
        onClose={() => setTallOpen(false)}
      >
        <p className="text-muted-foreground">
          This drawer sets <code>height</code> to 320px.
        </p>
      </Drawer>
    </div>
  );
}
Preview
Dark
import { useState } from 'react';
import Button from '@/components/ui/Button';
import Drawer from '@/components/ui/Drawer';

export default function FooterDemo() {
  const [open, setOpen] = useState(false);

  return (
    <div className="flex justify-center">
      <Button variant="solid" onClick={() => setOpen(true)}>
        Open Drawer
      </Button>

      <Drawer
        title="Drawer Title"
        isOpen={open}
        onClose={() => setOpen(false)}
        footer={
          <div className="flex justify-end gap-2">
            <Button onClick={() => setOpen(false)}>Cancel</Button>
            <Button variant="solid" onClick={() => setOpen(false)}>
              Confirm
            </Button>
          </div>
        }
      >
        <p className="text-muted-foreground">
          The footer stays pinned while this content area scrolls.
        </p>
      </Drawer>
    </div>
  );
}

Custom Style

Preview
Dark
import { useState } from 'react';
import Button from '@/components/ui/Button';
import Drawer from '@/components/ui/Drawer';

export default function CustomStyleDemo() {
  const [open, setOpen] = useState(false);

  return (
    <div className="flex justify-center">
      <Button variant="solid" onClick={() => setOpen(true)}>
        Open Drawer
      </Button>

      <Drawer
        title="Drawer Title"
        isOpen={open}
        onClose={() => setOpen(false)}
        headerClass="bg-muted"
        bodyClass="bg-muted/40"
        footerClass="border-t-0"
        contentClassName="m-0"
        footer={
          <div className="flex w-full gap-2">
            <Button block onClick={() => setOpen(false)}>
              Cancel
            </Button>
            <Button block variant="solid" onClick={() => setOpen(false)}>
              Confirm
            </Button>
          </div>
        }
      >
        <p className="text-muted-foreground">
          The header, body, and footer regions each take their own class name.
        </p>
      </Drawer>
    </div>
  );
}

Closable

Preview
Dark
import { useState } from 'react';
import Button from '@/components/ui/Button';
import Drawer from '@/components/ui/Drawer';

export default function ClosableDemo() {
  const [open, setOpen] = useState(false);

  return (
    <div className="flex justify-center">
      <Button variant="solid" onClick={() => setOpen(true)}>
        Open Drawer
      </Button>

      <Drawer
        title="Drawer Title"
        isOpen={open}
        closable={false}
        onClose={() => setOpen(false)}
        footer={
          <div className="flex justify-end gap-2">
            <Button onClick={() => setOpen(false)}>Cancel</Button>
            <Button variant="solid" onClick={() => setOpen(false)}>
              Confirm
            </Button>
          </div>
        }
      >
        <p className="text-muted-foreground">
          With the close button hidden, the footer actions are the only way to
          dismiss the drawer.
        </p>
      </Drawer>
    </div>
  );
}

Custom Backdrop

Preview
Dark
import { useState } from 'react';
import Button from '@/components/ui/Button';
import Drawer from '@/components/ui/Drawer';

export default function BackdropDemo() {
  const [open, setOpen] = useState(false);

  return (
    <div className="flex justify-center">
      <Button variant="solid" onClick={() => setOpen(true)}>
        Open Drawer
      </Button>

      <Drawer
        title="Drawer Title"
        isOpen={open}
        overlayClassName="bg-transparent hidden"
        onClose={() => setOpen(false)}
      />
    </div>
  );
}

Form

Preview
Dark
import { useState } from 'react';
import Button from '@/components/ui/Button';
import Drawer from '@/components/ui/Drawer';
import Form from '@/components/ui/Form';
import Input from '@/components/ui/Input';

export default function FormDemo() {
  const [open, setOpen] = useState(false);

  return (
    <div className="flex justify-center">
      <Button variant="solid" onClick={() => setOpen(true)}>
        Edit Profile
      </Button>

      <Drawer
        title="Edit Profile"
        isOpen={open}
        onClose={() => setOpen(false)}
        footer={
          <div className="flex justify-end gap-2">
            <Button onClick={() => setOpen(false)}>Cancel</Button>
            <Button variant="solid" type="submit" form="drawer-profile-form">
              Save
            </Button>
          </div>
        }
      >
        <Form
          id="drawer-profile-form"
          onSubmit={(event) => {
            event.preventDefault();
            setOpen(false);
          }}
        >
          <Form.Field label="Full name" htmlFor="drawer-profile-name">
            <Input id="drawer-profile-name" defaultValue="Jordan Blake" />
          </Form.Field>

          <Form.Field label="Email" htmlFor="drawer-profile-email">
            <Input
              id="drawer-profile-email"
              defaultValue="jordan@example.com"
            />
          </Form.Field>
        </Form>
      </Drawer>
    </div>
  );
}

API

Drawer accepts the props below plus native props for the drawer content div.

PropDescriptionTypeDefault
bodyClassExtra class names applied to the scrollable body region.string-
childrenContent rendered in the scrollable body of the drawer.ReactNode-
closableShows the default close button in the header.booleantrue
contentClassNameClass names applied to the drawer content surface.string-
footerContent rendered in a footer region below the body.string | ReactNode-
footerClassExtra class names applied to the footer region.string-
headerClassExtra class names applied to the header region.string-
heightHeight of the drawer. Used when placement is 'top' or 'bottom'.string | number370
isOpenControls whether the drawer is visible.booleanfalse
lockScrollLocks document scrolling through the overlay while the drawer is mounted.booleantrue
onCloseCalled when the drawer asks to close through the close button, overlay, Escape key, or a drag-to-dismiss gesture.DrawerOpenChangeHandler-
onOpenCalled when the floating layer reports that the drawer opened.DrawerOpenChangeHandler-
overlayClassNameClass names applied to the overlay element.string-
placementEdge the drawer slides out from. 'top' and 'bottom' add a drag handle for dismissing the drawer by swipe.DrawerPlacement'right'
refRef forwarded to the drawer content element.Ref<HTMLDivElement>-
shouldCloseOnEscAllows Escape to request closing the drawer.booleantrue
shouldCloseOnOverlayClickAllows an overlay click to request closing the drawer.booleantrue
titleTitle rendered in the header region.string | ReactNode-
widthWidth of the drawer. Used when placement is 'left' or 'right'.number370
...nativeDivPropsNative props applied to the drawer content element.NativeDrawerDivProps-

Types

type DrawerPlacement = 'top' | 'right' | 'bottom' | 'left';
type DrawerOpenChangeHandler = (open: boolean) => void;
type NativeDrawerDivProps = Omit<ComponentProps<'div'>, 'title'>;