Marigold
v17.5.1
Marigold
v17.5.1

Application

MarigoldProvider
RouterProvider

Layout

AppLayoutbeta
Aside
Aspect
Breakout
Center
Columns
Container
Grid
Inline
Inset
Scrollable
Split
Stack
Tiles

Actions

ActionBaralpha
Button
Link
LinkButton
ToggleButtonbeta

Form

Autocomplete
Calendar
Checkbox
ComboBox
DateField
DatePicker
FileField
Form
Multiselectdeprecated
NumberField
Radio
RangeCalendaralpha
SearchField
Select
Slider
Switch
TagFieldbeta
TextArea
TextField
TimeField

Collection

SelectList
Table
Tag

Navigation

Accordion
Breadcrumbsupdated
Pagination
Sidebarbeta
Tabsupdated
TopNavigationbeta

Overlay

ContextualHelp
Dialog
Drawerupdated
Menu
Toastbeta
Tooltip

Content

Badge
Card
Divider
EmptyStatebeta
Headline
Icon
List
Loader
SectionMessage
SVG
Text

Formatters

DateFormat
NumericFormat

Hooks and Utils

cn
cva
extendTheme
parseFormData
useAsyncListData
useListData
useResponsiveValue
useTheme
VisuallyHidden
Components

Drawer

Component for showing additional content alongside the main page.

The <Drawer> is a slide-in panel that hosts a supplementary, in-context task alongside the main view, without blocking it. It is non-modal on desktop, falls back to a modal sheet on small screens, and is intended for light, single-step work.

Anatomy

A <Drawer> slides in alongside the main page content. It is composed of an optional title, an optional close button, the main content area, and a row of actions.

Page contentDrawerClose buttonTitleContentActions
  • Page content: The main view that stays visible and interactive while the drawer is open (non-modal on desktop).
  • Drawer: The slide-in panel itself, anchored to the side of the screen and hosting the supplementary task.
  • Title: Optional heading at the top of the panel that gives context for the task inside.
  • Close button: Optional dismiss control in the top corner, opt in via the closeButton prop.
  • Content: The body of the drawer, holding the main message or interactive elements like a form.
  • Actions: Bottom row of buttons for confirming or cancelling the task.

Appearance

The appearance of a component can be customized using the variant and size props. These props adjust the visual style and dimensions of the component, available values are based on the active theme.

The selected theme does not has any options for"variant".
PropertyTypeDescription
variant-The available variants of this component.
sizexsmall | small | mediumThe available sizes of this component.

Usage

A drawer hosts a supplementary, in-context task that benefits from staying alongside the main view. Three constraints define it:

  • It stays non-modal on desktop, so the page behind remains interactive.
  • Its content is about what is already on screen.
  • It caps task complexity at roughly five fields and a single information layer.

When in doubt, ask: is this content about the visible main view, or for it? About means a drawer fits. For means a dialog or an inline UI is the better fit.

Do

Use a drawer when

  • users inspect or adjust something already on screen,
  • the main workflow should stay uninterrupted,
  • the task is light: roughly five fields, a single information layer.

Don't

Avoid a drawer when

  • critical confirmations or destructive actions are required,
  • the flow has multiple steps, multiple save points, or its own internal navigation, which is a page,
  • the task requires full attention or a shareable URL.

Drawer vs page

The call teams most often get wrong is choosing a drawer for content that really belongs on a page. It comes down to how many information layers the content has.

  • Single layer → drawer. One coherent set of fields or content the user works through linearly, without needing to navigate inside it. A record's details, a small edit form, a filter panel, a comments feed.
  • Multiple layers → page. Content that splits into separately navigable areas: tabs across different aspects, nested editable records, drill-down sub-views, or multi-step flows where each step has its own content.

A drawer slides over the current view and is temporary (opened, used, dismissed). A page replaces it or routes somewhere new and is lasting, with its own URL, browser history, and place in the app's information architecture. Multi-layer content needs that durability to keep users oriented.

