oks-ui

How to use

Modal

Modal is a focus-trapping, portal-rendered dialog for content that needs the user's full attention before they continue — confirmations, forms that block the underlying page, detail views opened from a list. It's fully controlled: you own the open/closed state, Modal just renders it correctly and safely.

import { Modal, Button } from "oks-ui";

Controlled open state

isOpen and onClose are both required — Modal never manages its own visibility. onClose fires from every dismissal path (backdrop click, Escape, the header's close button) so there's exactly one place to update your state, regardless of how the user closed it.

function DeleteConfirm() {
  const [isOpen, setIsOpen] = useState(false);
  return (
    <>
      <Button color="danger" onPress={() => setIsOpen(true)}>Delete item</Button>
      <Modal
        isOpen={isOpen}
        onClose={() => setIsOpen(false)}
        title="Delete item"
        actions={
          <>
            <Button variant="soft" onPress={() => setIsOpen(false)}>Cancel</Button>
            <Button color="danger" onPress={handleDelete}>Delete</Button>
          </>
        }
      >
        This can't be undone. Are you sure?
      </Modal>
    </>
  );
}

Header, body, footer

title renders the header (built on PageTitle internally, with a close button next to it unless dismissible={false}), children is the body, and actions is the footer — typically Cancel/Confirm-style buttons. All three are optional independently, so a Modal can be body-only, or header+body with no footer.

Safe stacking

Opening a second Modal (or a Drawer, since they share the same overlay stack) while one is already open works correctly out of the box — z-index and backdrop layering are computed automatically, and Escape only ever closes the topmost overlay, not every open one at once.

// Nested confirm-inside-a-modal -- stacks correctly without manual z-index math
<Modal isOpen={editOpen} onClose={() => setEditOpen(false)} title="Edit profile">
  <ProfileForm />
  <Modal
    isOpen={confirmDiscard}
    onClose={() => setConfirmDiscard(false)}
    title="Discard changes?"
    role="alertdialog"
  >
    Unsaved changes will be lost.
  </Modal>
</Modal>

Sizing and animation

size accepts the usual token scale plus "full" for an edge-to-edge modal, or a raw string/number for an exact width. animationType controls how it enters/exits (fade, zoom, or a directional slide) with animationDuration/easing to fine-tune the feel; blur and backgroundOpacity control the backdrop underneath it.

<Modal isOpen={isOpen} onClose={onClose} size="lg" animationType="slideUp" title="Details">
  {content}
</Modal>

Confirmation dialogs: role="alertdialog"

For a modal whose sole purpose is a decision the user must make (not just informational content), set role="alertdialog" instead of the default "dialog" — screen readers announce it more assertively, matching the native alertdialog semantics.

Accessibility

  • Traps focus inside the modal while open, and restores focus to whatever triggered it on close — a keyboard user never gets dropped behind the modal into the rest of the page.
  • closeOnEscape (on by default) closes the modal on Escape; when overlays are stacked, Escape only ever closes the topmost one.
  • initialFocusRef lets you send initial focus somewhere other than the default (the modal itself, or its first focusable element) — useful when a specific field should be focused immediately, like a text input in a form modal.
  • The header's close button has an accessible name via closeLabel (defaults to "Close modal") — override it if the default doesn't read naturally for a specific modal's content.

Props reference

All available props for Modal. See the full component page for an interactive playground.

Props

title

ReactNode

Default:

Header title.

children

ReactNode

Default:

Body content.

actions

ReactNode

Default:

Footer action buttons.

Best practices

  • Use role="alertdialog" for confirmations and destructive actions, and leave the default role="dialog" for general content modals.
  • Set closeOnOutsideClick={false} for a modal representing a multi-step or destructive flow the user shouldn't be able to dismiss by an accidental click outside it.
  • Keep actions to one or two buttons — a modal footer with many actions usually means the content itself should be a full page instead.
  • For content that slides in from an edge rather than appearing centered (a filters panel, a details sidebar), reach for Drawer instead — it shares Modal's focus-trapping and stacking behavior.
  • Don't nest more than two modals deep in practice, even though stacking is supported — each additional layer makes it harder for the user to track their way back out.
View full API reference for Modal