Modalcomponent
Modal({ children, className, ...props }: ModalProps): ReactElement| Param | Type | |
|---|---|---|
props | ModalProps | Props for <Modal> — edge, padding, indent, radius, and shadow variants, optional children content, and an optional className. required |
| Return | |
|---|---|
ReactElement |
Styled <aside> panel for content inside a <Dialog>, with dark text on a light surface.
- Centred by default. It fades in and out with its
<Dialog>. - Has a
--shadow-largedrop shadow by default. Setshadow="none",shadow="small"orshadow="normal"to change it. top,right,bottom, orleftpins it to that edge. It then takes its own layer in the<Dialog>view transition, and slides in from that edge and out to it.
The panel inside a <Dialog>. <Dialog> dims the page; Modal gives the content a shadowed surface with dark text on a light fill.
Things to know:
- A native
<dialog>does both jobs. Shelving splits them:<Dialog>is the overlay andModalis the panel. Content placed directly in a<Dialog>shows as white text on the dark overlay. Modalsets its text back to--tint-00, so it reads on its own--tint-100surface.DialogsStore.show()wraps its content in a<Dialog>only. It does not add aModal, so put theModalin the content yourself.Modalonly styles the box. Lay out its contents with the usual block components.- A centred
Modalfills the width of its<Dialog>, so set--dialog-widthto change it.--modal-widthsets the width of aleftorrightpanel, and the maximum width of atoporbottompanel. - The
paddingvariant (for examplepadding="large") sets the top and bottom padding, and theindentvariant sets the left and right padding, the same as on<Panel>. - The
radiusvariant sets the corner radius. On a pinned panel, corners that touch a screen edge stay square. - It has a
--shadow-largedrop shadow by default. Setshadow="none",shadow="small"orshadow="normal"to change it, the same as on<Card>. - Set
top,right,bottom, orleftto pin the panel to that edge of the screen. A top or bottom panel is full width up to--modal-width, and centred on a wider screen. A left or right panel is full height. Use these for mobile menus, bottom sheets, and side menus. - A centred panel fades in and out with its
<Dialog>. A pinned panel slides in from its edge and out to it, in its own view-transition layer. With reduced motion, a pinned panel fades in place.
Usage
Declarative
Put the Modal inside a <Dialog> that you mount from React state.
import { Dialog, Modal } from "shelving/ui";
function ConfirmDelete({ onConfirm, onClose }: { onConfirm: () => void; onClose: () => void }) {
return (
<Dialog onClose={onClose}>
<Modal>
<p>Delete this item?</p>
<button type="button" onClick={onConfirm}>Delete</button>
</Modal>
</Dialog>
);
}Imperative
Pass the Modal to DialogsStore.show(). The store adds the <Dialog> for you. See <DialogsContext> and <Dialogs> for the setup.
import { Modal, requireDialogs } from "shelving/ui";
function DeleteButton({ onConfirm }: { onConfirm: () => void }) {
const dialogs = requireDialogs();
const open = () =>
dialogs.show(
<Modal>
<p>Delete this item?</p>
<button type="button" onClick={onConfirm}>Delete</button>
</Modal>,
);
return <button type="button" onClick={open}>Delete</button>;
}Padding, indent, and radius
<Modal padding="large" indent="large" radius="large">
<p>More space on all sides, and rounder corners.</p>
</Modal>Shadow
<Modal shadow="small">
<p>Sits closer to the page.</p>
</Modal>Pinned to an edge
import { Menu, MenuItem, Modal, requireDialogs } from "shelving/ui";
function MenuButton() {
const dialogs = requireDialogs();
const open = () =>
dialogs.show(
<Modal left>
<Menu>
<MenuItem href="/">Home</MenuItem>
<MenuItem href="/settings">Settings</MenuItem>
</Menu>
</Modal>,
);
return <button type="button" onClick={open}>Menu</button>;
}A link click inside a <Dialog> closes it, so the menu slides out as the page changes. Use <Modal bottom> for a bottom sheet on mobile.
Styling
Modal paints a shadowed surface with no border. Set --modal-stroke (for example var(--stroke-normal)) to show the themed border, or --modal-border to replace it. A pinned panel then has no visible border on the side that touches the screen edge. That side has a transparent border as wide as the screen's safe area (notch, rounded corners, home bar), so the content stays clear of it and the background fills it. On a pinned panel, only the corners that do not touch a screen edge are round. Override these hooks at :root (or any ancestor scope) to retheme.
| Variable | Styles | Default |
|---|---|---|
--modal-width | Width of a left or right panel, and maximum width of a top or bottom panel (a centred panel takes --dialog-width) | var(--width-narrow) |
--modal-stroke | Border width | 0 (no border) |
--modal-border | Border shorthand | var(--modal-stroke) solid --tint-80 |
--modal-radius | Corner radius (the radius variant wins over it) | var(--radius-normal) (16px) |
--modal-background | Surface fill | var(--tint-100) |
--modal-padding | Inner top and bottom padding (one length; the padding variant overrides it) | var(--space-normal) (16px) |
--modal-indent | Inner left and right padding (one length; the indent variant overrides it) | var(--space-normal) (16px) |
--modal-color | Text colour | var(--tint-00) |
--modal-max-height | Maximum height of a top or bottom panel (it scrolls past this) | 100% |
--modal-transition-duration | Length of the slide for a pinned panel. Keep it the same as --fade-transition-duration, so the panel and the <Dialog> overlay finish together | var(--duration-fast) (150ms) |
--modal-shadow | Drop shadow (the shadow variant wins over it) | var(--shadow-large) |
Global tokens it reads — move these to retheme broadly: the tint ladder --tint-00 / --tint-80 / --tint-100, plus --width-narrow, --space-normal, --radius-normal, --shadow-large, and --duration-fast.