Core logic for the dialog widget implemented as a state machine
npx @tessl/cli install tessl/npm-zag-js--dialog@1.22.0Zag.js Dialog provides a headless, framework-agnostic dialog (modal) component implemented as a finite state machine. It delivers accessible dialog functionality following WAI-ARIA authoring practices, with complete keyboard interactions, focus management, and ARIA roles/attributes handled automatically.
npm install @zag-js/dialogimport { machine, connect, anatomy, props, splitProps } from "@zag-js/dialog";
import type { Api, Props, Service, ElementIds, OpenChangeDetails } from "@zag-js/dialog";
import type { FocusOutsideEvent, InteractOutsideEvent, PointerDownOutsideEvent } from "@zag-js/dialog";For CommonJS:
const { machine, connect, anatomy, props, splitProps } = require("@zag-js/dialog");import { machine, connect } from "@zag-js/dialog";
import { normalizeProps } from "@zag-js/react"; // or your framework adapter
function MyDialog() {
// Create machine instance
const [state, send] = useMachine(
machine({
id: "dialog-1",
onOpenChange: (details) => {
console.log("Dialog open:", details.open);
},
})
);
// Connect machine to UI
const api = connect(state, normalizeProps);
return (
<div>
<button {...api.getTriggerProps()}>Open Dialog</button>
{api.open && (
<div {...api.getPositionerProps()}>
<div {...api.getBackdropProps()} />
<div {...api.getContentProps()}>
<h2 {...api.getTitleProps()}>Dialog Title</h2>
<p {...api.getDescriptionProps()}>Dialog content goes here</p>
<button {...api.getCloseTriggerProps()}>Close</button>
</div>
</div>
)}
</div>
);
}Zag.js Dialog is built around several key components:
Core state machine that manages dialog open/closed states, handles events, and orchestrates all dialog behaviors including accessibility features.
function machine(props: Props): Machine;
interface Machine extends StateMachine<DialogSchema> {
state: "open" | "closed";
send: (event: MachineEvent) => void;
}
interface MachineEvent {
type: "OPEN" | "CLOSE" | "TOGGLE" | "CONTROLLED.OPEN" | "CONTROLLED.CLOSE";
}Connection layer that transforms state machine into framework-agnostic prop functions for UI elements, providing complete accessibility and interaction handling.
function connect<T extends PropTypes>(
service: Service<DialogSchema>,
normalize: NormalizeProps<T>
): Api<T>;
interface Api<T extends PropTypes> {
open: boolean;
setOpen: (open: boolean) => void;
getTriggerProps: () => T["button"];
getBackdropProps: () => T["element"];
getPositionerProps: () => T["element"];
getContentProps: () => T["element"];
getTitleProps: () => T["element"];
getDescriptionProps: () => T["element"];
getCloseTriggerProps: () => T["button"];
}Anatomical structure defining the dialog's component parts with consistent naming and CSS class generation.
interface Anatomy {
trigger: AnatomyPart;
backdrop: AnatomyPart;
positioner: AnatomyPart;
content: AnatomyPart;
title: AnatomyPart;
description: AnatomyPart;
closeTrigger: AnatomyPart;
}
const anatomy: Anatomy;Event types for dismissible interactions that can occur outside the dialog.
type FocusOutsideEvent = CustomEvent & {
target: Element;
preventDefault: () => void;
};
type InteractOutsideEvent = CustomEvent & {
target: Element;
preventDefault: () => void;
};
type PointerDownOutsideEvent = CustomEvent & {
target: Element;
preventDefault: () => void;
};Utility functions for working with dialog props in framework integrations.
/**
* Props utility for type-safe prop definitions
*/
const props: PropsDefinition<Props>;
/**
* Split props utility for separating dialog props from other props
* @param allProps - Object containing all props
* @returns Tuple with [dialogProps, otherProps]
*/
const splitProps: <T extends Partial<Props>>(
allProps: T & Record<string, any>
) => [T, Record<string, any>];
interface PropsDefinition<T> {
/** Array of prop keys for type validation */
keys: (keyof T)[];
/** Type-safe prop object */
object: T;
}interface Props {
// Element IDs
ids?: ElementIds;
// State Management
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (details: OpenChangeDetails) => void;
// Behavior Configuration
modal?: boolean;
trapFocus?: boolean;
preventScroll?: boolean;
restoreFocus?: boolean;
closeOnInteractOutside?: boolean;
closeOnEscape?: boolean;
// Accessibility
role?: "dialog" | "alertdialog";
"aria-label"?: string;
// Focus Management
initialFocusEl?: () => MaybeElement;
finalFocusEl?: () => MaybeElement;
// Event Handlers
onInteractOutside?: (event: InteractOutsideEvent) => void;
onFocusOutside?: (event: FocusOutsideEvent) => void;
onPointerDownOutside?: (event: PointerDownOutsideEvent) => void;
onEscapeKeyDown?: (event: KeyboardEvent) => void;
onRequestDismiss?: () => void;
// Other
dir?: "ltr" | "rtl";
id?: string;
getRootNode?: () => Node | ShadowRoot | Document;
persistentElements?: () => Element[];
}
interface ElementIds {
trigger?: string;
positioner?: string;
backdrop?: string;
content?: string;
closeTrigger?: string;
title?: string;
description?: string;
}
interface OpenChangeDetails {
open: boolean;
}
type MaybeElement = Element | null | undefined;interface Service extends StateMachineService<DialogSchema> {
state: State;
send: (event: MachineEvent) => void;
context: Context;
prop: (key: string) => any;
scope: Scope;
}
interface DialogSchema {
props: Props;
state: "open" | "closed";
context: {
rendered: { title: boolean; description: boolean };
};
guard: "isOpenControlled";
effect: "trackDismissableElement" | "preventScroll" | "trapFocus" | "hideContentBelow";
action: "checkRenderedElements" | "syncZIndex" | "invokeOnClose" | "invokeOnOpen" | "toggleVisibility";
event: MachineEvent;
}