> Leaf UI 0.6.0 · Package: @ifhover/leaf-ui · Source: b30c3d3a8fa9c5e15d0d0522cebfbd33f60c6b06
> Browser documentation: https://ifhover.github.io/leaf-ui/v/0.6.0/en/components/modal.html

> For AI agents: the complete documentation index is available at https://ifhover.github.io/leaf-ui/v/0.6.0/en/llms.txt, the full documentation bundle is available at https://ifhover.github.io/leaf-ui/v/0.6.0/en/llms-full.txt.

# Modal

Display content or complete an action in a dialog.

## Edit content


Edit project

basic.tsx

Copy code

```tsx lineNumbers file="../../../src/examples/en/modal/basic.tsx"
import { Button, Form, FormField, Input, Modal, Select } from '@ifhover/leaf-ui';

import { useRef, useState } from 'react';

export function ModalBasic() {
  const [open, setOpen] = useState(false);
  const formRef = useRef<HTMLFormElement>(null);
  return (
    <>
      <Button onClick={() => setOpen(true)}>Edit project</Button>
      <Modal
        open={open}
        title="Project settings"
        confirmText="Save"
        onClose={() => setOpen(false)}
        onConfirm={() => formRef.current?.requestSubmit()}
      >
        <Form
          ref={formRef}
          layout="vertical"
          onSubmit={(event) => {
            event.preventDefault();
            setOpen(false);
          }}
        >
          <FormField label="Project name" required>
            <Input name="name" defaultValue="Leaf UI" />
          </FormField>
          <FormField label="Owner">
            <Select
              name="owner"
              options={[
                { value: 'design', label: 'Design team' },
                { value: 'engineering', label: 'Engineering team' },
              ]}
            />
          </FormField>
        </Form>
      </Modal>
    </>
  );
}
```

Expand code

## Custom footer


Custom footer

footer.tsx

Copy code

```tsx lineNumbers file="../../../src/examples/en/modal/footer.tsx"
import { Button, Modal } from '@ifhover/leaf-ui';

import { useState } from 'react';

export function ModalFooter() {
  const [open, setOpen] = useState(false);
  const [extra, setExtra] = useState(false);
  return (
    <>
      <Button variant="outline" onClick={() => setOpen(true)}>
        Custom footer
      </Button>
      <Modal
        open={open}
        title="More actions"
        onClose={() => setOpen(false)}
        footer={({ cancelButton, confirmButton }) => (
          <>
            <Button
              variant="ghost"
              style={{ marginRight: 'auto' }}
              onClick={() => setExtra(!extra)}
            >
              Details
            </Button>
            {cancelButton}
            {confirmButton}
          </>
        )}
      >
        <p>Extend the footer while keeping the default actions.</p>
        {extra && <p>Additional details are shown here.</p>}
      </Modal>
    </>
  );
}
```

Expand code

## Destroying state and initial focus


Open editor

capabilities.tsx

Copy code

```tsx lineNumbers file="../../../src/examples/modal/capabilities.tsx"
import { Button, Input, Modal } from '@ifhover/leaf-ui';

import { useRef, useState } from 'react';

export function Capabilities() {
  const [open, setOpen] = useState(false);
  const focus = useRef<HTMLInputElement>(null);
  return (
    <>
      <Button onClick={() => setOpen(true)}>{'Open editor'}</Button>
      <Modal
        open={open}
        onClose={() => setOpen(false)}
        title={'Disposable draft'}
        destroyOnClose
        initialFocus={focus}
        footer={({ cancelButton, confirmButton }) => (
          <>
            {cancelButton}
            {confirmButton}
          </>
        )}
      >
        <Input
          ref={focus}
          aria-label={'Draft title'}
          placeholder={'The draft resets after closing'}
        />
      </Modal>
    </>
  );
}
```

Expand code

## Icon and description

Use `icon` and `description` to explain the action.


Icon and description

description.tsx

Copy code

```tsx lineNumbers file="../../../src/examples/en/modal/description.tsx"
import { Button, Modal } from '@ifhover/leaf-ui';

import { Save } from 'lucide-react';

import { useState } from 'react';

export function ModalDescription() {
  const [open, setOpen] = useState(false);
  return (
    <>
      <Button variant="outline" onClick={() => setOpen(true)}>
        Icon and description
      </Button>
      <Modal
        open={open}
        title="Save settings"
        description="Your changes take effect immediately after saving."
        icon={<Save />}
        confirmText="Save"
        onClose={() => setOpen(false)}
        onConfirm={() => setOpen(false)}
      >
        Save the current settings?
      </Modal>
    </>
  );
}
```

Expand code

The body uses Scrollbar; long content scrolls while the heading and actions remain visible. Headers without a description and the footer use compact vertical spacing.

## Modal API

