Customization
A guide to customizing the behavior of Base UI components.
View as MarkdownBase UI events
Change events such as onOpenChange
, onValueChange
, and onPressedChange
are custom to Base UI.
They can be emitted by various different DOM events, effects, or even during rendering.
onOpenChange: (open, eventDetails) => void
onValueChange: (value, eventDetails) => void
onPressedChange: (pressed, eventDetails) => void
The eventDetails
property is passed as a second argument to Base UI event handlers.
This enables the event to be customized, and also allows you to conditionally run side effects based on the reason or DOM event that caused the change.
interface BaseUIChangeEventDetails {
reason: string;
event: Event;
cancel: () => void;
allowPropagation: () => void;
isCanceled: boolean;
isPropagationAllowed: boolean;
}
reason
is used to determine why the change event occurred, which can be useful to conditionally run certain side effects. Most IDEs will show the possible string values after typingreason === '
.event
is the native DOM event that caused the change.cancel
stops the component from changing its internal state.allowPropagation
allows the DOM event to propagate in cases where Base UI will stop the propagation.isCanceled
indicates whether the change event has been canceled.isPropagationAllowed
indicates whether the DOM event is allowed to propagate.
Canceling a Base UI event
An event can be canceled with the cancel()
method on eventDetails
:
<Tooltip.Root
onOpenChange={(open, eventDetails) => {
if (eventDetails.reason === 'trigger-press') {
// Stop the open change (false) from happening.
eventDetails.cancel();
}
}}
>
...
</Tooltip.Root>
This lets you leave the component uncontrolled as its internal state will be prevented from updating. This is an alternative to controlling the component with external state and guarding the state updates conditionally.
Allowing propagation of the DOM event
In most components, pressing the Esc key stops the propagation of the event so parent popups don’t close simultaneously.
This can also be customized with the allowPropagation()
method:
<Tooltip.Root
onOpenChange={(open, eventDetails) => {
if (eventDetails.reason === 'escape-key') {
// Allow the DOM event to propagate.
eventDetails.allowPropagation();
}
}}
>
...
</Tooltip.Root>
Preventing Base UI from handling a React event
To prevent Base UI from handling a React event like onClick
, you can use the preventBaseUIHandler()
method on the event object:
<NumberField.Input
onPaste={(event) => {
event.preventBaseUIHandler();
}}
/>
This should be used as an escape hatch in cases where there isn’t a prop yet to customize the behavior. In various cases, native events are used instead of React events, so this method will not have an effect.
Controlling components with state
Change event handlers enable a component to be controlled with your own external state.
Components are uncontrolled by default, meaning that they will manage their own state internally.
<Dialog.Root>
<Dialog.Trigger /> {/* Opens the dialog when clicked. */}
</Dialog.Root>
A component can be made controlled by passing external state to a prop, such as open
or value
, and the state’s setter to its corresponding change handler, such as onOpenChange
or onValueChange
.
For instance, you can open Dialog after a timeout, without a trigger:
const [open, setOpen] = React.useState(false);
React.useEffect(() => {
const timeout = setTimeout(() => {
setOpen(true);
}, 1000);
return () => clearTimeout(timeout);
}, []);
return (
<Dialog.Root open={open} onOpenChange={setOpen}>
No trigger is needed in this case.
</Dialog.Root>
);
This also allows you to read the state of the component outside of the root component.