Marigold
v18.0.0-beta.4
Marigold
v18.0.0-beta.4

Application

MarigoldProvider
RouterProvider

Layout

AppShellbeta
Aside
Aspect
Center
Columns
Container
Grid
Inline
Inset
Pagebeta
Panelbeta
Scrollable
Split
Stack
Tiles

Actions

Buttonupdated
ButtonGroupbeta
Link
LinkButton
ToggleButtonbeta

Form

Autocomplete
Calendar
Checkbox
ComboBox
DateField
DatePicker
DateRangePickerbeta
FileField
Form
NumberField
Radio
RangeCalendaralpha
SearchField
SegmentedControlbeta
Select
SelectListupdated
Slider
Switchupdated
TagFieldbeta
TextArea
TextField
TimeField

Collection

Cardupdated
Table
Tag
ActionBaralpha

Navigation

Accordion
Breadcrumbs
Pagination
Sidebarbeta
Tabs
TopNavigationbeta

Overlay

ActionMenualpha
ContextualHelp
Dialog
Drawer
Menuupdated
Toastbeta
Tooltip

Content

Badge
Descriptionalpha
Divider
EmptyStatebeta
ErrorStatebeta
Headline
Keyboardbeta
List
Loader
SectionMessage
SVG
Text
TextValuealpha
Titlealpha

Formatters

DateFormat
NumericFormat

Hooks and Utils

cn
cva
extendTheme
parseFormData
useAsyncListData
useLandmark
useListData
useTheme
VisuallyHidden
Components

Panel

Organize page content into labeled regions.

A <Panel> organizes page content into labeled sections. Use it to split a settings, profile, or admin page into regions, each with its own title, content, and optional actions. What goes inside is flexible: form fields, tables, media, or plain text.

A typical page has a handful of Panels, each owning one topic: "Organizer profile", "Team members", "Billing". Every Panel has a unique title and renders as a landmark region labelled by that title, so the page outline stays meaningful to assistive tech.

Panels live inside a <Page> under a <Page.Header>. Panel's default headingLevel={2} assumes a page <h1> above it (which <Page.Header> provides), so the outline reads h1 (page) → h2 (Panel) → h3 (Panel collapsible) with no configuration.

Is a Panel a Card?

They look similar, but they're not interchangeable. A Panel owns a region of a page, with its own title, description, and optional actions. A Card just stands in for one item in a list or grid. Cards repeat. Panels don't.

Anatomy

TitleDescriptionContentFooterHeader actions

A Panel has three areas, all optional, each backed by its own sub-component:

  • Header contains a title, an optional description, and optional actions slot for buttons or overflow menus.
  • Content is the main body. Form fields, text, tables, or any layout primitive lives here.
  • Footer is an optional action bar for panel-scoped actions. Often omitted, since header actions cover most section-scoped controls and page-level Save/Cancel sit outside the <Panel>.

A Panel can also wrap part of its content in a Collapsible to hide fields until the user opens them. A Collapsible has two parts: a header that names the section, and the content it reveals. The header follows the same pattern as the Panel header above: a title, with an optional description underneath. The whole header is clickable, and a caret on the right marks whether the section is open.

TitleDescriptionHeaderContent

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.

Featured venues

A hand-picked selection of spaces across three countries, ready for your next booking.

Grand Avenue Ballroom
Shakytown, United States · 800 guests
Main Street Park Amphitheater
Laughville, United States · 500 guests
Maple Court Theatre
Hee-Haw City, Canada · 400 guests
PropertyTypeDescription
variantdefault | master | admin | destructiveThe available variants of this component.
sizedefault | formThe available sizes of this component.

Every Panel separates from the page by fill, not by a shadow: a white surface against the gray page background, traced by a quiet rim.

Panel has four variants. They match the ones in Card and Badge so master, admin, and destructive sections read consistently, whether they show up as a collection item or a page region.

