Modal
Render a full-screen dialog with a backdrop, Escape handling, and router-aware close behaviour.
Modal is a client component that renders a dialog panel over a backdrop, portalled to document.body by default. Use it for focused tasks such as confirmations, short forms, or route-driven detail views. It uses useRouter and usePathname from next/navigation, so it must be rendered inside a Next.js App Router tree. The modal is open while mounted; the parent controls mounting.
Import#
import { Modal, useModalClose, pushThemeColor, popThemeColor } from "@reactberry/system/blocks";
Usage#
import { useState } from "react";
import { Modal, useModalClose } from "@reactberry/system/blocks";
import { Box, Button, Text } from "@reactberry/system/elements";
function CloseButton() {
const close = useModalClose();
return (
<Button variant="ghost" onClick={() => close?.()}>
<Text>Close</Text>
</Button>
);
}
export default function InviteDialog() {
const [open, setOpen] = useState(false);
return (
<>
<Button variant="primary" onClick={() => setOpen(true)}>
<Text>Invite</Text>
</Button>
{open && (
<Modal onClose={() => setOpen(false)} animated maxWidth="30rem" height="auto">
<Box p="medium" display="flex" flexDirection="column" gap="small">
<Text fontWeight="600">Invite a teammate</Text>
<CloseButton />
</Box>
</Modal>
)}
</>
);
}
Examples#
Animated, closed from content#
Set animated to scale and fade the dialog with a spring. Call useModalClose() inside the content so the exit animation plays before onClose runs.
Required action#
Set closeOnOverlayClick={false} so a backdrop click doesn't dismiss the dialog. Escape still closes it.
Route-driven#
Without onClose, closing navigates back in history or to fallbackHref. Render the modal from a route, such as an intercepted detail page:
<Modal fallbackHref="/tasks" closeOnOverlayClick={false}>
<Text>Task details</Text>
</Modal>
API#
Modal#
| Prop | Type | Default | Description |
|---|---|---|---|
children* | ReactNode | — | Dialog content, wrapped in a full-height flex column. |
onClose | (() => void) | — | Close handler. When provided it always takes precedence over router navigation. With |
portal | boolean | true | Render into |
fallbackHref | string | — | Explicit fallback route used when there is no browser history to return to. Example: "/tasks" |
forceFallback | boolean | false | (Optional) Disable using browser history even if available and always use fallback / derived path. |
closeOnOverlayClick | boolean | true | Whether clicking the backdrop overlay should close the modal. Defaults to true for current behavior. Set to false to prevent overlay clicks from closing. |
backdrop | boolean | true | Whether to show the tinted backdrop overlay behind the modal. Defaults to true. Set to false for a transparent backdrop. |
backdropProps | Record<string, any> | — | Additional props forwarded to the underlying |
animated | boolean | false | Whether to animate the modal open/close with a scale + fade animation matching the sign-in card style. Defaults to false. |
Any other props are spread onto the dialog panel Box (defaults include skin="surface", shape="rounded", $shadow="medium", width="100%", height="100%", maxWidth="inherit", overflow="hidden"). Pass height="auto" and a maxWidth for a content-sized dialog.
useModalClose#
| Prop | Type | Default | Description |
|---|---|---|---|
close | (() => void) | null | — | The enclosing modal's close function, or |
pushThemeColor / popThemeColor#
Neither function takes arguments or returns a value.
pushThemeColor()increments a shared overlay counter. On the first push it setsbodyoverflow tohiddenand, on touch devices, sets thetheme-colormeta tag andhtmlbackground to#000000.popThemeColor()decrements the counter and restores the original values when it reaches zero.
Modal calls pushThemeColor on mount and popThemeColor on unmount. Call them in pairs if building a custom overlay.
Keyboard#
| Key | Action |
|---|---|
Escape | Closes the modal. The listener is on `window`, so it fires wherever focus is. |
Accessibility#
- The panel has
role="dialog"andaria-modal="true". It has no accessible name; addaria-labeloraria-labelledby, which are spread onto the panel. - Focus is not moved into or trapped within the dialog, and is not restored on close.
- The backdrop has
aria-hidden="true".
Notes#
- Without
onClose, closing callsrouter.back()whenwindow.history.lengthis greater than 1, otherwise pushesfallbackHref. If no fallback is given, paths matching/tasks/[id]and/activity/[id]resolve to/tasksand/activity; anything else resolves to/. - Clicks, pointer, and keyboard events inside the modal stop propagating to React ancestors.
- Body scroll is locked while the modal is mounted.
- Nothing is rendered until after the first client mount. In PWA standalone mode, top padding includes the safe-area inset.