Modal
Dialog overlay for focused tasks, confirmations, and forms.
Overview
The Modal component provides an overlay dialog with header, body, and footer sections. It supports custom sizing, fullscreen mode, actions (submit/button), and compound composition via primitives.
Basic Usage
import { Modal } from '@fsai/shared-ui';
<Modal
isOpen={isOpen}
handleClose={() => setIsOpen(false)}
title="Edit Profile"
action={{
type: 'button',
label: 'Save',
onClick: handleSave,
}}
>
<p>Modal body content goes here.</p>
</Modal>With Form Submit
Tie the action to a form using type: 'submit':
<Modal
isOpen={isOpen}
handleClose={() => setIsOpen(false)}
title="Create Item"
action={{
type: 'submit',
formId: 'create-item-form',
label: 'Create',
}}
>
<form id="create-item-form" onSubmit={handleSubmit}>
<Input label="Name" name="name" />
</form>
</Modal>Loading Action
<Modal
isOpen={isOpen}
handleClose={() => setIsOpen(false)}
title="Saving..."
action={{
type: 'button',
label: 'Save',
onClick: handleSave,
isLoading: isSaving,
}}
>
{children}
</Modal>Compound Components (Primitives)
For advanced layouts, use the modal primitives directly:
import { Modal } from '@fsai/shared-ui';
<Modal.Root isOpen={isOpen} onClose={() => setIsOpen(false)}>
<Modal.Backdrop />
<Modal.Panel>
<Modal.Header>
<Modal.Title>Custom Layout</Modal.Title>
<Modal.CloseButton />
</Modal.Header>
<Modal.Body>Content</Modal.Body>
<Modal.Footer>
<Button role="button" variant="secondary" onClick={() => setIsOpen(false)}>Cancel</Button>
<Button role="button" variant="primary" onClick={handleSave}>Save</Button>
</Modal.Footer>
</Modal.Panel>
</Modal.Root>Overlay Hierarchy Rule
Modal is the highest interruption surface in the platform.
The allowed overlay order is:
- page
PanelModal
That means:
- a
Modalmay open on top of aPanel - a
Panelmust never open on top of aModal - a
Modalmust never open on top of anotherModal
This rule applies to major overlay surfaces.
It does not forbid local floating UI inside a modal. Components such as Popover, Menu, Picker, Tooltip, and similar anchored primitives are expected to open within the current modal when they belong to a control inside that modal.
If a modal workflow needs another step, do not stack a second overlay above it.
Prefer one of these patterns instead:
- expand the existing modal
- swap the modal body to the next step
- use an inline stay-in-context pattern inside the modal
- close the modal and open a
Panel - escalate to a page if the task is large enough to deserve its own workspace
If a feature seems to require Modal -> Panel or Modal -> Modal, that is usually a sign the first surface was the wrong abstraction.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
isOpen | boolean | — | Required. Controls visibility |
title | ReactNode | — | Required. Modal heading |
children | ReactNode | — | Required. Modal body content |
handleClose | () => void | — | Called when modal should close |
action | ModalAction | — | Primary action button config |
subTitle | ReactNode | — | Subtitle below the title |
Icon | ComponentType | — | Icon in the header |
width | string | number | — | Custom width |
maxWidth | string | number | — | Maximum width |
height | string | number | — | Custom height |
maxHeight | string | number | — | Maximum height |
fullscreen | boolean | false | Expands to fill the viewport |
closeOnOutsideClick | boolean | true | Close when clicking the backdrop |
withPadding | boolean | true | Applies body padding |
hideHeader | boolean | false | Hides the header section |
secondaryLabel | string | 'Cancel' | Secondary button label |
footer | ReactNode | — | Custom footer content |
zIndex | number | 80 | CSS z-index |
ModalAction
| Prop | Type | Description |
|---|---|---|
type | 'button' | 'submit' | Required. Action type |
label | string | Button label |
onClick | () => void | Click handler (button type) |
formId | string | Form ID to submit (submit type) |
variant | ButtonVariant | Button variant |
isLoading | boolean | Shows loading spinner |
disabled | boolean | Disables the action |
icon | IconName | Action button icon |
Guidelines
- Do always provide a clear title that describes the task
- Do use
closeOnOutsideClickfor non-destructive modals - Do set
closeOnOutsideClick={false}for forms with unsaved changes - Do treat
Modalas the top overlay layer abovePanel - Don't nest modals inside modals
- Don't open a
Panelon top of aModal - Don't use modals for simple confirmations — use
ConfirmActionModaland follow theConfirmation Modalspattern instead - Don't use fullscreen mode on desktop unless the content genuinely requires it