VariantDescriptionWhen to use
defaultSubtle surface tint.Most page sections: profile, notifications, billing.
masterTint and accent for master users.Sections only master users see. Learn more.
adminTint and accent for admin users.Sections only admin users see. Learn more.
destructiveTint for danger zones.Irreversible actions, like deleting an account or revoking access.

Panel also offers two sizes. The default is unconstrained and grows with its container. The form size caps the Panel at a comfortable reading width so long label-input pairs don't stretch across the page.

SizeDescriptionWhen to use
defaultFull width of the parent.Data-heavy sections: tables, lists, dashboards, prose.
formCaps the Panel at a reading width.Form-heavy sections so fields stay easy to scan top-to-bottom.

Usage

Panel is the primary tool for giving a page structure. Dense screens, like settings, admin forms, or dashboards, hold more fields than a user can scan in one pass. Breaking that content into labeled regions turns a wall of inputs into scannable topics.

A page typically has two to six Panels, each owning one topic. A settings page, for example, might split into "Store Information", "Regional Settings", and "Notifications". Each Panel groups related fields under a single heading, so a user can find the section they need without reading everything before it.

Panel is content-agnostic: form fields, tables, media, or text blocks can all live inside. The one rule is topical cohesion. Everything in a Panel should share the topic its title names. Without that, the Panel stops being a region and turns into a box.

Panel vs Card

PanelCard

Panel and Card look similar, but they solve different problems. A Panel is a named region of a page, usually unique, with its own title and (sometimes) actions. A Card stands in for one item in a collection and is built to repeat.

Rule of thumb

If the element repeats as you scroll (events, products, team members), reach for a Card. If it owns a page region on its own, like "Organizer profile", "Team members", or "Billing", reach for a Panel.

Header

The header names the section and tells the user what it's about before they read a single field. It has three slots: a title, an optional description, and an optional actions region on the right (covered in Actions below). Each slot has its own job.

Title

The title is how a user finds this section when they scan the page. Aim for a short noun phrase, two to five words, that names the topic.

Do

Keep titles specific and unique on the page.

Do

Use sentence case.

Do

Front-load the meaningful keyword so the user recognizes the section at a glance.

Don't

Don't use a generic heading that two sections could share.

Every Panel needs a name

A Panel is a landmark region, and assistive technologies need a name for it. Include a title, or pass an aria-label on the Panel itself when the section does not have a visible title.

Description

A description is a single-sentence subtitle directly under the title. Include one when the title alone doesn't tell the user what's at stake, or when the section introduces a concept they may not have seen before. Skip it when the title already says it. A description that restates the heading adds reading weight without adding information.

The header configures its children

Panel.Header is the layout wrapper that arranges title, description, and actions in a grid. Drop a <Title>, an optional <Description>, and any buttons (<Button>, <ButtonGroup>, <ActionMenu>, <LinkButton>) directly inside it. The header configures slot context so each piece picks up its level, id wiring, grid position, size, and variant from the Panel, so consumers don't need to set any of that themselves. A bare <Button> is slot-aware here. Opt one out of the cascade with slot={null}.

Multiple actions belong in a <ButtonGroup> so the cluster claims one grid cell and renders as a toolbar.

Title-only Panels can skip the header

When a Panel has only a title (no description, no actions), drop a bare <Title> directly inside <Panel> and skip the wrapper. Accessibility wires up the same way (aria-labelledby resolves to the title), and the title still aligns with the panel's horizontal padding. As soon as a Panel needs a description or actions, reach for <Panel.Header> so the slot context arranges them in the grid.

Quick settings

import { Panel, Stack, TextField, Title } from '@marigold/components';export default () => (  <Panel>    <Title>Quick settings</Title>    <Panel.Content>      <Stack space="regular">        <TextField label="Display name" defaultValue="Marigold Events" />        <TextField label="Support email" defaultValue="hello@marigold-ui.io" />      </Stack>    </Panel.Content>  </Panel>);

Content

The content area is the body of the Panel. It's deliberately open, and in practice most sections you'll build are one of three shapes:

  • Form sections group related fields under one topic, like "Store information" or "Notifications". Stack the fields so labels and inputs share a vertical rhythm.
  • Data sections put a single table, list, chart, diagram, or media surface at the centre of the region. These usually want full-width content.
  • Prose sections explain a concept or a consequence before the user acts, common above a Danger zone or at the top of a settings page.

Panel doesn't impose a layout primitive. Reach for the same tools you'd use outside a Panel (<Stack>, <Inline>, a grid). The Panel owns the region. The content inside still owns its own rhythm.

Full-width content

By default, content sits inside the Panel's horizontal padding so its left and right edges line up with the header and footer. That shared edge is what makes the Panel read as one region.

Tables, media, charts, and status bars are the exceptions. They look cramped when inset and read best edge-to-edge. Opt in with the bleed prop on the content area.

Recent bookings

Last 7 days, across all events.

Booking
Event
Customer
Tickets
Amount
Status
BKG-2401
Summer Comedy Night
Alice Chen
2
€48.00
Confirmed
BKG-2402
Oak Ridge Wedding Showcase
Bob Martinez
4
€96.00
Pending
BKG-2403
Harborfront Jazz Festival
Charlie Park
1
€65.00
Confirmed
BKG-2404
Grand Avenue Gala
Diana Wong
8
€480.00
Refunded
BKG-2405
Maple Court Film Night
Eli Brown
3
€45.00
Confirmed
Showing 1 - 5 of 810
…
import { useState } from 'react';import {  Badge,  Description,  Inline,  Pagination,  Panel,  Table,  Text,  Title,} from '@marigold/components';import { NumericFormat } from '@marigold/system';const bookings = [  {    id: 'BKG-2401',    event: 'Summer Comedy Night',    customer: 'Alice Chen',    tickets: 2,    amount: 48,    status: 'Confirmed',    variant: 'success',  },  {    id: 'BKG-2402',    event: 'Oak Ridge Wedding Showcase',    customer: 'Bob Martinez',    tickets: 4,    amount: 96,    status: 'Pending',    variant: 'warning',  },  {    id: 'BKG-2403',    event: 'Harborfront Jazz Festival',    customer: 'Charlie Park',    tickets: 1,    amount: 65,    status: 'Confirmed',    variant: 'success',  },  {    id: 'BKG-2404',    event: 'Grand Avenue Gala',    customer: 'Diana Wong',    tickets: 8,    amount: 480,    status: 'Refunded',    variant: 'error',  },  {    id: 'BKG-2405',    event: 'Maple Court Film Night',    customer: 'Eli Brown',    tickets: 3,    amount: 45,    status: 'Confirmed',    variant: 'success',  },] as const;const pageSize = bookings.length;const totalItems = pageSize * 162;export default () => {  const [page, setPage] = useState(1);  const start = (page - 1) * pageSize + 1;  const end = Math.min(start + pageSize - 1, totalItems);  return (    <Panel>      <Panel.Header>        <Title>Recent bookings</Title>        <Description>Last 7 days, across all events.</Description>      </Panel.Header>      <Panel.Content bleed>        <Table aria-label="Recent bookings">          <Table.Header>            <Table.Column rowHeader>Booking</Table.Column>            <Table.Column minWidth={200}>Event</Table.Column>            <Table.Column>Customer</Table.Column>            <Table.Column alignX="right">Tickets</Table.Column>            <Table.Column alignX="right">Amount</Table.Column>            <Table.Column width={120}>Status</Table.Column>          </Table.Header>          <Table.Body items={bookings}>            {booking => (              <Table.Row id={booking.id}>                <Table.Cell>{booking.id}</Table.Cell>                <Table.Cell>{booking.event}</Table.Cell>                <Table.Cell>{booking.customer}</Table.Cell>                <Table.Cell>{booking.tickets}</Table.Cell>                <Table.Cell>                  <NumericFormat                    style="currency"                    currency="EUR"                    value={booking.amount}                  />                </Table.Cell>                <Table.Cell>                  <Badge variant={booking.variant}>{booking.status}</Badge>                </Table.Cell>              </Table.Row>            )}          </Table.Body>        </Table>      </Panel.Content>      <Panel.Content>        <Inline alignY="center" alignX="between">          <Text fontSize="sm">            Showing {start} - {end} of {totalItems}          </Text>          <Pagination            totalItems={totalItems}            pageSize={pageSize}            page={page}            onChange={setPage}          />        </Inline>      </Panel.Content>    </Panel>  );};

