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.