AI
NUI logov3.1.0

Modal

A portal-based, fully accessible dialog primitive engineered for interruptive workflows, confirmations, and multi-step forms. Implements WAI-ARIA 1.2 modal dialog standards, focus trapping, background inerting, body scroll locking, and physics-based entrance/exit transitions.

tsx
import { Modal } from '@nofinite/nui';

Interactive Preview


Anatomy & Modes

NUI Modal can be used as a self-contained single element via declarative props (matching Storybook's default pattern) or composed via structured compound components for custom layout arrangements:

tsx
<Modal
  open={open}
  onClose={handleClose}
  title="Modal Title"
  description="Descriptive helper subtitle."
  footer={
    <>
      <Button variant="outline" onClick={handleClose}>Cancel</Button>
      <Button variant="primary" onClick={handleConfirm}>Confirm</Button>
    </>
  }
>
  <p>Modal body content goes here.</p>
</Modal>

Compound Components Mode

tsx
<Modal open={open} onClose={handleClose}>
  <Modal.Header>
    <Modal.Title>Modal Title</Modal.Title>
    <Modal.Description>Descriptive helper subtitle.</Modal.Description>
  </Modal.Header>
  <Modal.Body>
    {/* Scrollable content area */}
  </Modal.Body>
  <Modal.Footer>
    {/* Action buttons */}
  </Modal.Footer>
</Modal>
Component / PropDescription
ModalThe root portal controller and backdrop overlay container.
titleAccessible title prop rendered into h2.modal-title.
descriptionAccessible description subtitle rendered into p.modal-description.
footerCustom footer actions container rendered with border separation and flex-end alignment.
Modal.HeaderContainer for the dialog title and descriptive subtitle.
Modal.TitleAccessible heading (<h2>) linked via aria-labelledby.
Modal.DescriptionAccessible subtitle (<p>) linked via aria-describedby.
Modal.BodyMain scrollable content region with responsive padding.
Modal.FooterBottom action bar with border separation and flex-end alignment.

Usage Examples

1. Standard Declarative Modal

Self-contained modal using title, description, and footer props:

tsx
import React, { useState } from 'react';
import { Button, Modal } from '@nofinite/nui';

export default function DeploymentSettings() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <Button variant="primary" onClick={() => setOpen(true)}>
        Deployment Settings
      </Button>

      <Modal
        open={open}
        onClose={() => setOpen(false)}
        title="Production Deployment"
        description="Configure edge nodes and SSL certificate options."
        footer={
          <>
            <Button variant="outline" onClick={() => setOpen(false)}>Cancel</Button>
            <Button variant="primary" onClick={() => setOpen(false)}>Deploy Now</Button>
          </>
        }
      >
        <p className="text-sm text-muted leading-relaxed">
          Your application will be distributed across 35 edge regions with automated fallback routing and instant cache invalidation.
        </p>
      </Modal>
    </>
  );
}

2. Form Dialog with Initial Focus

Automatically focus an input field when the dialog opens using initialFocusRef:

tsx
import React, { useState, useRef } from 'react';
import { Button, Modal, Input } from '@nofinite/nui';

export default function RenameProjectModal() {
  const [open, setOpen] = useState(false);
  const inputRef = useRef<HTMLInputElement>(null);

  return (
    <>
      <Button variant="outline" onClick={() => setOpen(true)}>Rename Project</Button>

      <Modal 
        open={open} 
        onClose={() => setOpen(false)}
        initialFocusRef={inputRef}
      >
        <Modal.Header>
          <Modal.Title>Rename Project</Modal.Title>
          <Modal.Description>Choose a unique identifier for your workspace.</Modal.Description>
        </Modal.Header>
        <Modal.Body>
          <div className="space-y-4">
            <label className="block text-sm font-medium text-default">Project Name</label>
            <Input ref={inputRef} defaultValue="nebula-ui" placeholder="e.g. nebula-ui" />
          </div>
        </Modal.Body>
        <Modal.Footer>
          <Button variant="outline" onClick={() => setOpen(false)}>Cancel</Button>
          <Button variant="primary" onClick={() => setOpen(false)}>Save Name</Button>
        </Modal.Footer>
      </Modal>
    </>
  );
}

3. Non-Dismissible Transactional Modal

Prevent accidental dismissal during critical or destructive workflows:

tsx
<Modal 
  open={isMigrating} 
  onClose={() => {}} 
  disableClickOutside 
  disableEsc 
  hideCloseButton
>
  <Modal.Header>
    <Modal.Title>Migrating Database</Modal.Title>
  </Modal.Header>
  <Modal.Body>
    <p className="text-sm text-muted">Please do not close this window while the migration executes.</p>
  </Modal.Body>
</Modal>

Keyboard Interaction

KeyBehavior
EscapeCloses the modal and restores focus to the previously focused trigger element (unless disableEsc is active).
TabTraps and cycles focus forward through all interactive elements within the modal dialog.
Shift + TabCycles focus backwards within the modal focus trap.

Accessibility (WAI-ARIA)

  • Role & Semantics: Root dialog renders role="dialog" and aria-modal="true".
  • Labeling: Automatically links aria-labelledby to Modal.Title and aria-describedby to Modal.Description.
  • Focus Management: Focus is automatically trapped inside the dialog upon mounting and returned to the trigger element on close.
  • Background Inerting: All sibling nodes outside the portal are marked inert to prevent screen reader bleed.
  • Scroll Locking: Automatically locks body scroll with padding compensation to avoid layout shift.

Design Tokens & NUICSS Superclasses

NUI Modal strictly adheres to semantic NUICSS tokens and superclasses:

Superclass / TokenTypeUsage
modal-boxSuperclassDialog container surface with elevation, radius, and max-width.
modal-headerSuperclassTop header area with title spacing.
modal-titleSuperclassSemantically styled dialog title.
modal-descriptionSuperclassMuted subtitle text.
modal-bodySuperclassScrollable body area with responsive gutters.
modal-footerSuperclassBottom border-separated action bar.
modal-closeSuperclassAbsolute close button in the upper-right corner.
bg-overlayTokenSemi-transparent dimmed backdrop (--bg-overlay).
bg-surfaceTokenDialog panel background surface (--bg-surface).
border-defaultTokenContainer border token (--border-default).

Best Practices

Do

  • Always provide clear Modal.Title and Modal.Description for screen reader context.
  • Use initialFocusRef for form dialogs to guide keyboard users directly to the first input.
  • Provide explicit action buttons in Modal.Footer to clearly communicate outcomes.

Don't

  • Do not nest modals inside other modals; use sequential drawers or full-page workflows instead.
  • Avoid using modals for non-interruptive information that could live directly on the page canvas.
  • Do not disable outside click or Escape dismissal without an explicit functional reason (such as unsaved changes).

API Reference

PropTypeDefaultDescription
open*boolean——
onClose*() => void——
titleReactNode——
descriptionReactNode——
footerReactNode—Custom footer actions (rendered with modal-footer styling and border separation)
labelledByIdstring——
describedByIdstring——
disableClickOutsideboolean——
disableEscboolean——
initialFocusRefRefObject<HTMLElement | null>——
overlayClassNamestring——
hideCloseButtonboolean—Hides the 'X' close button in the top right corner.