Modal
Modal dialog overlaid on the page. Blocks interaction with the background
content until the user confirms/cancels the action. Supports controlled
usage (isOpen/onDismiss) or uncontrolled via trigger.
When to use
| ✅ Use when… | 🚫 Avoid when… |
|---|---|
|
|
Basic usage
The content is passed via children.
<Modal trigger={<Button text="simple modal" />}>
<Text text="Lorem ipsum dolor sit amet." />
</Modal>Variants
The modal has four color variants: primary (default), warning, success,
danger. The color affects the header icon and the footer's primary button.
<Modal variant="primary" {...otherProps} />
<Modal variant="warning" {...otherProps} />
<Modal variant="success" {...otherProps} />
<Modal variant="danger" {...otherProps} />Controlled Modal
When the state needs to be managed outside (e.g. opening in response to a
network event), use isOpen/onDismiss.
import { Button } from '@apollion-dsi/core/elements/button';
import { Icon } from '@apollion-dsi/core/elements/icon';
import { Modal } from '@apollion-dsi/core/elements/modal';
import { useToggle } from '@apollion-dsi/core/hooks';
import { cog } from '@apollion-dsi/core/icons';
const Example = () => {
const { active: isOpen, disable, enable } = useToggle();
return (
<>
<Button onClick={enable} text="Show Modal" />
<Modal isOpen={isOpen} onDismiss={disable} icon={<Icon icon={cog} />} title="Modal Title" tagline="Lorem ipsum.">
<Text text="Content" />
</Modal>
</>
);
};Uncontrolled Modal
Pass a trigger — it can be an element (receives an automatic onClick) or
a render function with { isOpen, open }.
<Modal trigger={<Button text="Show Modal" />} icon={<Icon icon={cog} />} title="Modal Title" tagline="Lorem ipsum.">
<Text text="Content" />
</Modal>Sizes
small, base (default), medium, large — control the maximum width.
<Modal size="small" trigger={<Button text="small" />}> ... </Modal>
<Modal size="base" trigger={<Button text="base" />}> ... </Modal>
<Modal size="medium" trigger={<Button text="medium" />}> ... </Modal>
<Modal size="large" trigger={<Button text="large" />}> ... </Modal>Footer
To display the footer, provide primaryAction. The button layout is
controlled by footer: right (default), expanded, full. The callbacks
receive { close } to dismiss the modal after the action.
<Modal
footer="right"
secondaryText="Cancel"
primaryText="Confirm"
primaryAction={({ close }) => {
save();
close();
}}
{...otherProps}
>
...
</Modal>Accessibility
role="dialog" with aria-modal, labeled by the title. On open, focus moves to the first interactive element; Tab and Shift+Tab cycle inside the dialog; Escape closes; on close, focus returns to the opener.
Two caveats:
- Without
title, the dialog has no accessible name — passaria-label. - With
noCloseButton,Escapestill closes, but mouse-only users depend on the overlay. Ensure an explicit exit action in the footer.
The close button text comes from closeLabel (default 'Fechar').
Properties
Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Modal body content. |
closeLabel | string | Fechar | Accessible name for the close button. An icon-only button has no name of
its own: without this, a screen reader announces just "button". |
footer | "expanded" | "right" | "full" | right | Layout of the footer buttons.
- `right`: both on the right (default).
- `expanded`: secondary on the left, primary on the right.
- `full`: primary only, spanning the full width. |
icon | ReactNode | — | Icon displayed to the left of the title in the header. |
isOpen | boolean | — | Controls the open state when NOT using `trigger`. |
noCloseButton | boolean | false | Hides the header's close button. Reserved for flows where the modal can
only be exited through a confirmed action (e.g. accepting terms). |
onDismiss | (() => void) | — | Called when clicking the overlay or the close button. |
onModalOpen | (() => void) | — | Callback fired when the modal opens — useful for tracking. |
primaryAction | ((props: { close: () => void; }) => void) | — | Callback for the footer's primary button. Receiving `{ close }` lets you
trigger the action and close the modal in the same function. |
primaryButton | ReactNode | — | Replaces the default primary button. Receives injected `onClick` and `fullWidth`. |
primaryText | string | Confirmar | Primary button text (ignored when `primaryButton` is provided). |
role | AriaRole | — | Custom `aria-role` (e.g. `alertdialog`). |
secondaryAction | ((props: { close: () => void; }) => void) | — | Callback for the footer's secondary button. |
secondaryButton | ReactNode | ((p: { close: () => void; }) => ReactNode) | — | Replaces the default secondary button (accepts a render prop). |
secondaryText | string | Cancelar | Secondary button text (ignored when `secondaryButton` is provided). |
size | "small" | "base" | "medium" | "large" | 'base' | Semantic width of the modal. |
tagline | string | — | Supporting text displayed below the title. |
title | string | — | Title displayed in the header. |
variant | "primary" | "success" | "warning" | "danger" | primary | Theme color (used on the icon and the primary button). |
Besides the props above, every component accepts the layout props (spacing, color, flex/grid, sizing, border) — not repeated here.
See also
- Storybook story: Components / Modal
- Alternatives:
Notification,Popover,Dropdown
Theming — surface ladder
Modal sits on the surface ladder: the panel paints surface.overlay (top of the ladder) and the backdrop uses surface.scrim. The roles are
mode-aware by construction — paper is lighter than the canvas in both light and
dark — and every ink inside resolves against this ground through the ground
channel. Override a role per polarity with createTheme({ colors: { ladder: … } });
see Dark Mode Engine.