Reach for a page over a drawer when:

  • the user needs a URL to share, bookmark, or return to later,
  • the content needs tabs or internal navigation inside the panel,
  • the form has nested editable records (line items, sub-tasks with their own forms),
  • the flow has multiple steps with their own commit moments (save here, then save again there),
  • the user could reasonably get lost inside it without a back button or breadcrumbs to orient them.

Drawer vs Dialog

A user is filling out a "Create event" form and needs to pick a category that doesn't exist yet. The temptation is to slide in a drawer for "Create category" so the half-filled form stays visible behind it. This is exactly where a dialog fits better than a drawer: the new category is for that form, not about the page.

This is the canonical drawer-vs-dialog confusion. The underlying difference: a dialog is modal, blocking the page and asking the user to commit, acknowledge, or decide before continuing. A drawer is non-modal, so the page behind stays interactive while the user inspects, refines, or augments.

Choose a dialog when the user must acknowledge or confirm before moving on, especially for destructive actions. The same applies when the task simply demands full attention and interacting with the page behind would be a distraction. Choose a drawer when the work is supplementary to whatever is already on screen.

Detail-row inspection

The user's task here is "scan rows, drill into one." Keeping the list visible preserves orientation, while routing away breaks the rhythm.

Click a row in a table or list and a drawer slides in showing the record's full details. Fields are read-only or support small inline edits (a status toggle, a quick rename, picking a value from a list), and the user can move on to the next row without losing the table view.

