Skip to main content

Modal

A portal-based dialog primitive implementing focus isolation, background inerting, scroll locking, and controlled mounting with animated lifecycle transitions. Designed for transactional workflows, confirmations, forms, and interruptive UI.


Usage

Basic

import { Modal } from '@nofinite/nui';
<Modal open={open} onClose={() => setOpen(false)}>
Content
</Modal>

With title + description

<Modal
open={open}
onClose={close}
title="Delete account"
description="This action cannot be undone."
>
<Actions />
</Modal>

Form dialog with initial focus

<Modal
open={open}
onClose={close}
title="Edit profile"
initialFocusRef={inputRef}
>
<Form />
</Modal>

Disable outside click

<Modal open={open} onClose={close} disableClickOutside>
Blocking flow
</Modal>

Props

PropTypeDefaultDescription
openboolean Controlled visibility
onClose() => voidClose handler
titleReactNode Dialog heading
descriptionReactNode Supporting text
labelledByIdstring autoCustom title ID
describedByIdstring autoCustom description ID
disableClickOutsideboolean falseDisable overlay dismissal
disableEscboolean falseDisable ESC close
initialFocusRefRefObject Initial focus target
overlayClassNamestring Overlay override
classNamestring Dialog override
childrenReactNode Dialog content

Extends React.HTMLAttributes<HTMLDivElement>.


Variants

Modal exposes lifecycle state variants via data attributes.

<Modal open />
<Modal disableEsc />
<Modal disableClickOutside />

Available variants:

  • closed
  • mounted
  • visible
  • overlay-open
  • dialog-open

Guidelines:

  • Use for interruptive workflows only
  • Avoid nested modals

Sizes

No intrinsic size API.
Default constraints:

  • max-width: 500px
  • max-height: 90vh
  • Mobile safe margin
  • Scrollable content region

Sizing should be overridden via className.


Shapes / Modes

Interaction modes

  • ESC close
  • Outside click close
  • Explicit close button
  • Controlled close only (disabled dismissal)

Accessibility modes

  • Labelled dialog
  • Described dialog
  • Focus-trapped modal
  • Background inert environment

Lifecycle modes

  • Mounted
  • Visible
  • Animating in
  • Animating out
  • Unmounted

Design tokens / theming

Primary tokens:

  • --nui-bg-surface
  • --nui-border-default
  • --nui-radius-lg
  • --nui-fg-default
  • --nui-fg-subtle
  • --nui-space-*
  • --nui-color-primary

Elevation defined via layered shadow tokens.


Accessibility

Implemented:

  • role="dialog"
  • aria-modal="true"
  • aria-labelledby / aria-describedby
  • Focus trap
  • Initial focus management
  • Focus restoration
  • Background inerting
  • Scroll locking
  • ESC dismissal
  • Overlay click dismissal
  • Close button with accessible label

Strong compliance with WCAG modal dialog guidance.

Limitations:

  • No alertdialog role variant
  • No nested focus trap stacking
  • No announcement for dynamic content updates
  • No aria-live integration

Animation

Lifecycle animation model:

  • Overlay fade
  • Dialog scale + fade
  • Mount delay for entrance
  • Exit delay for unmount
  • GPU-accelerated transform animation
  • Respects reduced motion preference

Architectural notes

Key behaviors:

  • Portal rendering avoids stacking context conflicts
  • Two-phase state (mounted + visible) enables exit animation
  • Focus trap lifecycle bound to visibility
  • Inert background ensures screen reader isolation
  • Click catcher prevents overlay bubbling conflicts
  • Scroll lock prevents layout shift
  • Ref preservation restores user context
  • ID auto-generation ensures accessibility linkage

Tradeoffs:

  • Fixed animation duration coupling with unmount timing
  • No collision-aware viewport repositioning
  • No scrollable overlay strategy for extremely tall dialogs
  • Potential inert polyfill dependency in older browsers

Best practices

Do

  • Use for blocking user decision flows
  • Provide explicit close affordance
  • Maintain concise content hierarchy
  • Use initialFocusRef for form dialogs
  • Override size for content-heavy dialogs
  • Provide keyboard-accessible primary action

Don’t

  • Nest modals
  • Use for persistent UI
  • Overload with navigation content
  • Disable dismissal without strong UX reason
  • Place interactive content outside focus trap
  • Rely solely on overlay click for close