Tables get a small bonus: when bled, their cell edges line up with the Panel's header and footer, so what's inside a row still reads in rhythm with the title above it.

An <Accordion> behaves the same way. Drop one into a bled content area and its item dividers span edge-to-edge while the header and content stay inset, aligned with the Panel title. No extra prop is needed: the Accordion picks up the Panel's horizontal padding automatically, exactly like Table cells do.

Reach for this only when the Panel's body is a set of peer, equally-weighted sections that each expand: a per-field breakdown, a list of records each with details. For a single optional or advanced fold alongside visible primary content, use Panel.Collapsible instead. The two aren't interchangeable: a stack of Collapsibles is exactly the anti-pattern Accordion exists to solve (see One Collapsible per Panel).

Prop values

Usage across all Button instances, last 30 days.

primary
63%
secondary
27%
destructive
10%

default
81%
small
19%

false
93%
true
7%
import {  Accordion,  Badge,  Description,  Inline,  Panel,  Stack,  Text,  Title,} from '@marigold/components';const props = [  {    name: 'variant',    total: 1284,    values: [      { value: 'primary', count: 812 },      { value: 'secondary', count: 341 },      { value: 'destructive', count: 131 },    ],  },  {    name: 'size',    total: 1284,    values: [      { value: 'default', count: 1043 },      { value: 'small', count: 241 },    ],  },  {    name: 'disabled',    total: 1284,    values: [      { value: 'false', count: 1198 },      { value: 'true', count: 86 },    ],  },] as const;export default () => (  <Panel>    <Panel.Header>      <Title>Prop values</Title>      <Description>        Usage across all Button instances, last 30 days.      </Description>    </Panel.Header>    <Panel.Content bleed>      <Accordion defaultExpandedKeys={['variant']}>        {props.map(prop => (          <Accordion.Item key={prop.name} id={prop.name}>            <Accordion.Header>              <Inline space={2} alignY="center">                <Text>{prop.name}</Text>                <Badge>{prop.values.length}</Badge>              </Inline>            </Accordion.Header>            <Accordion.Content>              <Stack space={2}>                {prop.values.map(entry => (                  <Inline key={entry.value} alignX="between" alignY="center">                    <Text fontSize="sm">{entry.value}</Text>                    <Text fontSize="sm" color="secondary">                      {Math.round((entry.count / prop.total) * 100)}%                    </Text>                  </Inline>                ))}              </Stack>            </Accordion.Content>          </Accordion.Item>        ))}      </Accordion>    </Panel.Content>  </Panel>);

Two details are intentional for now: there is no divider between the Panel.Header and the first item, and the last item's bottom corners stay square rather than following the Panel's radius.

Collapsible

Some sections have fields most users won't touch, like legacy SEO settings, import defaults, or an optional webhook URL. Hiding them by default keeps the Panel scannable for the common case, while staying one click away for anyone who needs them.

Reach for a Collapsible when a handful of fields in a Panel are:

  • Optional, and unrelated to the primary action of the section.
  • Advanced, meant for power users who already know what they're looking for.
  • Uncommon, edited rarely enough that showing them by default adds noise.

Event details

Information shown on the event page, confirmations, and tickets.

Start date
MM/DD/YYYY
Venue

