modal

LobeHub imperative modal conventions. Use when creating or migrating modals, dialogs, popups, confirm flows, ModalHost wiring, createModal, confirmModal, useModalContext, or base-ui modal APIs.

lobehub/lobehub3.8k installsApache-2.0Synced Aug 31

Works with

Claude CodeCursorCodex CLIGitHub CopilotGemini CLI
---
name: modal
description: LobeHub imperative modal conventions. Use when creating or migrating modals, dialogs, popups, confirm flows, ModalHost wiring, createModal, confirmModal, useModalContext, or base-ui modal APIs.
license: Apache-2.0
---

# Modal Imperative API Guide

## Recommended: `@lobehub/ui/base-ui`

New code should use the **base-ui** modal stack (headless primitives, not antd `Modal`):

- `createModal`, `confirmModal`, `ModalHost` from `@lobehub/ui/base-ui`
- `useModalContext` from `@lobehub/ui/base-ui` inside modal **content**

Body slot: pass **`content`** (or `children`; runtime uses `content ?? children`).

### Global `ModalHost` (required)

Base-ui `createModal` renders through a **separate** host from the root package. The app must mount **`ModalHost`** from `@lobehub/ui/base-ui` once near the root (e.g. next to other global hosts). Without it, `createModal` calls will not appear.

If the project only mounts `ModalHost` from `@lobehub/ui`, add a second lazy `ModalHost` from `@lobehub/ui/base-ui` until all imperative modals are migrated.

### Why imperative?

| Mode        | Characteristics                      | Recommended |
| ----------- | ------------------------------------ | ----------- |
| Declarative | `open` state + `<Modal />`           | ❌          |
| Imperative  | Call `createModal()`, no local state | ✅          |

### File structure

```
features/
└── MyFeatureModal/
    ├── index.tsx            # export createXxxModal
    └── MyFeatureContent.tsx # modal body
```

### 1. Content (`MyFeatureContent.tsx`)

```tsx
'use client';

import { useModalContext } from '@lobehub/ui/base-ui';
import { useTranslation } from 'react-i18next';

export const MyFeatureContent = () => {
  const { t } = useTranslation('namespace');
  const { close } = useModalContext();

  return <div>{/* ... */}</div>;
};
```

### 2. `createModal` (`index.tsx`)

```tsx
'use client';

import { createModal } from '@lobehub/ui/base-ui';
import { t } from 'i18next';

import { MyFeatureContent } from './MyFeatureContent';

export const createMyFeatureModal = () =>
  createModal({
    content: <MyFeatureContent />,
    footer: null,
    maskClosable: true,
    styles: {
      content: { overflow: 'hidden', padding: 0 },
    },
    title: t('myFeature.title', { ns: 'setting' }),
    width: 'min(80%, 800px)',
  });
```

### 3. Usage

```tsx
import { createMyFeatureModal } from '@/features/MyFeatureModal';

const handleOpen = useCallback(() => {
  createMyFeatureModal();
}, []);

return <Button onClick={handleOpen}>Open</Button>;
```

### i18n

- **Content**: `useTranslation` in components.
- **`createModal` options**: `import { t } from 'i18next'` where hooks are unavailable.

### `useModalContext`

```tsx
const { close, setCanDismissByClickOutside } = useModalContext();
```

### Closing: which callback actually fires

`close()` — from `useModalContext()` inside the content, or from the returned
`ModalInstance` — only flips the stack entry to `open: false`. It does **not** go
through base-ui's dismissal path, so:

| callback               | user dismissal (Esc / backdrop / header ✕) | `close()` from content or instance |
| ---------------------- | ------------------------------------------ | ---------------------------------- |
| `onOpenChange`         | fires                                      | **does not fire**                  |
| `onOpenChangeComplete` | fires with `false`                         | fires with `false`                 |

Put caller-side cleanup (clearing an editing flag, resetting the provider's
`open` state) on **`onOpenChangeComplete`**. Wiring it to `onOpenChange` looks
correct until a footer button closes the modal, and then the caller never learns
it went away — typically leaving a flag set so the modal cannot be reopened.

`createModal` only ever completes with `false` (the imperative renderer supplies
the argument itself and never forwards the prop to base-ui), but still guard on
it — other base-ui primitives such as `DropdownMenu` do report both directions,
and the guard keeps the call site from depending on that difference:

```tsx
onOpenChangeComplete: (open) => {
  if (!open) onClosed?.();
},
```

### Common options (base-ui)

`ImperativeModalProps` builds on `BaseModalProps`: `title`, `width`, `maskClosable`, `open`, `onOpenChange`, `footer`, `styles` / `classNames` (keys: `backdrop`, `popup`, `header`, `title`, `close`, `content`, …).

| Property       | Notes                                    |
| -------------- | ---------------------------------------- |
| `content`      | Main body (preferred name vs `children`) |
| `maskClosable` | Click outside to dismiss                 |
| `styles.*`     | Semantic regions, not antd `styles.body` |

### Confirm

```tsx
import { confirmModal } from '@lobehub/ui/base-ui';

confirmModal({
  title: '…',
  content: '…',
  okText: '…',
  cancelText: '…',
  onOk: async () => {},
});
```

---

## Legacy: `@lobehub/ui` (root)

Older call sites use **`createModal` from `@lobehub/ui`**, which is typed as **antd `Modal` props** (`children`, `allowFullscreen`, `getContainer`, `destroyOnHidden`, `styles.body`, etc.). Prefer migrating new work to **`@lobehub/ui/base-ui`**.

Examples (legacy): `src/features/SkillStore/index.tsx`, `src/features/LibraryModal/CreateNew/index.tsx`.

---

## Examples

- Base-ui (preferred): follow sections above; ensure **base-ui `ModalHost`** is mounted.
- Legacy: `src/features/SkillStore/index.tsx`, `src/features/LibraryModal/CreateNew/index.tsx`

More General & Other skills

← All General & Other skills

Check your AI visibility

One URL in, a 0–100 score and the exact fixes out.

RUN THE CHECK

Browse all the tools

15 tools across six categories
13 of them never send your data anywhere

Free · No signup · No trial clock

SEE THE DIRECTORY