UnflowUI
Components

Modal

A general-purpose modal dialog with center, sidepanel, and bottom-sheet placements, optional item navigation, and a hint checkbox.

Overview

Modal renders a dialog on top of a Backdrop. Unlike Alert (built for confirmations), Modal is a general-purpose container with a placement prop that controls where and how the panel appears — center, sidepanel, or bottomSheet. sidepanel automatically collapses into the same bottom-sheet treatment as bottomSheet on small viewports; this is handled entirely with responsive CSS, no JavaScript viewport detection.

Modal also supports an optional item-navigation control (previous/next + "N of X items") for browsing between records without closing the dialog, with built-in ArrowUp/ArrowDown keyboard support. That control is its own standalone, reusable component — see ItemNav.

Import

import { Modal } from '@unflow.io/ui/components/Modal';

Props

PropTypeDefaultDescription
placement'center' | 'sidepanel' | 'bottomSheet''center'Panel layout. sidepanel collapses into bottomSheet below the md breakpoint
type'info' | 'success' | 'error' | 'warning' | 'destructive' | undefinedSemantic type; controls the default icon and colour scheme. Leave unset (or pass undefined) to render no status icon
openbooleanControls visibility
titlestringHeading text inside the header
subtitlestringSecondary text below the title
descriptionstringBody text below the header
actionsFooterAction[]Array of buttons rendered in the footer
onClose() => voidCalled when the close button, backdrop, or Esc is triggered
hintstringOptional hint message rendered in the footer area
hasHintCheckboxbooleanWhen true, renders hint as a checkbox instead of plain text
itemCountnumberTotal number of items being browsed. The nav control renders only when greater than 1
currentIndexnumber00-based index of the item currently displayed
onNavigate(index: number) => voidCalled with the target index when the previous/next control is used
onPrevious / onNext() => voidCalled alongside onNavigate for the respective direction
disableArrowKeysbooleanDisables the built-in ArrowUp/ArrowDown keydown handling
previousKeys / nextKeysstring[]Additional hotkey combo (via useHotkey) that also triggers previous/next, e.g. ['k']/['j']. Arrow keys can't be registered here — see disableArrowKeys
disablePortalbooleanRender relative to parent instead of document.body
keepMountedbooleanKeep the panel mounted (invisible) in the DOM while closed, instead of unmounting it
autoFocusbooleanMove focus into the dialog when it opens
childrenReactNodeCustom content rendered inside the body, alongside description

FooterAction extends the Button props with an adjusted onClick, same shape as Alert's footerActions:

type FooterAction = Omit<IButton, 'onClick' | 'disabled'> & {
  onClick?: (e: MouseEvent, hintChecked: boolean) => void;
  disabled?: (hintChecked: boolean) => boolean;
};

Placement

Three placements are available. sidepanel is docked to the right edge on desktop and becomes a full-width bottom sheet below md.

<Modal placement="center"     title="..." description="..." open onClose={close} actions={actions} />
<Modal placement="sidepanel"  title="..." description="..." open onClose={close} actions={actions} />
<Modal placement="bottomSheet" title="..." description="..." open onClose={close} actions={actions} />

Item navigation

Pass itemCount (and currentIndex/onNavigate) to render a previous/next control in the header, backed by the standalone ItemNav component. While the modal is open, ArrowUp moves to the previous item and ArrowDown moves to the next — unless focus is on an editable element (input, textarea, select, or a contenteditable node), or disableArrowKeys is set. Pass previousKeys/nextKeys to additionally bind a custom combo (e.g. ['k']/['j']) alongside the arrows.

<Modal
  placement="sidepanel"
  open={open}
  title="INVO-2026-003"
  itemCount={30}
  currentIndex={currentIndex}
  onNavigate={setCurrentIndex}
  onClose={() => setOpen(false)}
/>

Checkbox hint

Set hasHintCheckbox to render hint as a checkbox the user must actively check. The checked state is forwarded as the second argument to each action.onClick, so you can guard a destructive action behind it.

<Modal
  type="destructive"
  open={open}
  title="Delete records"
  description="This will permanently delete the selected records. This action cannot be undone."
  hint="I understand that this action is irreversible."
  hasHintCheckbox
  onClose={() => setOpen(false)}
  actions={[
    { label: 'Cancel', variant: 'secondary', onClick: () => setOpen(false) },
    {
      label: 'Delete',
      variant: 'destructive',
      onClick: (e, hintChecked) => {
        if (!hintChecked) return;
        setOpen(false);
      },
      disabled: (hintChecked) => !hintChecked,
    },
  ]}
/>

Custom content

Use children to render arbitrary content inside the body alongside or instead of description, or slots.bodyComponent to replace the body content entirely (bypassing description/children).

<Modal
  open={open}
  title="Review before confirming"
  onClose={() => setOpen(false)}
  actions={[
    { label: 'Cancel', variant: 'secondary', onClick: () => setOpen(false) },
    { label: 'Confirm', onClick: () => setOpen(false) },
  ]}
>
  <ul className="text-sm text-grey-600 list-disc pl-4 space-y-1">
    <li>5 records will be updated</li>
    <li>Change will apply to all environments</li>
  </ul>
</Modal>

Customization (slots & slotProps)

SlotTypeDescription
headReactNodeFull override of the header row
footerReactNodeFull override of the footer row
bodyComponentReactNodeFull override of the body content (description/children are skipped)
startIcon / endIconReactNodeHeader icon overrides
arrowIconUp / arrowIconDownReactNodeIcon overrides for the built-in item nav control
itemNav(ctx) => ReactNodeFull override of the item nav control; receives { currentIndex, itemCount, isFirst, isLast, onPrevious, onNext }
Slot PropPropsDescription
headBaseHTMLProps<HTMLDivElement>Header row container
closeButtonOmit<IIconButton, 'onClick'>Close icon button
iconBaseHTMLProps<HTMLDivElement>Start icon wrapper
titleBaseHTMLProps<HTMLHeadingElement>Title <h2> element
subtitleBaseHTMLProps<HTMLParagraphElement>Subtitle text element
bodyBaseHTMLProps<HTMLDivElement>Body container — a wrapper override only; use slots.bodyComponent to replace the content itself
descriptionBaseHTMLProps<HTMLParagraphElement>Description text element
hintTextBaseHTMLProps<HTMLParagraphElement>Hint label text
hintCheckboxBaseHTMLProps<HTMLDivElement>Checkbox indicator element
footerBaseHTMLProps<HTMLDivElement>Footer container
itemNavRootBaseHTMLProps<HTMLDivElement>Built-in item nav container
itemNavPreviousButton / itemNavNextButtonOmit<IIconButton, 'onClick'>Built-in item nav buttons
itemNavCounterBaseHTMLProps<HTMLParagraphElement>"N of X items" text element

Accessibility

  • Renders a <div role="dialog" aria-modal="true">, with aria-labelledby/aria-describedby pointing at the rendered title/subtitle/description.
  • Focus is trapped inside the dialog while open (via the underlying Backdrop and ClickAwayEvent), and restored to the previously focused element on close.
  • Clicking outside the dialog or pressing Esc triggers onClose.
  • The close button always has an accessible label and is reachable by keyboard.
  • The item nav "N of X items" counter is an aria-live="polite" region, so screen reader users get position feedback without extra wiring.
  • Provide a meaningful title so screen readers can announce the dialog's purpose when focus enters.