Skip to content

Instantly share code, notes, and snippets.

@marcospgp
Created October 9, 2025 20:01
Show Gist options
  • Save marcospgp/8402e48c8a888891a2e27c41888d3d49 to your computer and use it in GitHub Desktop.
Save marcospgp/8402e48c8a888891a2e27c41888d3d49 to your computer and use it in GitHub Desktop.
react baseUI dialog manager
import { Dialog as D } from "@base-ui-components/react/dialog";
import { type ComponentType, type ReactNode, useEffect, useState } from "react";
import { Button } from "./Button";
import { Form, Input } from "./forms";
export type DialogAction = {
label: string;
action?: () => void | Promise<void>;
danger?: boolean;
};
function DialogTitle(props: Readonly<{ title: string }>) {
return <D.Title render={(ps) => <h1 {...ps}>{props.title}</h1>} />;
}
function DialogDescription(props: Readonly<{ description: string }>) {
return (
<D.Description className="text-text-muted col-span-2" render={<div>{props.description}</div>} />
);
}
function DialogButtons(props: Readonly<{ actions: DialogAction[]; onClose: () => void }>) {
return (
<div className="mt-md gap-xs col-span-2 flex justify-end">
{props.actions.map((action, index) => (
<Button
key={index}
danger={action.danger}
onClick={() => {
void (async () => {
if (action.action) await action.action();
props.onClose();
})();
}}
>
{action.label}
</Button>
))}
</div>
);
}
type BaseDialogOptions = {
dismissible?: boolean;
fitContent?: boolean;
};
type DialogState = {
id: number;
open: boolean;
hasBeenOpened: boolean;
isClosing: boolean;
element: ReactNode;
onOpenChange: (open: boolean) => void;
onRemove: () => void | Promise<void>;
} & BaseDialogOptions;
const dialogStore = {
dialogs: [] as DialogState[],
nextId: 0,
listener: null as (() => void) | null,
add(
id: number,
element: ReactNode,
onRemove: () => void | Promise<void>,
options?: { dismissible?: boolean; fitContent?: boolean },
) {
const dialog: DialogState = {
id,
open: false,
hasBeenOpened: false,
isClosing: false,
element,
onRemove,
dismissible: options?.dismissible,
fitContent: options?.fitContent,
// Handles all close triggers (escape, backdrop, button clicks).
// Sets isClosing to immediately exclude dialog from position
// calculations, so parent dialogs transition back to full size without
// delay.
onOpenChange: (open) => {
this.dialogs = this.dialogs.map((d) =>
d.id === id ? { ...d, open, isClosing: !open ? true : d.isClosing } : d,
);
this.listener?.();
},
};
this.dialogs = [...this.dialogs, dialog];
this.listener?.();
},
remove(id: number) {
const dialog = this.dialogs.find((d) => d.id === id);
if (dialog) {
dialog.onOpenChange(false);
}
},
removeComplete(id: number) {
this.dialogs = this.dialogs.filter((d) => d.id !== id);
this.listener?.();
},
};
type PromptDialogContentProps = Readonly<{
title: string;
onSubmit: (value: string) => void | Promise<void>;
description?: string;
label?: string;
placeholder?: string;
cancelLabel?: string;
submitLabel?: string;
}>;
function PromptDialogContent(props: PromptDialogContentProps & { close: () => void }) {
const [inputRef, setInputRef] = useState<HTMLInputElement | null>(null);
return (
<>
<DialogTitle title={props.title} />
{props.description && <DialogDescription description={props.description} />}
<Form.Root
onSubmit={async () => {
await props.onSubmit(inputRef?.value ?? "");
props.close();
}}
>
<Input
name="input"
label={props.label}
ref={setInputRef}
placeholder={props.placeholder}
autoComplete="off"
autoFocus
grow
validate={(value: unknown) => {
const valid = typeof value === "string" && value.trim().length > 0;
return valid ? null : "Please enter a value";
}}
validationMode="onChange"
/>
</Form.Root>
<DialogButtons
actions={[
{ label: props.cancelLabel ?? "Cancel" },
{
label: props.submitLabel ?? "Submit",
action: async () => {
await props.onSubmit(inputRef?.value ?? "");
props.close();
},
},
]}
onClose={props.close}
/>
</>
);
}
type ConfirmDialogContentProps = Readonly<{
title: string;
description?: string;
onConfirm?: () => void | Promise<void>;
confirmLabel?: string;
cancelLabel?: string;
danger?: boolean;
}>;
function ConfirmDialogContent(props: ConfirmDialogContentProps & { close: () => void }) {
const actions: DialogAction[] = [
{ label: props.cancelLabel ?? "Cancel" },
{
label: props.confirmLabel ?? "Ok",
action: props.onConfirm,
danger: props.danger,
},
];
return (
<>
<DialogTitle title={props.title} />
{props.description && <DialogDescription description={props.description} />}
<DialogButtons actions={actions} onClose={props.close} />
</>
);
}
type AlertDialogContentProps = Readonly<{
title: string;
description?: string;
label?: string;
}>;
function AlertDialogContent(props: AlertDialogContentProps & { close: () => void }) {
const label = props.label ?? "Got it";
return (
<>
<DialogTitle title={props.title} />
{props.description && <DialogDescription description={props.description} />}
<DialogButtons actions={[{ label }]} onClose={props.close} />
</>
);
}
type FormDialogContentProps = Readonly<{
title: string;
formChildren: ReactNode;
onSubmit: () => void | Promise<void>;
description?: string;
cancelLabel?: string;
submitLabel?: string;
}>;
function FormDialogContent(props: FormDialogContentProps & { close: () => void }) {
return (
<>
<DialogTitle title={props.title} />
{props.description && <DialogDescription description={props.description} />}
<Form.Root
onSubmit={async () => {
await props.onSubmit();
props.close();
}}
>
{props.formChildren}
</Form.Root>
<DialogButtons
actions={[
{ label: props.cancelLabel ?? "Cancel" },
{
label: props.submitLabel ?? "Submit",
action: async () => {
await props.onSubmit();
props.close();
},
},
]}
onClose={props.close}
/>
</>
);
}
type CustomDialogContentProps = Readonly<{
content: ReactNode | ComponentType<{ close: () => void }>;
title?: string | null;
actions?: DialogAction[];
}>;
function CustomDialogContent(props: CustomDialogContentProps & { close: () => void }) {
let contentElement: ReactNode;
if (typeof props.content === "function") {
const ContentComponent = props.content;
contentElement = <ContentComponent close={props.close} />;
} else {
contentElement = props.content;
}
return (
<>
{props.title !== undefined && props.title !== null && <DialogTitle title={props.title} />}
{contentElement}
{props.actions && props.actions.length > 0 && (
<DialogButtons actions={props.actions} onClose={props.close} />
)}
</>
);
}
type AlertDialogProps = AlertDialogContentProps & Pick<BaseDialogOptions, "dismissible">;
type CustomDialogProps = CustomDialogContentProps &
BaseDialogOptions & { onClose?: () => void | Promise<void> };
/**
* Imperative API allows auto state reset on dialog close via key-based component
* remounting and centralizes complex rendering logic (child dialog transitions,
* stacking, backdrop) in DialogProvider. Declarative would require manual state
* management and scattered render logic.
*/
export const dialogManager = {
prompt: (props: PromptDialogContentProps) => {
const id = dialogStore.nextId++;
const close = () => {
dialogStore.remove(id);
};
dialogStore.add(id, <PromptDialogContent {...props} close={close} />, () => {
dialogStore.removeComplete(id);
});
},
confirm: (props: ConfirmDialogContentProps) => {
const id = dialogStore.nextId++;
const close = () => {
dialogStore.remove(id);
};
dialogStore.add(id, <ConfirmDialogContent {...props} close={close} />, () => {
dialogStore.removeComplete(id);
});
},
alert: (props: AlertDialogProps) => {
const id = dialogStore.nextId++;
const close = () => {
dialogStore.remove(id);
};
const { dismissible, ...contentProps } = props;
dialogStore.add(
id,
<AlertDialogContent {...contentProps} close={close} />,
() => {
dialogStore.removeComplete(id);
},
{ dismissible },
);
},
form: (props: FormDialogContentProps) => {
const id = dialogStore.nextId++;
const close = () => {
dialogStore.remove(id);
};
dialogStore.add(id, <FormDialogContent {...props} close={close} />, () => {
dialogStore.removeComplete(id);
});
},
custom: (props: CustomDialogProps) => {
const id = dialogStore.nextId++;
const close = () => {
dialogStore.remove(id);
};
const { onClose, fitContent, dismissible, ...contentProps } = props;
dialogStore.add(
id,
<CustomDialogContent {...contentProps} close={close} />,
async () => {
await onClose?.();
dialogStore.removeComplete(id);
},
{ fitContent, dismissible },
);
},
};
/**
* Should be included once in app layout.
*
* Per React docs: "Components should only be used in JSX. Don't call them as
* regular functions." Calling component functions directly violates Rules of
* Hooks - hooks attach to the caller's chain instead of the component's own
* boundary, causing hook order violations when dialogs open/close.
*/
export function DialogProvider() {
const [_renderCount, setRenderCount] = useState(0);
useEffect(() => {
dialogStore.listener = () => {
setRenderCount((prev) => prev + 1);
};
return () => {
dialogStore.listener = null;
};
}, []);
// Open dialogs after they're added to DOM to trigger CSS transitions
useEffect(() => {
dialogStore.dialogs.forEach((dialog) => {
if (!dialog.hasBeenOpened) {
dialogStore.dialogs = dialogStore.dialogs.map((d) =>
d.id === dialog.id ? { ...d, open: true, hasBeenOpened: true } : d,
);
dialogStore.listener?.();
}
});
});
// BaseUI expects nested dialogs for proper backdrop/styling handling, but
// nesting causes all parent dialogs to re-render when children open/close,
// replaying transitions.
// We render as siblings and manually handle backdrop + stacking styles to
// avoid this.
const openDialogCount = dialogStore.dialogs.filter((d) => d.open).length;
// isClosing flag excludes dialogs from positioning calculations while they
// animate out, so parent dialogs instantly scale back up when children close.
const activeDialogs = dialogStore.dialogs.filter((d) => !d.isClosing);
return (
<>
{openDialogCount > 0 && (
<div className="fixed inset-0 cursor-pointer bg-black opacity-50 transition-opacity" />
)}
{dialogStore.dialogs.map((dialog) => {
const activeIndex = activeDialogs.indexOf(dialog);
const reverseIndex = activeIndex >= 0 ? activeDialogs.length - 1 - activeIndex : 0;
return (
<D.Root
key={dialog.id}
open={dialog.open}
onOpenChange={dialog.onOpenChange}
onOpenChangeComplete={(isOpen) => {
if (isOpen) return;
void dialog.onRemove();
}}
dismissible={dialog.dismissible}
>
<D.Portal>
<div className="fixed inset-0 flex items-center justify-center">
<D.Popup
className={`${!dialog.fitContent && "w-lg"} bg-bg px-md py-sm bevel relative flex max-w-[min(var(--container-lg),80vw)] flex-col transition data-[ending-style]:scale-90 data-[starting-style]:scale-90 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0`}
style={{
transform: `translateY(${reverseIndex * 32}px) scale(${Math.max(0, 1 - reverseIndex * 0.1)})`,
}}
>
{dialog.element}
</D.Popup>
</div>
</D.Portal>
</D.Root>
);
})}
</>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment