Overlay / modal system for @uxf/ui: a controlled Modal component plus an imperative modal stack (ModalProvider + openModal / closeModal) with named layers.
Modal component when you own the open state locally (isOpen / onClose).openModal API (backed by a single ModalProvider) to open modals from anywhere — event handlers, services — without threading state, and to stack modals in named layers.DialogPanel (@uxf/ui/dialog) for the standard panel, use ModalDialog for a ready-made title / body / footer layout, or ModalHeader for just the header row.Modal wraps the lower-level Dialog and supplies its Floating UI plumbing, so you rarely need Dialog directly. There is no @uxf/form twin.
Modalimport { Button } from "@uxf/ui/button";
import { DialogPanel } from "@uxf/ui/dialog";
import { Modal } from "@uxf/ui/modal";
import { useState } from "react";
function Example() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<Button onClick={() => setIsOpen(true)}>Open</Button>
<Modal isOpen={isOpen} onClose={() => setIsOpen(false)}>
<DialogPanel width="xs">Modal content</DialogPanel>
</Modal>
</>
);
}
openModal)Mount a single ModalProvider once at the app root, wired to the shared ref:
import { getModalStackRef, ModalProvider } from "@uxf/ui/modal";
<ModalProvider ref={getModalStackRef()} />;
Then open modals from anywhere:
import { Button } from "@uxf/ui/button";
import { DialogPanel } from "@uxf/ui/dialog";
import { closeModal, openModal } from "@uxf/ui/modal";
openModal({
children: (
<DialogPanel width="xs">
<Button onClick={() => closeModal()}>Close</Button>
</DialogPanel>
),
onClose: handleClose,
});
Modal| Prop | Type | Default | Description |
|---|---|---|---|
isOpen | boolean | — (required) | Whether the modal is rendered. |
onClose | () => void | — (required) | Called when the modal requests to close (backdrop click or ESC, unless disabled). |
children | ReactNode | — (required) | Modal content, typically a DialogPanel. |
variant | ModalVariant | "default" | Visual variant (default, drawer-right). |
isBackdropCloseDisabled | boolean | false | Disable closing on outside / backdrop press. |
isEscKeyCloseDisabled | boolean | false | Disable closing on the ESC key. |
className | string | — | Extra class on the overlay. |
style | CSSProperties | — | Inline style on the overlay. |
openModal(modal, options?)modal is a ModalProviderProps descriptor; returns the created modal instance id.
| Field | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — (required) | Modal content. |
onClose | () => void | — | Called after the modal closes (backdrop, ESC, or programmatic). |
variant | ModalVariant | "default" | Visual variant. |
isBackdropCloseDisabled | boolean | false | Disable backdrop close. |
isEscapeKeyCloseDisabled | boolean | false | Disable ESC close. |
className | string | — | Extra class on the overlay. |
Note the different spelling: the
Modalcomponent usesisEscKeyCloseDisabled, whileopenModal/ModalProviderPropsusesisEscapeKeyCloseDisabled.
options is OpenModalOptions:
| Field | Type | Default | Description |
|---|---|---|---|
layer | ModalLayerName | defaultLayer ("main") | Layer to open in. |
shouldReplace | boolean | true | Replace an existing modal in the same layer instead of stacking. |
default or drawer-right (right-anchored drawer on sm+). Passed through to the underlying Dialog.isBackdropCloseDisabled / isEscKeyCloseDisabled (component) or isBackdropCloseDisabled / isEscapeKeyCloseDisabled (openModal) to disable them. In the stack, only the topmost modal responds to backdrop / ESC, so clicking inside a higher modal never closes ones underneath.The imperative API renders modals through the mounted ModalProvider. Service functions (from @uxf/ui/modal):
| Function | Description |
|---|---|
openModal(modal, options?) | Opens a modal in the given layer (or the default layer); returns its instance id. |
closeModal() | Closes the topmost modal (highest z-index). |
closeModalLayer(layer) | Closes all modals in the given layer. |
closeAllModals() | Closes every modal across all layers. |
getModalStackRef() | Returns the ref to pass to ModalProvider. |
getModalRef() | Deprecated alias of getModalStackRef(). |
By default two layers are configured (from DEFAULT_MODAL_LAYERS):
{
layers: {
main: { name: "main", zIndex: 100 },
confirm: { name: "confirm", zIndex: 1000 },
},
defaultLayer: "main",
}
import { closeModalLayer, openModal } from "@uxf/ui/modal";
import { DialogPanel } from "@uxf/ui/dialog";
openModal({ children: <DialogPanel>Confirmation</DialogPanel> }, { layer: "confirm" });
closeModalLayer("confirm");
Known limitation: the shipped
ModalProviderassigns every opened modal the default layer's z-index regardless of thelayeroption, so custom per-layer z-index values do not currently affect stacking (modals paint in the order they were opened). Layer names still work for targeted closing viacloseModalLayer.
Provide a custom configuration through ModalLayerConfigProvider (optional — the default config is used when absent):
import { ModalLayerConfigProvider, ModalLayersConfiguration, ModalProvider, getModalStackRef } from "@uxf/ui/modal";
const customModalLayers: ModalLayersConfiguration = {
layers: {
main: { name: "main", zIndex: 100 },
confirm: { name: "confirm", zIndex: 1000 },
notification: { name: "notification", zIndex: 9000 },
},
defaultLayer: "main",
};
<ModalLayerConfigProvider config={customModalLayers}>
<ModalProvider ref={getModalStackRef()} />
</ModalLayerConfigProvider>;
Add custom layer names to the type system with declaration merging on ModalLayers (@uxf/ui/modal/theme):
// modal.d.ts (in your project)
declare module "@uxf/ui/modal/theme" {
interface ModalLayers {
notification: true;
}
}
ModalLayerName (keyof ModalLayers) then autocompletes the new name in openModal(..., { layer }) and closeModalLayer.
Modal renders through Dialog, so import the dialog stylesheet once in your global CSS:
@import url("@uxf/ui/css/dialog.css");
Also requires the global @uxf/ui token layer, set up once per app — see @uxf/ui setup.
For the imperative API, mount exactly one <ModalProvider ref={getModalStackRef()} /> at the app root.