Used in the event page URL.
import { venues } from '@/lib/data/venues';import {  DatePicker,  Description,  Panel,  Select,  Stack,  TextField,  Title,} from '@marigold/components';export default () => (  <Panel size="form">    <Panel.Header>      <Title>Event details</Title>      <Description>        Information shown on the event page, confirmations, and tickets.      </Description>    </Panel.Header>    <Panel.Content>      <Stack space="regular">        <TextField label="Event title" defaultValue="Summer Comedy Night" />        <DatePicker label="Start date" />        <Select label="Venue" placeholder="Select a venue">          {venues.slice(0, 6).map(v => (            <Select.Option key={v.id} id={v.id}>              {v.name}            </Select.Option>          ))}        </Select>      </Stack>    </Panel.Content>    <Panel.Collapsible>      <Panel.CollapsibleHeader>        <Title>SEO &amp; tracking</Title>        <Description>          Optional metadata used on the event page and in social shares.        </Description>      </Panel.CollapsibleHeader>      <Panel.CollapsibleContent>        <Stack space="regular">          <TextField            label="URL slug"            description="Used in the event page URL."            defaultValue="summer-comedy-night"          />          <TextField label="Analytics tracking ID" />          <TextField label="Social share image URL" />        </Stack>      </Panel.CollapsibleContent>    </Panel.Collapsible>  </Panel>);

What to keep visible

A Collapsible is for fields that are safe to miss. Anything else belongs in the main content, where users can scan it without clicking. In particular, don't fold away:

  • Required fields. Hiding something a user must fill in to save the form is the classic trap. They submit, the form errors inside a collapsed section, and they're left hunting for an invisible field. If you find yourself wanting to fold one away, that's usually a sign something else is off: maybe the field isn't really required, or the section deserves its own Panel.
  • Content central to the Panel's topic. If a Panel is "Billing address", hiding the address fields themselves defeats the Panel. A Collapsible is for the periphery of the topic, not the topic itself.
  • Non-obvious defaults. Settings the user would want to notice, like a timezone pinned to "UTC" on a regional account, shouldn't sit behind a fold. Out of sight means out of audit.
  • An overloaded Panel. If a Panel feels too long, split it into two Panels with clearer names. A Collapsible that folds away half the content is usually papering over the wrong problem.
  • Fields with validation errors. Even optional fields can have format errors: an invalid tracking ID, a malformed webhook URL. When a Collapsible is closed and one of its fields fails validation on submit, the error message has no visible target. Auto-expand the section so the error shows up where the user can see it. The demo below shows the pattern.

The example wraps the Panel in a <Form> and validates on submit. The invalid tracking ID is pre-filled inside a closed Collapsible. Click Save to run the submit handler: it reads the form data, checks the field, and forces the Collapsible open when validation fails.

Event details

Format: G-XXXXXXXXXX
import { useState } from 'react';import type { ComponentProps } from 'react';import {  Button,  Description,  Form,  Inline,  Panel,  Stack,  TextField,  Title,  parseFormData,} from '@marigold/components';const TRACKING_ID_PATTERN = /^G-[A-Z0-9]{10}$/;type SubmitHandler = NonNullable<ComponentProps<typeof Form>['onSubmit']>;export default () => {  const [expanded, setExpanded] = useState(false);  const [error, setError] = useState(false);  const handleSubmit: SubmitHandler = event => {    event.preventDefault();    const { trackingId = '' } = parseFormData<{ trackingId: string }>(event);    const invalid = trackingId !== '' && !TRACKING_ID_PATTERN.test(trackingId);    setError(invalid);    if (invalid) setExpanded(true);  };  return (    <Form onSubmit={handleSubmit}>      <Stack space="regular">        <Panel size="form">          <Panel.Header>            <Title>Event details</Title>          </Panel.Header>          <Panel.Content>            <TextField              name="title"              label="Event title"              defaultValue="Summer Comedy Night"            />          </Panel.Content>          <Panel.Collapsible expanded={expanded} onExpandedChange={setExpanded}>            <Panel.CollapsibleHeader>              <Title>SEO &amp; tracking</Title>              <Description>                Optional metadata used on the event page and in social shares.              </Description>            </Panel.CollapsibleHeader>            <Panel.CollapsibleContent>              <TextField                name="trackingId"                label="Analytics tracking ID"                description="Format: G-XXXXXXXXXX"                errorMessage="That does not look like a valid Google Analytics ID."                defaultValue="invalid-id-123"                onChange={() => {                  if (error) setError(false);                }}                error={error}              />            </Panel.CollapsibleContent>          </Panel.Collapsible>        </Panel>        <Inline space="related" alignY="center">          <Button variant="primary" type="submit">            Save          </Button>          <Button variant="secondary" type="button">            Cancel          </Button>        </Inline>      </Stack>    </Form>  );};

