Capítulo 8 de 51
Base UI's change events (onOpenChange, onValueChange, onPressedChange, ...) all receive a second eventDetails argument carrying the change reason, the native event, and cancel()/allowPropagation() methods — this is the mechanism for intercepting, canceling, or letting propagate any state change without fully taking over control of the component.
onOpenChange: (open, eventDetails) => void (and equivalently for onValueChange, onPressedChange, etc.).eventDetails shape: { reason: string, event: Event, cancel: () => void, allowPropagation: () => void, isCanceled: boolean, isPropagationAllowed: boolean }.
reason explains why the change happened (IDE autocomplete after reason === ' shows the possible values for that component) — branch logic on it instead of guessing from the DOM event.cancel() stops the component's internal state from changing — lets you veto a change while keeping the component uncontrolled, as an alternative to full external control.allowPropagation() overrides the default propagation-stopping behavior (e.g. most components stop Esc from closing parent popups too) so the DOM event bubbles further.event.preventBaseUIHandler(): escape hatch to stop Base UI's own default handling of a React event (e.g. onPaste) when no dedicated customization prop exists yet. Has no effect on components that listen to native events instead of React synthetic events.open, value, ...) plus its setter to the matching change handler (onOpenChange, onValueChange, ...) to control externally — e.g. opening a Dialog on a timeout with no trigger at all./* Cancel a state change based on reason */
<Tooltip.Root onOpenChange={(open, eventDetails) => {
if (eventDetails.reason === 'trigger-press') {
eventDetails.cancel();
}
}}>...</Tooltip.Root>
/* Allow Esc to propagate to a parent popup instead of being swallowed */
<Tooltip.Root onOpenChange={(open, eventDetails) => {
if (eventDetails.reason === 'escape-key') {
eventDetails.allowPropagation();
}
}}>...</Tooltip.Root>
/* Fully controlled Dialog, opened without a Trigger */
const [open, setOpen] = React.useState(false);
React.useEffect(() => {
const t = setTimeout(() => setOpen(true), 1000);
return () => clearTimeout(t);
}, []);
<Dialog.Root open={open} onOpenChange={setOpen}>No trigger needed.</Dialog.Root>
cancel()/allowPropagation() let you intercept specific interaction reasons surgically, instead of reimplementing the whole open/close state machine externally just to block one case (e.g. "don't close on trigger press, but do close on Esc").eventDetails.cancel() over converting a component to fully controlled state when you only need to veto one specific interaction reason — it's less code and keeps the component's own state machine authoritative.reason before assuming which interaction caused a change — the same onOpenChange fires for trigger clicks, outside clicks, Esc, and programmatic changes alike.preventBaseUIHandler() only works on React synthetic events; if a component uses native DOM listeners internally, this method won't intercept them — check the component's own chapter for its actual event-handling mechanism.eventDetails patterns for common customizations like escape-key handling and outside-press behavior.eventDetails convention.