| Property             | Type                                                                                          | Default        | Description                                                         |
| -------------------- | --------------------------------------------------------------------------------------------- | -------------- | ------------------------------------------------------------------- |
| `initialFocus`       | `RefObject<HTMLElement \| null> \| (() => HTMLElement \| null)`                               | —              | Specify initial focus after opening.                                |
| `returnFocus`        | `boolean \| RefObject<HTMLElement \| null> \| (() => HTMLElement \| null)`                    | —              | Restore focus on close; specify a destination or use false.         |
| `open`               | `boolean`                                                                                     | Required       | Required. Whether the dialog is visible                             |
| `title`              | `ReactNode`                                                                                   | —              | Title / content                                                     |
| `description`        | `ReactNode`                                                                                   | —              | Additional description.                                             |
| `icon`               | `ReactNode`                                                                                   | —              | Decorative icon to the left of the heading.                         |
| `divided`            | `boolean`                                                                                     | `true`         | Separate the heading, content and footer with subtle borders.       |
| `footer`             | `ReactNode \| ((actions: `[`ModalFooterActions`](#modalfooteractions)`) => ReactNode)`        | Cancel + OK    | Custom footer; null hides it; a function receives default buttons   |
| `confirmText`        | `ReactNode`                                                                                   | Locale default | Button labels                                                       |
| `cancelText`         | `ReactNode`                                                                                   | Locale default | Button labels                                                       |
| `confirmLoading`     | `boolean`                                                                                     | `false`        | Confirm loading state                                               |
| `confirmButtonProps` | [`ButtonProps`](https://ifhover.github.io/leaf-ui/v/0.6.0/en/components/button.md#button-api) | —              | Customize the default buttons                                       |
| `cancelButtonProps`  | [`ButtonProps`](https://ifhover.github.io/leaf-ui/v/0.6.0/en/components/button.md#button-api) | —              | Customize the default buttons                                       |
| `onConfirm`          | `() => void \| Promise<void>`                                                                 | —              | Application controls open; rejection displays an error              |
| `onCancel`           | `() => void`                                                                                  | —              | Cancel or dismiss; falls back to onClose                            |
| `width`              | `number \| string`                                                                            | `480`          | Width; numbers are pixels                                           |
| `closable`           | `boolean`                                                                                     | `true`         | Close button / backdrop dismissal / Escape dismissal                |
| `maskClosable`       | `boolean`                                                                                     | `true`         | Close button / backdrop dismissal / Escape dismissal                |
| `keyboard`           | `boolean`                                                                                     | `true`         | Close button / backdrop dismissal / Escape dismissal                |
| `onClose`            | `() => void`                                                                                  | —              | Requests closure; update open in the application                    |
| `afterClose`         | `() => void`                                                                                  | —              | Called after the closing animation finishes                         |
| `forceRender`        | `boolean`                                                                                     | `false`        | Mount before opening and keep hidden while closed.                  |
| `destroyOnClose`     | `boolean`                                                                                     | `false`        | Unmount content after the closing transition; preserved by default. |
| `preserve`           | `boolean`                                                                                     | —              | Explicitly preserve closed content; overrides destroyOnClose.       |
| `container`          | `Element \| DocumentFragment \| (() => Element \| DocumentFragment)`                          | —              | Custom portal or scroll container.                                  |

## ModalFooterActions

| Property        | Type        | Default | Description                                         |
| --------------- | ----------- | ------- | --------------------------------------------------- |
| `confirmButton` | `ReactNode` | —       | Required. Default confirm button with loading state |
| `cancelButton`  | `ReactNode` | —       | Required. Default cancel button                     |

## Types

Import types when defining reusable component props or assembling options.

```ts
import type { ModalProps, ModalFooterActions } from '@ifhover/leaf-ui';
```

## Usage notes

Focus moves into the dialog when opened and returns to the trigger on dismissal. Tab stays within the dialog. Use maskClosable and keyboard to control backdrop and Escape dismissal. Supply aria-label when omitting title.

Customize footer actions or place a Form inside the body. Picker overlays inside a dialog retain its regional theme.

The default footer has Cancel followed by OK. With onConfirm, update open after the action; without it, OK calls onClose. Promises show loading and prevent duplicates. Submit forms with requestSubmit() in onConfirm or a submit type and form ID in confirmButtonProps. Native validation still applies. A footer function extends default buttons; `footer={null}` hides the footer.

## DialogFocusOptions

| Property       | Type                                                                       | Default | Description                                                 |
| -------------- | -------------------------------------------------------------------------- | ------- | ----------------------------------------------------------- |
| `initialFocus` | `RefObject<HTMLElement \| null> \| (() => HTMLElement \| null)`            | —       | Specify initial focus after opening.                        |
| `returnFocus`  | `boolean \| RefObject<HTMLElement \| null> \| (() => HTMLElement \| null)` | —       | Restore focus on close; specify a destination or use false. |

## Surfaces and rich content

Use `description` for supporting text and `icon` for a decorative heading icon. Divided dialogs have subtle dividers and a muted footer by default; `divided={false}` groups content with spacing. A `footer` function can add status text on the left. Group its buttons with `Space` so they wrap together and stay aligned to the right on narrow screens.
