Twigs Logo

Drawer

A sliding panel that overlays the main content, typically used for navigation, forms, or additional content. Supports multiple placements (top, right, bottom, left) with smooth animations and backdrop overlay.

Import

import { Drawer, DrawerHeader, DrawerFooter, DrawerBody } from "@sparrowengg/twigs-react";

Usage

const DrawerExample = () => {
  const [isDrawerOpen, setIsDrawerOpen] = React.useState(false);

  return (
    <>
      <Drawer
        isOpen={isDrawerOpen}
        onClose={() => setIsDrawerOpen(false)}
        placement="right"
      >
        <DrawerHeader>
          <Heading size="h4">Edit Profile</Heading>
        </DrawerHeader>
        <DrawerBody>
          {/* Your content here */}
        </DrawerBody>
        <DrawerFooter>
          {/* Footer content */}
        </DrawerFooter>
      </Drawer>
      <Button onClick={() => setIsDrawerOpen(true)}>
        Open Drawer
      </Button>
    </>
  );
};

Nested drawers

Render a Drawer inside another one to stack them. There is nothing to configure — a nested drawer is detected automatically.

Drawers behind the frontmost one step back and shrink slightly, so a peek of each stays visible and the depth of the stack is readable at a glance. Drawers of different sizes still step evenly: while nested, a drawer adopts the size of the drawer in front of it, and returns to its own size when it comes back to the front.

const NestedDrawerExample = () => {
  const [isSettingsOpen, setIsSettingsOpen] = React.useState(false);
  const [isBillingOpen, setIsBillingOpen] = React.useState(false);

  return (
    <>
      <Drawer isOpen={isSettingsOpen} onClose={() => setIsSettingsOpen(false)}>
        <DrawerHeader>
          <Heading size="h4">Settings</Heading>
        </DrawerHeader>
        <DrawerBody>
          <Button onClick={() => setIsBillingOpen(true)}>Open billing</Button>

          {/* The nested drawer lives inside its parent, which stays mounted. */}
          <Drawer isOpen={isBillingOpen} onClose={() => setIsBillingOpen(false)}>
            <DrawerHeader>
              <Heading size="h4">Billing</Heading>
            </DrawerHeader>
            <DrawerBody>{/* Your content here */}</DrawerBody>
          </Drawer>
        </DrawerBody>
      </Drawer>
      <Button onClick={() => setIsSettingsOpen(true)}>Open Drawer</Button>
    </>
  );
};

The parent must stay mounted for the stack to hold, so keep the nested Drawer rendered inside it rather than as a sibling.

Within a stack:

  • One backdrop is shared by the whole stack, so the page behind does not get progressively darker as drawers open.
  • Clicking the backdrop or pressing Escape closes only the frontmost drawer.
  • Page scroll stays locked until every drawer in the stack has closed.

Styling a stacked drawer

A drawer knows where it sits in the stack and exposes that, so you can style the ones behind through the css prop:

HookDescription
data-nested-drawer-openPresent while at least one drawer is stacked on top of this one. Absent on the frontmost drawer.
--nested-drawersHow many drawers are stacked on top of this one. 0 for the frontmost.

Because the attribute comes and goes as drawers open and close, styles applied through it appear while the drawer is behind another and are dropped the moment it returns to the front — no bookkeeping of your own.

<Drawer
  isOpen={isOpen}
  onClose={onClose}
  css={{
    '&[data-nested-drawer-open]': {
      opacity: 0.6
    }
  }}
>
  <DrawerBody>{/* Your content here */}</DrawerBody>
</Drawer>

Rounding the stacked drawers makes each visible edge read as its own card. Round only the edge facing into the screen — the opposite edge sits flush against the viewport, so rounding it would show a sliver of backdrop through the corners. Which edge that is depends on placement; for the default right placement it is the left one:

<Drawer
  isOpen={isOpen}
  onClose={onClose}
  css={{
    '&[data-nested-drawer-open]': {
      borderTopLeftRadius: '$2xl',
      borderBottomLeftRadius: '$2xl'
    }
  }}
>
  <DrawerBody>{/* Your content here */}</DrawerBody>
</Drawer>

To scale a style with how deep the stack is, use --nested-drawers in a calc() rather than writing a rule per level:

css={{
  '&[data-nested-drawer-open]': {
    opacity: 'calc(1 - var(--nested-drawers) * 0.15)'
  }
}}

Props

Drawer

You can alter the placement of the Modal by changing the prop to top ,right ,left and bottom

PropTypeDefault
isOpen?
boolean
false
onClose?
() => void
-
placement?
'top' | 'left' | 'right' | 'bottom'
'right'
finalFocusRef?
React.RefObject<HTMLButtonElement>
-
portalContainer?
HTMLElement
-
children?
ReactNode
-
css?
object
-
Drawer | Twigs