| name | implement-modus-modal-with-refs |
| description | Create Modus modals using the forwardRef + useImperativeHandle pattern for programmatic control |
Implement Modus Modal with Refs
Create Modus modal components using the forwardRef + useImperativeHandle pattern for programmatic control.
When to Use
Use this skill when:
- Creating modal dialogs that need to be opened/closed programmatically
- You need to control modal state from parent components
- Building modals with custom triggers (not just button clicks)
Pattern Overview
Modus modals require:
forwardRef to expose methods to parent components
useImperativeHandle to define the API (openModal, closeModal)
querySelector("dialog") to access the native dialog element
- Event listeners for dialog close events
Reference Implementation
See src/components/ModusModal.tsx for the complete implementation.
Complete Template
import { ModusWcModal } from "@trimble-oss/moduswebcomponents-react";
import type { ReactNode } from "react";
import { useRef, useEffect, forwardRef, useImperativeHandle } from "react";
interface ModusModalProps {
modalId: string;
ariaLabel?: string;
backdrop?: 'default' | 'static';
position?: 'top' | 'center' | 'bottom';
fullscreen?: boolean;
showFullscreenToggle?: boolean;
showClose?: boolean;
customClass?: string;
header?: ReactNode;
children: ReactNode;
footer?: ReactNode;
onClose?: () => void;
}
export interface ModusModalRef {
openModal: () => void;
closeModal: () => void;
}
const ModusModal = forwardRef<ModusModalRef, ModusModalProps>(
(
{
modalId,
ariaLabel,
backdrop = 'default',
position = 'center',
fullscreen = false,
showFullscreenToggle = false,
showClose = true,
customClass,
header,
children,
footer,
onClose,
},
ref
) => {
const modalRef = useRef<HTMLModusWcModalElement>(null);
const openModal = () => {
if (modalRef.current) {
const dialog = modalRef.current.querySelector(
"dialog"
) as HTMLDialogElement;
if (dialog) {
dialog.showModal();
}
}
};
const closeModal = () => {
if (modalRef.current) {
const dialog = modalRef.current.querySelector(
"dialog"
) as HTMLDialogElement;
if (dialog) {
dialog.close();
}
}
};
useImperativeHandle(ref, () => ({
openModal,
closeModal,
}));
useEffect(() => {
const modal = modalRef.current;
if (modal) {
const handleClose = () => {
onClose?.();
};
const dialogElement = modal.querySelector("dialog");
if (dialogElement) {
dialogElement.addEventListener("close", handleClose);
return () => {
dialogElement.removeEventListener("close", handleClose);
};
}
}
}, [onClose]);
return (
<ModusWcModal
ref={modalRef}
modal-id={modalId}
aria-label={ariaLabel}
backdrop={backdrop}
position={position}
fullscreen={fullscreen}
show-fullscreen-toggle={showFullscreenToggle}
show-close={showClose}
custom-class={customClass}
>
{header && <div slot="header">{header}</div>}
<div slot="content">{children}</div>
{footer && <div slot="footer">{footer}</div>}
</ModusWcModal>
);
}
);
ModusModal.displayName = "ModusModal";
export default ModusModal;
Usage Pattern
import { useRef } from "react";
import ModusButton from "./components/ModusButton";
import ModusModal, { type ModusModalRef } from "./components/ModusModal";
function MyComponent() {
const modalRef = useRef<ModusModalRef>(null);
return (
<>
<ModusButton
onButtonClick={() => {
modalRef.current?.openModal();
}}
>
Open Modal
</ModusButton>
<ModusModal
ref={modalRef}
modalId="my-modal"
ariaLabel="Example modal"
onClose={() => {
console.log("Modal closed");
}}
header={
<div className="text-xl font-semibold text-foreground">
Modal Title
</div>
}
footer={
<div className="flex gap-2">
<ModusButton
variant="borderless"
onButtonClick={() => {
modalRef.current?.closeModal();
}}
>
Cancel
</ModusButton>
<ModusButton
onButtonClick={() => {
modalRef.current?.closeModal();
}}
>
Confirm
</ModusButton>
</div>
}
>
<div className="text-sm text-foreground opacity-80">
Modal content goes here.
</div>
</ModusModal>
</>
);
}
Key Patterns
1. forwardRef Pattern
const ModusModal = forwardRef<ModusModalRef, ModusModalProps>(
(props, ref) => {
}
);
2. useImperativeHandle
useImperativeHandle(ref, () => ({
openModal,
closeModal,
}));
3. Dialog Access
const openModal = () => {
if (modalRef.current) {
const dialog = modalRef.current.querySelector(
"dialog"
) as HTMLDialogElement;
if (dialog) {
dialog.showModal();
}
}
};
Critical: Always use querySelector("dialog") - don't try to access dialog methods directly on the web component.
4. Slot Structure
Modus modals use slots for content:
<ModusWcModal>
{header && <div slot="header">{header}</div>}
<div slot="content">{children}</div>
{footer && <div slot="footer">{footer}</div>}
</ModusWcModal>
5. Event Handling
Listen to the native dialog close event:
useEffect(() => {
const modal = modalRef.current;
if (modal) {
const dialogElement = modal.querySelector("dialog");
if (dialogElement) {
dialogElement.addEventListener("close", handleClose);
return () => {
dialogElement.removeEventListener("close", handleClose);
};
}
}
}, [onClose]);
Modal Variants
Centered Modal (Default)
<ModusModal
ref={modalRef}
modalId="centered-modal"
position="center"
/>
Top Positioned Modal
<ModusModal
ref={modalRef}
modalId="top-modal"
position="top"
/>
Bottom Positioned Modal
<ModusModal
ref={modalRef}
modalId="bottom-modal"
position="bottom"
/>
Fullscreen Modal
<ModusModal
ref={modalRef}
modalId="fullscreen-modal"
fullscreen={true}
showFullscreenToggle={true}
/>
Static Backdrop Modal
<ModusModal
ref={modalRef}
modalId="static-modal"
backdrop="static"
/>
Common Mistakes
- Missing displayName: Always set
displayName for forwardRef components
- Wrong dialog access: Use
querySelector("dialog"), not direct property access
- Missing null checks: Always check
modalRef.current before use
- Wrong event: Listen to
close event on dialog element, not web component
- Forgetting cleanup: Always remove event listeners in cleanup function
Testing Checklist
Related Files
src/components/ModusModal.tsx - Complete implementation
src/demos/modal-demo/page.tsx - Usage examples
.cursor/rules/modus-modal-implementation-react.mdc - Detailed documentation