One Collapsible per Panel

A Panel owns one topic, named by its title. A single Collapsible folds away the topic's periphery while the visible content still carries the section. Two or more folds flip that relationship: users see the title, a stack of closed triggers, and nothing to orient them. The Panel stops being a section and starts being a navigator.

When a Panel seems to want a second Collapsible, the shape is usually off in one of two ways:

  • The Panel is trying to own two topics. Split it into two Panels with clearer titles. Each gets its own scannable content and, if they need it, their own single Collapsible.
  • The content is a set of peer sub-sections that all live at the same level. Reach for Accordion instead. Accordion is built for parallel collapsible items. Panel is not.

A Panel made entirely of Collapsibles runs into the same problem: the title labels an empty container, and the user has to click before anything reads. A single fold in an otherwise bare Panel can work when the Panel is purely a wrapper for one advanced section. Anything more should be an Accordion.

Actions

A Panel has three places for actions, each carrying a different scope:

  • Header. For controls that act on the whole section, like "Invite member" or "Reset to defaults". Putting them in the header anchors them to the title, so their scope is clear: they act on this section, not on the page or on a row inside it. Keep the set small: one prominent button, or an overflow menu when a handful of related controls belong together. More than that usually means the scope is too broad, and one of the actions probably belongs in its own Panel.

Team members

People with access to this workspace and their roles.

Chip 'Pixel' Patterson
Chief Button Herder
Admin
Dr. Alex 'Token' Chen
Font Whisperer & Color Alchemist
Editor
Sandy 'Specs' Vega
Director of Component Choreography
Editor
Diana 'Crash' Martinez
Professional Bug Hunter
Viewer
import { people } from '@/lib/data/people';import {  ActionMenu,  Badge,  Button,  Description,  Inline,  Panel,  Stack,  Text,  Title,} from '@marigold/components';import { UserRoundPlus } from '@marigold/icons';const members = [  { ...people[0], role: 'Admin', variant: 'admin' },  { ...people[1], role: 'Editor', variant: 'default' },  { ...people[2], role: 'Editor', variant: 'default' },  { ...people[3], role: 'Viewer', variant: 'default' },];export default () => (  <Panel>    <Panel.Header>      <Title>Team members</Title>      <Description>        People with access to this workspace and their roles.      </Description>      <Button size="icon" aria-label="Invite member">        <UserRoundPlus />      </Button>    </Panel.Header>    <Panel.Content>      <Stack space="regular">        {members.map(member => (          <Inline            key={member.id}            space="regular"            alignY="center"            alignX="between"          >            <Inline space="related" alignY="center">              <img                src={member.avatar}                alt=""                className="size-9 shrink-0 rounded-full object-cover"              />              <Stack space="tight">                <Text weight="medium">{member.name}</Text>                <Text size="xs" color="secondary">                  {member.position}                </Text>              </Stack>            </Inline>            <Inline space="regular" alignY="center">              <Badge variant={member.variant}>{member.role}</Badge>              <ActionMenu size="icon" aria-label={`Manage ${member.name}`}>                <ActionMenu.Item id="change-role">Change role</ActionMenu.Item>                <ActionMenu.Item id="resend-invite">                  Resend invite                </ActionMenu.Item>                <ActionMenu.Item id="remove" variant="destructive">                  Remove from workspace                </ActionMenu.Item>              </ActionMenu>            </Inline>          </Inline>        ))}      </Stack>    </Panel.Content>  </Panel>);
  • Footer. For a single, weighty action scoped to the Panel, most often a destructive one. Rare, because header actions cover most section-scoped controls and page-level Save/Cancel sit outside the Panel. Reach for it when a Panel needs its own action space. See Danger zone below for the canonical pattern.
  • Outside the Panel. For page-level controls like Save changes and Cancel at the bottom of a settings form. A Panel owns one region. A page-level action crosses every region on the page, and anchoring it to any single Panel misrepresents its scope.

