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#

typescript
import { Modal, useModalClose, pushThemeColor, popThemeColor } from "@reactberry/system/blocks";

Usage#

typescript
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:

typescript
<Modal fallbackHref="/tasks" closeOnOverlayClick={false}>
  <Text>Task details</Text>
</Modal>

API#

PropTypeDefaultDescription
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 animated, it runs after the exit animation.

portal
boolean
true

Render into document.body via createPortal.

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 Backdrop element (e.g. bg, zIndex, custom transition). Spread after the Modal-managed defaults so callers can override them.

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#

PropTypeDefaultDescription
close
(() => void) | null
—

The enclosing modal's close function, or null outside a Modal. When animated is set, calling it plays the exit animation before onClose runs.

pushThemeColor / popThemeColor#

Neither function takes arguments or returns a value.

  • pushThemeColor() increments a shared overlay counter. On the first push it sets body overflow to hidden and, on touch devices, sets the theme-color meta tag and html background 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#

KeyAction
Escape

Closes the modal. The listener is on `window`, so it fires wherever focus is.

Accessibility#

  • The panel has role="dialog" and aria-modal="true". It has no accessible name; add aria-label or aria-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 calls router.back() when window.history.length is greater than 1, otherwise pushes fallbackHref. If no fallback is given, paths matching /tasks/[id] and /activity/[id] resolve to /tasks and /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.