Modal Imperative API Guide
SkillDev toolsUse for modals, dialogs and confirmations with createModal, confirmModal, ModalHost or base-ui modal APIs.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Modal Imperative API Guide skill
What this skill tells your AI
The instructions your AI receives, as published by find-xposed-magisk/lobe-chat 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)
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
ModalHostis mounted. - Legacy:
src/features/SkillStore/index.tsx,src/features/LibraryModal/CreateNew/index.tsx
Signals
- GitHub stars
- 30
- Forks
- 9
- Last commit
- Sep 2026
- Hacker News mentions
- 20
Others that do the same job
Advanced
- Catalog kind
- skill
- Gateway key
modal-find-xposed-magisk- Source
- github.com/find-xposed-magisk/lobe-chat