Actions that belong to a row or item inside the content stay with their row, not in the Panel's header or footer.

Danger zone

A Danger zone is a clearly-marked section for actions that can't be undone: deleting an account, wiping data, transferring ownership, revoking access. Use the destructive variant so the surface itself warns the user, and put it last so it stays out of the way while they're editing safer settings.

Reach for a Danger zone when the action is one of these:

  • Not reversible. Deleting records. Wiping logs. Cancelling a subscription with no grace period.
  • Removes access or changes identity. Transferring ownership, kicking someone off the workspace, rotating the primary API key.
  • Affects people outside the current account. Cancelling events that customers hold tickets for. Closing a workspace other people depend on.

Anything reversible or low-stakes, like sign-out or reset-to-defaults, does not need a Danger zone. Keep it in the normal flow.

Most Danger zones hold one action. Put it in the Footer (see Actions for why), with a short paragraph in the Content explaining what disappears. Lead with the consequence, not the verb.

Delete organizer

This is permanent. There's no undo.

Deleting the organizer removes the account along with every published event, past booking, and customer record. Ticket holders are notified and refunded automatically.
import { Button, Description, Panel, Text, Title } from '@marigold/components';export default () => (  <Panel variant="destructive">    <Panel.Header>      <Title>Delete organizer</Title>      <Description>This is permanent. There's no undo.</Description>    </Panel.Header>    <Panel.Content>      <Text>        Deleting the organizer removes the account along with every published        event, past booking, and customer record. Ticket holders are notified        and refunded automatically.      </Text>    </Panel.Content>    <Panel.Footer>      <Button variant="destructive">Delete organizer</Button>    </Panel.Footer>  </Panel>);

If a couple of destructive actions genuinely belong together (say "Archive" next to "Delete"), stack them as rows inside the Content, each with a one-line consequence and its own destructive button. The Footer stays empty in that shape.

Danger zone

Permanent actions that affect this organizer. There's no undo.

Archive organizer
Hides every event from customers and locks the account. Past bookings and reports stay available.
Transfer ownership
Hands the organizer, its events, and its billing details to another user. You lose access immediately.
Delete organizer
Permanently removes the account, every event, and all booking history. Ticket holders are notified and refunded.
import {  Button,  Description,  Inline,  Panel,  Stack,  Text,  Title,} from '@marigold/components';const actions = [  {    id: 'archive',    label: 'Archive organizer',    description:      'Hides every event from customers and locks the account. Past bookings and reports stay available.',    action: 'Archive',  },  {    id: 'transfer',    label: 'Transfer ownership',    description:      'Hands the organizer, its events, and its billing details to another user. You lose access immediately.',    action: 'Transfer',  },  {    id: 'delete',    label: 'Delete organizer',    description:      'Permanently removes the account, every event, and all booking history. Ticket holders are notified and refunded.',    action: 'Delete',  },];export default () => (  <Panel variant="destructive">    <Panel.Header>      <Title>Danger zone</Title>      <Description>        Permanent actions that affect this organizer. There's no undo.      </Description>    </Panel.Header>    <Panel.Content>      <Stack space="regular">        {actions.map(item => (          <Inline key={item.id} alignY="center" alignX="between" space="group">            <Stack space="tight">              <Text weight="medium">{item.label}</Text>              <Text size="xs" color="secondary">                {item.description}              </Text>            </Stack>            <Button variant="destructive-ghost">{item.action}</Button>          </Inline>        ))}      </Stack>    </Panel.Content>  </Panel>);

Spacing

Panel controls two kinds of spacing: the padding inside each slot, and the gap between slots. The defaults suit most settings pages. Reach for the overrides when a Panel is unusually dense or unusually roomy.

Padding

Every slot is inset with p="square-regular". Stay with the default in most cases. Use square-loose when a Panel holds long prose or sits alone on the page, and px/py when content is wide but short and uniform padding feels cramped vertically.

<Panel p="square-loose">…</Panel>
<Panel px="padding-relaxed" py="padding-snug">…</Panel>

Section gap

space controls the gap between header, content, and footer. Default regular works almost everywhere. Drop to none when the slots should read as one dense surface, like a full-width table that reaches the Panel's edges. Step up to group when the Panel carries several distinct blocks that need more breathing room.

<Panel space="none">…</Panel>
<Panel space="group">…</Panel>

Accessibility

Panel renders a <section> and wires up the heading outline. Most of the story is handled for you. Two parts need your attention: giving the Panel a name, and making sure its heading levels match the page it sits in.

Labeling

A <section> only becomes a region landmark once it has an accessible name. Without one, it's just a grouping element. Give the Panel a name in one of two ways:

  • Preferred. Include a <Title> inside <Panel.Header>. The Panel is automatically labelled by the title.
  • Fallback. If the Panel has no visible title (a Collapsible-only Panel, for example), pass an aria-label on the Panel itself.

If both are set, aria-label takes precedence and the title is no longer used as the accessible name. Passing both is redundant, so pick one.

Heading levels

<Title> renders an <h2> by default. Inside <Panel.CollapsibleHeader> it renders one level deeper (<h3>) when the Panel has a title to nest under, with the structural heading provided by the collapsible header itself. In a Collapsible-only Panel (no <Title>), the collapsible heading renders at the base headingLevel instead, since there's no parent heading to defer to.

If the Panel lives under an existing <h2> on the page (inside a Tabs panel, for example), shift both down together with headingLevel:

<Panel headingLevel={3}>
  <Title>…</Title> {/* renders as <h3> */}
  <Panel.Collapsible>
    <Panel.CollapsibleHeader>
      <Title>…</Title> {/* renders inside an <h4> */}
    </Panel.CollapsibleHeader>
  </Panel.Collapsible>
</Panel>

Collapsible sections

<Panel.Collapsible> wraps each header in a heading whose button carries aria-expanded. The title and description render as spans inside the trigger button so the entire header surface is a single click target. The button's accessible name comes from the title via aria-labelledby, and the description is linked via aria-describedby so screen readers announce the extra context after the name. The caret is marked aria-hidden, so only the title and description are read. Keyboard users toggle a section with Enter or Space.

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 Panel stories

Panel

Prop

Type

Accessibility props (54)

Prop

Type

DOM event handlers (164)

Prop

Type

Panel.Header

Prop

Type

Panel.Content

Prop

Type

Panel.Footer

Prop

Type

Panel.Collapsible

Prop

Type

DOM event handlers (64)

Prop

Type

Panel.CollapsibleHeader

Prop

Type

Panel.CollapsibleContent

Prop

Type

Related

Card

The collection-item counterpart to Panel. Use Card for items that repeat in a list or grid.

Accordion

Use Accordion for a set of peer collapsible sections. Panel is for one topic, Accordion is for parallel items.

Admin- & master mark

Placement guidance for master- and admin-only content.

Error boundaries

What a panel shows when its content fails to load.

Elevation

How raised, sunken, and flat surfaces work together on a page.
Last update: a day ago

Page

The main content area of a screen, with its title, description, primary action, and the sections beneath them.

Scrollable

Lets the content scroll.

On this page

AnatomyAppearanceUsagePanel vs CardHeaderContentCollapsibleActionsDanger zoneSpacingAccessibilityLabelingHeading levelsCollapsible sectionsPropsPanelPanel.HeaderPanel.ContentPanel.FooterPanel.CollapsiblePanel.CollapsibleHeaderPanel.CollapsibleContentRelated