Modal

by lobehub6a3eba96b010No license83K starsListed Oct 8, 2026Updated Oct 8, 2026Repository updated today

Use for modals, dialogs and confirmations with createModal, confirmModal, ModalHost or base-ui modal APIs.

Instructions onlySoftware Development
AI-generated overview

Guides developers in using the @lobehub/ui base-ui imperative modal APIs for dialogs and confirmations.

What it does
This skill is a reference guide for building modals, dialogs and confirmations with the base-ui modal stack from @lobehub/ui. It documents createModal, confirmModal, ModalHost and useModalContext, including required host mounting, file structure, i18n handling, common options and close-callback behavior. It also notes that the legacy root-package createModal is deprecated and should not be reintroduced.
When to use it
Use it when implementing or migrating imperative modals, dialogs or confirmation prompts in a project that uses @lobehub/ui. It is also relevant when debugging why a modal does not appear or why caller-side cleanup does not run after closing.
Requirements
Requires a React project with @lobehub/ui and its base-ui entry, plus i18next/react-i18next for the translation patterns shown. No scripts are shipped; it is instructions only.

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?

ModeCharacteristicsRecommended
Declarativeopen state + <Modal />❌
ImperativeCall 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:

callbackuser dismissal (Esc / backdrop / header ✕)close() from content or instance
onOpenChangefiresdoes not fire
onOpenChangeCompletefires with falsefires 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, …).

PropertyNotes
contentMain body (preferred name vs children)
maskClosableClick 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)

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 ModalHost is mounted.
  • Base-ui call sites: src/features/SkillStore/index.tsx, src/features/LibraryModal/CreateNew/index.tsx

Source and attribution

Source:lobehub/lobehubin.agents/skills/modalat commit6a3eba9

License: No license

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal