mcpField
M
modal

modal

github.com/lobehubDevelopment83.6k4.2k installsUpdated today
Repository

About modal

modal is a guide for implementing imperative modals with @lobehub/ui, primarily using its base-ui API. It covers setup, content, confirmation dialogs, internationalization, and callback behavior for developers working in the library's apps.

Key features

  1. 01Uses base-ui createModal and confirmModal
  2. 02Requires a global ModalHost
  3. 03Explains modal content and usage
  4. 04Documents dismissal callbacks
  5. 05Covers internationalization and options

Use cases

  1. 01Add an imperative modal to an app
  2. 02Configure cleanup after a modal closes
  3. 03Build a confirmation dialog
  4. 04Migrate usage to base-ui

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/lobehub/lobehub --skill modal

After installing, type/modalin Claude Code to confirm it loaded.

Details

From the project README

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)
'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: useTranslation in components.
  • createModal options: 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:

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:

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
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

Comments

Markdown supported