FSAI Design System
Components

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:

  1. page
  2. Panel
  3. Modal

That means:

  • a Modal may open on top of a Panel
  • a Panel must never open on top of a Modal
  • a Modal must never open on top of another Modal

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

PropTypeDefaultDescription
isOpenbooleanRequired. Controls visibility
titleReactNodeRequired. Modal heading
childrenReactNodeRequired. Modal body content
handleClose() => voidCalled when modal should close
actionModalActionPrimary action button config
subTitleReactNodeSubtitle below the title
IconComponentTypeIcon in the header
widthstring | numberCustom width
maxWidthstring | numberMaximum width
heightstring | numberCustom height
maxHeightstring | numberMaximum height
fullscreenbooleanfalseExpands to fill the viewport
closeOnOutsideClickbooleantrueClose when clicking the backdrop
withPaddingbooleantrueApplies body padding
hideHeaderbooleanfalseHides the header section
secondaryLabelstring'Cancel'Secondary button label
footerReactNodeCustom footer content
zIndexnumber80CSS z-index

ModalAction

PropTypeDescription
type'button' | 'submit'Required. Action type
labelstringButton label
onClick() => voidClick handler (button type)
formIdstringForm ID to submit (submit type)
variantButtonVariantButton variant
isLoadingbooleanShows loading spinner
disabledbooleanDisables the action
iconIconNameAction button icon

Guidelines

  • Do always provide a clear title that describes the task
  • Do use closeOnOutsideClick for non-destructive modals
  • Do set closeOnOutsideClick={false} for forms with unsaved changes
  • Do treat Modal as the top overlay layer above Panel
  • Don't nest modals inside modals
  • Don't open a Panel on top of a Modal
  • Don't use modals for simple confirmations — use ConfirmActionModal and follow the Confirmation Modals pattern instead
  • Don't use fullscreen mode on desktop unless the content genuinely requires it

On this page