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
| Prop | Type | Default | Description |
|---|---|---|---|
placement | 'center' | 'sidepanel' | 'bottomSheet' | 'center' | Panel layout. sidepanel collapses into bottomSheet below the md breakpoint |
type | 'info' | 'success' | 'error' | 'warning' | 'destructive' | undefined | — | Semantic type; controls the default icon and colour scheme. Leave unset (or pass undefined) to render no status icon |
open | boolean | — | Controls visibility |
title | string | — | Heading text inside the header |
subtitle | string | — | Secondary text below the title |
description | string | — | Body text below the header |
actions | FooterAction[] | — | Array of buttons rendered in the footer |
onClose | () => void | — | Called when the close button, backdrop, or Esc is triggered |
hint | string | — | Optional hint message rendered in the footer area |
hasHintCheckbox | boolean | — | When true, renders hint as a checkbox instead of plain text |
itemCount | number | — | Total number of items being browsed. The nav control renders only when greater than 1 |
currentIndex | number | 0 | 0-based index of the item currently displayed |
onNavigate | (index: number) => void | — | Called with the target index when the previous/next control is used |
onPrevious / onNext | () => void | — | Called alongside onNavigate for the respective direction |
disableArrowKeys | boolean | — | Disables the built-in ArrowUp/ArrowDown keydown handling |
previousKeys / nextKeys | string[] | — | Additional hotkey combo (via useHotkey) that also triggers previous/next, e.g. ['k']/['j']. Arrow keys can't be registered here — see disableArrowKeys |
disablePortal | boolean | — | Render relative to parent instead of document.body |
keepMounted | boolean | — | Keep the panel mounted (invisible) in the DOM while closed, instead of unmounting it |
autoFocus | boolean | — | Move focus into the dialog when it opens |
children | ReactNode | — | Custom 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)
| Slot | Type | Description |
|---|---|---|
head | ReactNode | Full override of the header row |
footer | ReactNode | Full override of the footer row |
bodyComponent | ReactNode | Full override of the body content (description/children are skipped) |
startIcon / endIcon | ReactNode | Header icon overrides |
arrowIconUp / arrowIconDown | ReactNode | Icon overrides for the built-in item nav control |
itemNav | (ctx) => ReactNode | Full override of the item nav control; receives { currentIndex, itemCount, isFirst, isLast, onPrevious, onNext } |
| Slot Prop | Props | Description |
|---|---|---|
head | BaseHTMLProps<HTMLDivElement> | Header row container |
closeButton | Omit<IIconButton, 'onClick'> | Close icon button |
icon | BaseHTMLProps<HTMLDivElement> | Start icon wrapper |
title | BaseHTMLProps<HTMLHeadingElement> | Title <h2> element |
subtitle | BaseHTMLProps<HTMLParagraphElement> | Subtitle text element |
body | BaseHTMLProps<HTMLDivElement> | Body container — a wrapper override only; use slots.bodyComponent to replace the content itself |
description | BaseHTMLProps<HTMLParagraphElement> | Description text element |
hintText | BaseHTMLProps<HTMLParagraphElement> | Hint label text |
hintCheckbox | BaseHTMLProps<HTMLDivElement> | Checkbox indicator element |
footer | BaseHTMLProps<HTMLDivElement> | Footer container |
itemNavRoot | BaseHTMLProps<HTMLDivElement> | Built-in item nav container |
itemNavPreviousButton / itemNavNextButton | Omit<IIconButton, 'onClick'> | Built-in item nav buttons |
itemNavCounter | BaseHTMLProps<HTMLParagraphElement> | "N of X items" text element |
Accessibility
- Renders a
<div role="dialog" aria-modal="true">, witharia-labelledby/aria-describedbypointing 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
titleso screen readers can announce the dialog's purpose when focus enters.
Alert
A modal alert dialog for confirmations, notifications, and destructive actions — with semantic types, footer actions, and an optional hint checkbox.
Snackbar
A single toast/snackbar surface — info, warning, success, error, light, and dark variants, an optional action button, close button, and progress bar.