Skip to main content

Sheet (Drawer)

  • Component overview: Slide-out panel for forms, detail views, or secondary content. Supports four directions: top, right (default), bottom, and left. Based on Radix Dialog. Also exported as Drawer.
  • Current status: Supports declarative (trigger as children) and config (title/content/footer) modes, with imperative Sheet.open() API.
  • Implementation note: ui/sheet wraps Radix Dialog primitive; design layer provides config-style API, direction/width control, and imperative overlay support.

Basic Usage

Pass title, content, footer as props. The trigger is provided as children.

Result
Loading...
Live Editor
render(
  <div style={{ padding: '40px 0' }}>
    <Sheet
      title="Profile"
      content={
        <div style={{ padding: '0 16px', flex: 1 }}>
          <p style={{ fontSize: 14, color: '#6b7280', marginBottom: 8 }}>View and edit your profile information.</p>
          <p>Name: John Doe</p>
          <p>Email: john@example.com</p>
        </div>
      }
      footer={
        <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
          <Button variant="secondary">Cancel</Button>
          <Button>Save</Button>
        </div>
      }
    >
      <Button>Open Sheet</Button>
    </Sheet>
  </div>,
)

Direction

Use the side prop to control which edge the sheet slides from. Default is right.

Result
Loading...
Live Editor
render(
  <div style={{ display: 'flex', gap: 12, padding: '40px 0', flexWrap: 'wrap' }}>
    <Sheet side="right" title="Right Sheet" content={<div style={{ padding: '0 16px', flex: 1 }}>Slides from the right (default).</div>}>
      <Button variant="secondary">Right</Button>
    </Sheet>
    <Sheet side="left" title="Left Sheet" content={<div style={{ padding: '0 16px', flex: 1 }}>Slides from the left.</div>}>
      <Button variant="secondary">Left</Button>
    </Sheet>
    <Sheet side="top" title="Top Sheet" content={<div style={{ padding: '0 16px' }}>Slides from the top.</div>}>
      <Button variant="secondary">Top</Button>
    </Sheet>
    <Sheet side="bottom" title="Bottom Sheet" content={<div style={{ padding: '0 16px' }}>Slides from the bottom.</div>}>
      <Button variant="secondary">Bottom</Button>
    </Sheet>
  </div>,
)

Custom Width

For left/right sheets, use the width prop. Accepts number (px) or string.

Result
Loading...
Live Editor
render(
  <div style={{ display: 'flex', gap: 12, padding: '40px 0' }}>
    <Sheet width={400} title="Narrow Sheet" content={<div style={{ padding: '0 16px', flex: 1 }}>Width: 400px</div>}>
      <Button variant="secondary">400px</Button>
    </Sheet>
    <Sheet width="80vw" title="Wide Sheet" content={<div style={{ padding: '0 16px', flex: 1 }}>Width: 80vw</div>}>
      <Button variant="secondary">80vw</Button>
    </Sheet>
  </div>,
)

Controlled Mode

Result
Loading...
Live Editor
const Demo = () => {
  const [open, setOpen] = useState(false)
  return (
    <div style={{ display: 'flex', gap: 12, alignItems: 'center', padding: '40px 0' }}>
      <Button onClick={() => setOpen(true)}>Open Controlled</Button>
      <span style={{ fontSize: 14, color: '#999' }}>open: {String(open)}</span>
      <Sheet
        open={open}
        onOpenChange={setOpen}
        title="Controlled Sheet"
        content={
          <div style={{ padding: '0 16px', flex: 1 }}>
            <p>This sheet is controlled externally.</p>
            <Button variant="secondary" onClick={() => setOpen(false)}>Close from inside</Button>
          </div>
        }
      />
    </div>
  )
}
render(<Demo />)

Imperative API

Sheet.open() provides imperative access. Suitable for async callbacks, keyboard shortcuts, or table actions where a JSX trigger is not available. Requires DesignProvider or OverlayHost mounted at the application root.

import { Sheet } from '@plaud/design'

Sheet.open({
title: 'Edit Profile',
content: ({ close }) => (
<div>
<p>Content here.</p>
<Button onClick={close}>Close</Button>
</div>
),
})

Update an Open Sheet

open() returns a controller to update configuration after opening:

const controller = Sheet.open({
title: 'Loading...',
content: <div>Please wait.</div>,
})

setTimeout(() => {
controller.update({
title: 'Done',
content: <div>Data loaded.</div>,
})
}, 1000)

Controller

interface ImperativeOverlayController<TOptions> {
id: string
close: () => void
update: (updater: Partial<TOptions> | ((prev: TOptions) => TOptions)) => void
afterClosed: Promise<void>
}

Props

PropTypeDefaultDescription
titleReactNodeHeader title
contentReactNodeBody content
footerReactNodeFooter area
childrenReactNodeTrigger element (non-controlled mode)
openbooleanControlled open state
onOpenChange(open: boolean) => voidOpen state change callback
side'top' | 'right' | 'bottom' | 'left''right'Slide direction
widthnumber | string560Content width for left/right direction (px or CSS value)
showClosebooleantrueShow close button
destroyOnClosebooleantrueUnmount content when closed
contentClassNamestringCustom content area class

Usage Constraints

  • Default width is 560px for left/right directions.
  • Imperative API requires DesignProvider or OverlayHost at the application root.