Skip to content

Customization

A guide to customizing the behavior of Base UI Vue components.

Base UI events

Change events such as open-change, value-change, and form-submit are custom to Base UI Vue. They can be emitted by user interaction, effects, or internal state changes depending on the component.

ts
interface BaseUIEventHandlers {
  'open-change': (open: boolean, eventDetails: unknown) => void
  'value-change': (value: unknown, eventDetails: unknown) => void
  'form-submit': (formValues: Record<string, unknown>, event: Event) => void
}

Many Base UI Vue change handlers receive an eventDetails object as the second argument. This lets you inspect why a change happened and customize the default behavior without forcing every component into controlled mode.

ts
interface BaseUIChangeEventDetails {
  reason: string
  event: Event
  cancel: () => void
  allowPropagation: () => void
  isCanceled: boolean
  isPropagationAllowed: boolean
}
  • reason tells you why the change occurred.
  • event is the DOM event that triggered the change when one exists.
  • cancel() stops the internal state change from happening.
  • allowPropagation() allows the DOM event to bubble in cases where Base UI Vue would normally stop it.
  • isCanceled tells you whether the change was canceled.
  • isPropagationAllowed tells you whether DOM propagation has been re-enabled.

Canceling a Base UI event

An event can be canceled with eventDetails.cancel():

vue
<script setup lang="ts">
import { CollapsibleRoot } from 'base-ui-vue'

function handleOpenChange(open: boolean, eventDetails: { reason: string, cancel: () => void }) {
  if (eventDetails.reason === 'trigger-press' && !open) {
    eventDetails.cancel()
  }
}
</script>

<template>
  <CollapsibleRoot @open-change="handleOpenChange">
    <!-- parts -->
  </CollapsibleRoot>
</template>

This lets you keep a component uncontrolled while still preventing specific internal state transitions.

Allowing propagation of the DOM event

In some components, keyboard events like Escape are stopped so parent layers do not react at the same time. You can allow propagation again with eventDetails.allowPropagation() when a component exposes that detail object.

Preventing Base UI from handling a Vue event

To prevent Base UI Vue from handling a merged listener such as onClick, use event.preventBaseUIHandler():

vue
<script setup lang="ts">
import { mergeProps } from 'base-ui-vue'

const props = mergeProps(
  {
    onClick() {
      console.log('internal behavior')
    },
  },
  {
    onClick(event) {
      event.preventBaseUIHandler()
    },
  },
)
</script>

<template>
  <button v-bind="props">
    Click me
  </button>
</template>

This is an escape hatch for cases where there is not yet a dedicated prop for customizing the behavior.

Controlling components with state

Components are uncontrolled by default, meaning they manage their own state internally.

vue
<script setup lang="ts">
import {
  CollapsiblePanel,
  CollapsibleRoot,
  CollapsibleTrigger,
} from 'base-ui-vue'
</script>

<template>
  <CollapsibleRoot>
    <CollapsibleTrigger>Details</CollapsibleTrigger>
    <CollapsiblePanel>
      Base UI Vue manages this open state internally.
    </CollapsiblePanel>
  </CollapsibleRoot>
</template>

A component becomes controlled when you pass external state to props like open or value, and update that state in the corresponding change handler.

vue
<script setup lang="ts">
import {
  CollapsiblePanel,
  CollapsibleRoot,
  CollapsibleTrigger,
} from 'base-ui-vue'
import { ref } from 'vue'

const open = ref(false)

function handleOpenChange(nextOpen: boolean) {
  open.value = nextOpen
}
</script>

<template>
  <CollapsibleRoot :open="open" @open-change="handleOpenChange">
    <CollapsibleTrigger>Details</CollapsibleTrigger>
    <CollapsiblePanel>
      This open state is controlled externally.
    </CollapsiblePanel>
  </CollapsibleRoot>
</template>