Modal Imperative API Guide
SkillDev toolsLets your agent write code that opens modals, dialogs and confirmations using the base-ui modal APIs.
Use Modal Imperative API Guide in Claude, ChatGPT or Ahel Desktop
Free. Sign in, add Modal Imperative API Guide and connect your AI. About a minute.
Also: Claude Code · Cursor · Codex
Then ask your AI: use the Modal Imperative API Guide skill
Details
Instructions available. Your AI can read the instructions. Execution depends on the setup they require.
Account requirements not reviewed. Check the skill instructions before use; ahel provides instructions and does not run this skill.
No other account needed.
Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
About this skill
Use for modals, dialogs and confirmations with createModal, confirmModal, ModalHost or base-ui modal APIs.
What this skill tells your AI
The instructions your AI receives, as published by lobehub/lobehub in .agents/skills/modal/SKILL.md and read by ahel’s review.
Recommended: @lobehub/ui/base-ui
New code should use the base-ui modal stack (headless primitives, not antd Modal):
createModal,confirmModal,ModalHostfrom@lobehub/ui/base-uiuseModalContextfrom@lobehub/ui/base-uiinside 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)
'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)
'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
import { createMyFeatureModal } from '@/features/MyFeatureModal';
const handleOpen = useCallback(() => {
createMyFeatureModal();
}, []);
return <Button onClick={handleOpen}>Open</Button>;
i18n
- Content:
useTranslationin components. createModaloptions:import { t } from 'i18next'where hooks are unavailable.
useModalContext
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:
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
import { confirmModal } from '@lobehub/ui/base-ui';
confirmModal({
title: '…',
content: '…',
okText: '…',
cancelText: '…',
onOk: async () => {},
});
Legacy: @lobehub/ui (root)
createModal from the root @lobehub/ui entry is typed as antd Modal props (children, allowFullscreen, getContainer, destroyOnHidden, styles.body, etc.). App code no longer imports it; do not reintroduce it — use @lobehub/ui/base-ui.
Examples
- Base-ui (preferred): follow sections above; ensure base-ui
ModalHostis mounted. - Base-ui call sites:
src/features/SkillStore/index.tsx,src/features/LibraryModal/CreateNew/index.tsx
Signals
- Hacker News mentions
- 20
Advanced
- Item type
- skill
- Key
modal-lobehub- Source
- github.com/lobehub/lobehub