Build / Organisms
Sheet
A modal surface that slides up from the bottom.
This is the home of the Exchange action — "Show my card" and "Scan a card" presented together, because they are two halves of one moment.
Built on the native <dialog> element, so focus trapping, Escape to
dismiss, inertness of the page behind, and returning focus to whatever
opened it all come from the platform rather than hand-rolled listeners.
import { Sheet } from '@meetcard/deck'Example
<Sheet open={open} onClose={close} title="Exchange"> <Button fullWidth iconStart={<QrIcon />}>Show my card</Button> <Button fullWidth variant="secondary">Scan a card</Button> </Sheet>Stories
The Exchange sheet: showing your card and scanning someone else's, together under one control because they are two halves of the same moment.
Source
const Exchange = args => {
const [open, setOpen] = useState(false)
return (
<div style={{ padding: 24 }}>
<Button onClick={() => setOpen(true)}>Exchange</Button>
<Sheet
title="Exchange"
open={open}
onClose={() => {
setOpen(false)
args.onClose()
}}
description="Share your card or scan someone else's.">
<Button fullWidth iconStart={<QrIcon />}>Show my card
</Button>
<Button fullWidth variant="secondary">Scan a card
</Button>
</Sheet>
</div>
);
};Escape closes the sheet — behaviour inherited from native <dialog>.
Source
const ClosesOnEscape = () => {
const [open, setOpen] = useState(true)
return (
<div style={{ padding: 24 }}>
<Sheet title="Exchange" open={open} onClose={() => setOpen(false)}>
<Button fullWidth>Show my card</Button>
</Sheet>
{!open ? <Text>Sheet closed</Text> : null}
</div>
);
};Centered placement, for confirmations rather than thumb-reachable actions.
Source
const Centered = () => {
const [open, setOpen] = useState(false)
return (
<div style={{ padding: 24 }}>
<Button onClick={() => setOpen(true)}>Delete card</Button>
<Sheet
title="Delete this card?"
description="This cannot be undone."
placement="center"
open={open}
onClose={() => setOpen(false)}
footer={
<>
<Button variant="destructive" size="sm">
Delete
</Button>
<Button variant="ghost" size="sm" onClick={() => setOpen(false)}>
Cancel
</Button>
</>
} />
</div>
);
};Holding the card up to be scanned — the highest-frequency moment.
Source
const ShowMyCard = () => {
const [open, setOpen] = useState(false)
return (
<div style={{ padding: 24 }}>
<Button onClick={() => setOpen(true)}>Show my card</Button>
<Sheet
title="Scan to connect"
hideTitle
open={open}
onClose={() => setOpen(false)}>
<QRCode value="https://metcard.io/ben" size="lg">
<SampleMatrix />
</QRCode>
</Sheet>
</div>
);
};Props
| Prop | Type | Default | Description |
|---|---|---|---|
onCloserequired | () => void | — | — |
openrequired | boolean | — | — |
titlerequired | ReactNode | — | Required: the sheet's accessible name. |
children | ReactNode | — | — |
closeLabel | string | 'Close' | — |
description | ReactNode | — | — |
dismissOnBackdrop | boolean | true | Allow dismissal by backdrop click. Defaults to true. |
footer | ReactNode | — | Pinned action row at the bottom. |
hideTitle | boolean | false | Visually hide the title, keeping it for assistive tech. |
placement | 'sheet' | 'center' | 'side' | 'sheet' | sheet slides up from the bottom; center is a centered dialog; side
is a full-height drawer on the inline-end edge.
side is for account and navigation surfaces, which are lists you run
your eye down rather than a decision you make and dismiss. It keeps its
edge at every width — a drawer that became a bottom sheet on a phone
would put a long list of destinations under the thumb and off the screen. |
Also accepts everything from Omit, so id, data-* and event handlers pass straight through.