Ticket
Priority
Status
Assignee
Action
#4521 – Login issue
High
Open
Jane Doe
#4520 – Email notifications not sent
Medium
In Progress
Marco Lee
#4519 – Dashboard loading slowly
Low
Open
Priya Singh
import {  Button,  Drawer,  Inline,  Stack,  Table,  Text,} from '@marigold/components';import type { DrawerProps } from '@marigold/components';type Ticket = {  id: string;  subject: string;  priority: 'High' | 'Medium' | 'Low';  status: 'Open' | 'In Progress' | 'Resolved';  assignee: string;  created: string;  updated: string;  description: string;  customerNote: string;};const tickets: Ticket[] = [  {    id: '4521',    subject: 'Login issue',    priority: 'High',    status: 'Open',    assignee: 'Jane Doe',    created: 'Sep 12, 2025',    updated: 'Sep 15, 2025',    description:      'User reports being unable to log in after the latest update. Error: "Invalid session token."',    customerNote: '"I tried resetting my password, but I’m still locked out."',  },  {    id: '4520',    subject: 'Email notifications not sent',    priority: 'Medium',    status: 'In Progress',    assignee: 'Marco Lee',    created: 'Sep 11, 2025',    updated: 'Sep 14, 2025',    description:      'Order confirmation emails are not arriving for customers using gmail.com addresses.',    customerNote: '"Two of my recent orders never came through to my inbox."',  },  {    id: '4519',    subject: 'Dashboard loading slowly',    priority: 'Low',    status: 'Open',    assignee: 'Priya Singh',    created: 'Sep 10, 2025',    updated: 'Sep 12, 2025',    description:      'Dashboard takes 8–10 seconds to load on first visit. Subsequent loads are normal.',    customerNote: '"It used to be much faster a few weeks ago."',  },];const TicketDrawer = ({  ticket,  ...props}: { ticket: Ticket } & DrawerProps) => (  <Drawer.Trigger>    <Button variant="secondary" size="small">      View    </Button>    <Drawer {...props} size="medium">      <Drawer.Title>        Ticket #{ticket.id} – {ticket.subject}      </Drawer.Title>      <Drawer.Content>        <Stack space="group">          <Text>            <strong>Description:</strong> {ticket.description}          </Text>          <Stack space="related">            <Text>              <strong>Status:</strong> {ticket.status}            </Text>            <Text>              <strong>Priority:</strong> {ticket.priority}            </Text>            <Text>              <strong>Assigned to:</strong> {ticket.assignee}            </Text>            <Text>              <strong>Created:</strong> {ticket.created}            </Text>            <Text>              <strong>Last Updated:</strong> {ticket.updated}            </Text>          </Stack>          <Text>            <strong>Customer Notes:</strong> {ticket.customerNote}          </Text>        </Stack>      </Drawer.Content>      <Drawer.Actions>        <Inline space="regular">          <Button slot="close">Close</Button>          <Button slot="close" variant="primary">            Resolve Ticket          </Button>        </Inline>      </Drawer.Actions>    </Drawer>  </Drawer.Trigger>);export default function (props: DrawerProps) {  return (    <Table aria-label="Support tickets">      <Table.Header>        <Table.Column rowHeader>Ticket</Table.Column>        <Table.Column>Priority</Table.Column>        <Table.Column>Status</Table.Column>        <Table.Column>Assignee</Table.Column>        <Table.Column>Action</Table.Column>      </Table.Header>      <Table.Body>        {tickets.map(ticket => (          <Table.Row key={ticket.id} id={ticket.id}>            <Table.Cell>              #{ticket.id} – {ticket.subject}            </Table.Cell>            <Table.Cell>{ticket.priority}</Table.Cell>            <Table.Cell>{ticket.status}</Table.Cell>            <Table.Cell>{ticket.assignee}</Table.Cell>            <Table.Cell>              <TicketDrawer ticket={ticket} {...props} />            </Table.Cell>          </Table.Row>        ))}      </Table.Body>    </Table>  );}

Quick edit or create from a list

Users often need to make a small change to a list, like editing one row or adding a new one, without losing their place. A drawer alongside the list handles both: a small form slides in, the user saves, the drawer closes, and the list reflects the change.

The form should be small: a handful of fields, a single save, no multi-step flow. The list stays visible behind the drawer, so the surrounding rows, filters, and sort order tell the user why they are working on this one. The "roughly five fields" rule of thumb is a proxy for the underlying constraint: a single information layer. The moment the form needs nested records or tabs to organize its fields, it has crossed into page territory.

The two flows share the same shape but differ in trigger and result:

Quick editQuick create
Trigger"Edit" on a row"Add" above the list
ResultThe row updates in placeA new row is added to the list
Outgrows the drawer whenThe edit needs nested records or multi-step validationSetup is multi-step or has its own sub-records

For the related case of creating an object that belongs to a different form on the page (e.g. a category needed inside an "Edit event" form), see Drawer vs Dialog.

Members

Name
Role
Email
Action
Anna Müller
Designer
anna@example.com
Tom Becker
Developer
tom@example.com
Sara Klein
PM
sara@example.com
import {  Button,  Drawer,  Headline,  Inline,  Select,  Stack,  Table,  TextField,} from '@marigold/components';type Member = {  id: string;  name: string;  role: string;  email: string;};const members: Member[] = [  {    id: '1',    name: 'Anna Müller',    role: 'Designer',    email: 'anna@example.com',  },  {    id: '2',    name: 'Tom Becker',    role: 'Developer',    email: 'tom@example.com',  },  {    id: '3',    name: 'Sara Klein',    role: 'PM',    email: 'sara@example.com',  },];const EditDrawer = ({ member }: { member: Member }) => (  <Drawer.Trigger>    <Button variant="secondary" size="small">      Edit    </Button>    <Drawer>      <Drawer.Title>Edit member</Drawer.Title>      <Drawer.Content>        <Stack space="regular">          <TextField label="Name" defaultValue={member.name} />          <Select label="Role" defaultSelectedKey={member.role}>            <Select.Option id="Designer">Designer</Select.Option>            <Select.Option id="Developer">Developer</Select.Option>            <Select.Option id="PM">PM</Select.Option>          </Select>          <TextField label="Email" defaultValue={member.email} />        </Stack>      </Drawer.Content>      <Drawer.Actions>        <Button slot="close">Cancel</Button>        <Button slot="close" variant="primary">          Save changes        </Button>      </Drawer.Actions>    </Drawer>  </Drawer.Trigger>);const AddDrawer = () => (  <Drawer.Trigger>    <Button>+ Add member</Button>    <Drawer>      <Drawer.Title>Add member</Drawer.Title>      <Drawer.Content>        <Stack space="regular">          <TextField label="Name" placeholder="Full name" />          <Select label="Role" placeholder="Pick a role">            <Select.Option id="Designer">Designer</Select.Option>            <Select.Option id="Developer">Developer</Select.Option>            <Select.Option id="PM">PM</Select.Option>          </Select>          <TextField label="Email" placeholder="email@example.com" />        </Stack>      </Drawer.Content>      <Drawer.Actions>        <Button slot="close">Cancel</Button>        <Button slot="close" variant="primary">          Add member        </Button>      </Drawer.Actions>    </Drawer>  </Drawer.Trigger>);export default function () {  return (    <Stack space="regular">      <Inline alignX="between" alignY="center">        <Headline level={3}>Members</Headline>        <AddDrawer />      </Inline>      <Table aria-label="Members">        <Table.Header>          <Table.Column rowHeader>Name</Table.Column>          <Table.Column>Role</Table.Column>          <Table.Column>Email</Table.Column>          <Table.Column>Action</Table.Column>        </Table.Header>        <Table.Body>          {members.map(m => (            <Table.Row key={m.id} id={m.id}>              <Table.Cell>{m.name}</Table.Cell>              <Table.Cell>{m.role}</Table.Cell>              <Table.Cell>{m.email}</Table.Cell>              <Table.Cell>                <EditDrawer member={m} />              </Table.Cell>            </Table.Row>          ))}        </Table.Body>      </Table>    </Stack>  );}

Filter panel

A collection of filter controls displayed as a side panel. The user adjusts, applies, and results update behind the panel. Non-modality is the whole point: users iterate filters and want to see the table change.

See the filter pattern for end-to-end guidance, including applied-filter display.

Comments, activity, and history

The user's task here is "what's happened to this, and what should I add to it?" Long-lived records like tickets, pull requests, and issues accumulate context: comments, status changes, assignments, audit entries. A drawer slides in alongside the record and holds the whole feed, so users can scan the history, catch up on a thread, or post a reply without leaving the main view.

The pattern fits a drawer because:

  • the feed grows long and needs its own scroll,
  • users want the main record visible while reading or replying,
  • activity is something to glance at occasionally, not the focus of the page.

If activity is the primary task (a chat-first product, for example), it belongs in the main column, not in a drawer.

Ticket #4521 – Login issue

High priority
User reports being unable to log in after the latest update. Error: "Invalid session token."
Assigned to Jane Doe · Updated 2 hours ago
import {  Badge,  Button,  Card,  Drawer,  Headline,  Inline,  Stack,  Text,  TextField,} from '@marigold/components';import type { DrawerProps } from '@marigold/components';export default function (props: DrawerProps) {  return (    <Card>      <Stack space="regular">        <Stack space="regular">          <Inline space="related" alignY="center">            <Headline level={3}>Ticket #4521 – Login issue</Headline>            <Badge variant="warning">High priority</Badge>          </Inline>          <Text>            User reports being unable to log in after the latest update. Error:{' '}            <em>"Invalid session token."</em>          </Text>          <Text>            Assigned to <strong>Jane Doe</strong> · Updated 2 hours ago          </Text>        </Stack>        <Inline>          <Drawer.Trigger>            <Button variant="secondary">View activity (3)</Button>            <Drawer {...props} size="medium">              <Drawer.Title>Activity · Ticket #4521</Drawer.Title>              <Drawer.Content>                <Stack space="group">                  <Stack space="tight">                    <Inline space="related" alignY="center">                      <Text weight="bold">Jane Doe</Text>                      <Text>changed status to Open</Text>                    </Inline>                    <Text>2 hours ago</Text>                  </Stack>                  <Stack space="tight">                    <Inline space="related" alignY="center">                      <Text weight="bold">Sam Müller</Text>                      <Text>commented</Text>                    </Inline>                    <Text>                      Customer reset their password but is still locked out.                      Asked them to clear cookies and try again.                    </Text>                    <Text>4 hours ago</Text>                  </Stack>                  <Stack space="tight">                    <Inline space="related" alignY="center">                      <Text weight="bold">Jane Doe</Text>                      <Text>assigned to Sam Müller</Text>                    </Inline>                    <Text>Yesterday</Text>                  </Stack>                  <TextField                    label="Add a comment"                    placeholder="Write a reply…"                  />                </Stack>              </Drawer.Content>              <Drawer.Actions>                <Button slot="close">Close</Button>                <Button slot="close" variant="primary">                  Post comment                </Button>              </Drawer.Actions>            </Drawer>          </Drawer.Trigger>        </Inline>      </Stack>    </Card>  );}

Contextual utility

Persistent or summon-able utilities that support the main task without being part of it: in-app help, support chat, AI assistant, scratchpad. These utilities live alongside the main work, available when needed but never the focus.

The right shape is a drawer the user can pin or summon, not a modal or a popover. It opens when needed and never takes focus from the main task.

import { Accordion, Button, Drawer, Stack, Text } from '@marigold/components';import type { DrawerProps } from '@marigold/components';export default function (props: DrawerProps) {  return (    <Drawer.Trigger>      <Button>Open Help</Button>      <Drawer {...props}>        <Drawer.Title>Quick Help</Drawer.Title>        <Drawer.Content>          <Stack space="regular">            <Text>              Common questions while handling tickets. Expand a section for the              full answer.            </Text>            <Accordion>              <Accordion.Item id="reset-password">                <Accordion.Header>                  How do I reset a user's password?                </Accordion.Header>                <Accordion.Content>                  Open the user record, click <strong>Account</strong>, then{' '}                  <strong>Send password reset</strong>. The user receives an                  email with a one-time link valid for 24 hours.                </Accordion.Content>              </Accordion.Item>              <Accordion.Item id="escalation">                <Accordion.Header>                  When should I escalate a ticket?                </Accordion.Header>                <Accordion.Content>                  Escalate any ticket marked <strong>High priority</strong> that                  has been open longer than four hours, or any ticket flagged by                  the customer as a security concern.                </Accordion.Content>              </Accordion.Item>              <Accordion.Item id="login-loop">                <Accordion.Header>                  Customer is stuck in a login loop. What now?                </Accordion.Header>                <Accordion.Content>                  Most login loops resolve when the customer clears site cookies                  and reauthenticates. If that fails, verify their SSO provider                  status before assigning a developer.                </Accordion.Content>              </Accordion.Item>              <Accordion.Item id="refund">                <Accordion.Header>How do I issue a refund?</Accordion.Header>                <Accordion.Content>                  Refunds under €50 can be issued from the order detail page.                  Anything above that amount needs manager approval. Flag the                  ticket with <strong>refund-review</strong>.                </Accordion.Content>              </Accordion.Item>            </Accordion>          </Stack>        </Drawer.Content>        <Drawer.Actions>          <Button slot="close">Close</Button>        </Drawer.Actions>      </Drawer>    </Drawer.Trigger>  );}

Multi-field bulk edit

Users select rows in a table, an ActionBar appears, and one of its actions opens a drawer with a small form for editing several fields across the selection (for example, changing the date and venue across many events at once).

The drawer keeps the selected rows visible so users can confirm the selection while editing. Bulk edit stays within the single-layer rule because the fields are flat and identical across all selected records: one form applied to N rows, not N forms or nested edits. The boundary is clean: single-verb bulk actions (delete, archive, assign) belong in the action bar, while multi-field bulk edit belongs in a drawer triggered from it. Destructive bulk actions still need a confirm dialog after the verb.

Event
Date
Venue
Status
Spring Gala
Apr 15, 2025
Freiburg
Confirmed
Jazz Night
May 2, 2025
Berlin
Confirmed
Open Air Theater
Jun 10, 2025
Hamburg
On Sale
Summer Festival
Jul 22, 2025
Berlin
Confirmed
import { Pencil } from 'lucide-react';import {  ActionBar,  Badge,  Button,  Checkbox,  DatePicker,  Drawer,  Select,  Stack,  Table,  Text,} from '@marigold/components';import type { DrawerProps } from '@marigold/components';const events = [  {    id: '1',    name: 'Spring Gala',    date: 'Apr 15, 2025',    venue: 'Freiburg',    status: 'Confirmed',  },  {    id: '2',    name: 'Jazz Night',    date: 'May 2, 2025',    venue: 'Berlin',    status: 'Confirmed',  },  {    id: '3',    name: 'Open Air Theater',    date: 'Jun 10, 2025',    venue: 'Hamburg',    status: 'On Sale',  },  {    id: '4',    name: 'Summer Festival',    date: 'Jul 22, 2025',    venue: 'Berlin',    status: 'Confirmed',  },];export default function (props: DrawerProps) {  return (    <Table      aria-label="Events"      selectionMode="multiple"      defaultSelectedKeys={new Set(['1', '2', '3'])}      actionBar={() => (        <ActionBar>          <Drawer.Trigger>            <ActionBar.Button>              <Pencil />              Edit            </ActionBar.Button>            <Drawer {...props} size="medium">              <Drawer.Title>Edit selected events</Drawer.Title>              <Drawer.Content>                <Stack space="regular">                  <Text>                    Changes will apply to all selected events. Empty fields stay                    unchanged.                  </Text>                  <DatePicker label="Event date" />                  <Select label="Venue" placeholder="Choose a venue">                    <Select.Option id="freiburg">Freiburg</Select.Option>                    <Select.Option id="berlin">Berlin</Select.Option>                    <Select.Option id="hamburg">Hamburg</Select.Option>                    <Select.Option id="online">Online</Select.Option>                  </Select>                  <Checkbox label="Notify attendees of changes" />                </Stack>              </Drawer.Content>              <Drawer.Actions>                <Button slot="close">Cancel</Button>                <Button slot="close" variant="primary">                  Apply changes                </Button>              </Drawer.Actions>            </Drawer>          </Drawer.Trigger>        </ActionBar>      )}    >      <Table.Header>        <Table.Column rowHeader>Event</Table.Column>        <Table.Column>Date</Table.Column>        <Table.Column>Venue</Table.Column>        <Table.Column>Status</Table.Column>      </Table.Header>      <Table.Body>        {events.map(event => (          <Table.Row key={event.id} id={event.id}>            <Table.Cell>{event.name}</Table.Cell>            <Table.Cell>{event.date}</Table.Cell>            <Table.Cell>{event.venue}</Table.Cell>            <Table.Cell>              <Badge>{event.status}</Badge>            </Table.Cell>          </Table.Row>        ))}      </Table.Body>    </Table>  );}

Accessibility

A drawer renders with the ARIA role complementary by default, which fits almost every use case in this guide. Two exceptions are worth knowing:

  • For a filter panel, consider role="search" to give screen-reader users a more precise landmark.
  • For long-lived activity panels, consider role="region" with an aria-label if the panel is part of the persistent layout.

On small screens, the drawer falls back to a modal sheet that traps focus inside the panel. On desktop the drawer is non-modal, so focus only returns to the trigger when the drawer closes. Dismissal on Escape is handled automatically.

Props

Did you know? You can explore, test, and customize props live in Marigold's storybook. Watch the effects they have in real-time!
View Drawer stories

Drawer

Prop

Type

Accessibility props (5)

Prop

Type

DOM event handlers (64)

Prop

Type

Drawer.Trigger

Prop

Type

Drawer.Title

Prop

Type

Drawer.Content

Prop

Type

Drawer.Actions

Prop

Type

Alternative components

  • Dialog: Use for modal, attention-required moments like confirmations, destructive actions, and parent-form quick-create.
  • ActionBar: Use for single-verb bulk actions on a selection (delete, archive, assign). Pair with a drawer when bulk-editing several fields.
  • Menu: Use for a small set of toggles or quick choices, like table column settings, sort order, or density. A drawer is overkill when the controls are a handful of options that fit inline next to their trigger.
  • Sidebar: Use for app-level navigation, not for in-context content alongside a record.

Related

Filter pattern

End-to-end guidance for filter UIs, including drawer-based filter panels.
Last update: 7 days ago

Dialog

Component for displaying dialogs.

Menu

Flexible component for constructing dynamic and customizable menus.

On this page

AnatomyAppearanceUsageDrawer vs pageDrawer vs DialogDetail-row inspectionQuick edit or create from a listFilter panelComments, activity, and historyContextual utilityMulti-field bulk editAccessibilityPropsDrawerDrawer.TriggerDrawer.TitleDrawer.ContentDrawer.ActionsAlternative componentsRelated