Vize

Split Button

Accessible, unstyled SplitButton: a primary action paired with a menu button (WAI-ARIA APG) that opens related actions. Items, groups, radio/checkbox items, and submenus are the shared Menu parts re-exported under SplitButton names.

Package @vizejs/ui/split-button
Maturity stable
Own the source vize lib pull split-button
Requires collection, context, controllable-state, direction, dismissable-layer, focus-scope, id, inert-outside, menu, pointer-grace, portal, positioner, presence, scroll-lock, typeahead
Aliases split button, menu button with default action, dropdown button, combo button
Covers WAI-ARIA menu button pattern, PrimeVue SplitButton, Fluent UI SplitButton, Carbon Combo Button

Minimal setup

Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).

vp install @vizejs/ui

Add the public import below to <script setup lang="ts">. Components provide behavior without styles; the preview uses optional Paper styles.

import { SplitButton, SplitButtonRoot, SplitButtonAction, SplitButtonTrigger } from "@vizejs/ui/split-button";

Or copy the source into your project with vize lib pull split-button (see Source Distribution).

Try it

This is the basic example maintained with the component source, using the public package imports. The preview adds the optional Paper styles.

Open the interactive example · Styling these components

Copy the example

<!-- Save action with related save options in an attached menu. -->
<script setup lang="ts">
import { ref } from "vue";

import {
  SplitButton,
  SplitButtonAction,
  SplitButtonContent,
  SplitButtonItem,
  SplitButtonSeparator,
  SplitButtonTrigger,
} from "@vizejs/ui/split-button";

const lastAction = ref("none");
</script>

<template>
  <div>
    <SplitButton aria-label="Save options">
      <SplitButtonAction @press="() => (lastAction = 'Save')">Save</SplitButtonAction>
      <SplitButtonTrigger aria-label="More save options">▾</SplitButtonTrigger>
      <SplitButtonContent>
        <SplitButtonItem @select="() => (lastAction = 'Save as draft')">
          Save as draft
        </SplitButtonItem>
        <SplitButtonItem @select="() => (lastAction = 'Save and publish')">
          Save and publish
        </SplitButtonItem>
        <SplitButtonSeparator />
        <SplitButtonItem @select="() => (lastAction = 'Schedule')">Schedule…</SplitButtonItem>
      </SplitButtonContent>
    </SplitButton>
    <output>Last action: {{ lastAction }}</output>
  </div>
</template>

API

SplitButton

Source: src/families/actions/split-button/split-button-root.vue

Props

Prop Type Default Description
id string | null undefined Consumer-owned base id. null and undefined select a deterministic fallback.
open boolean undefined Controlled open state of the menu. undefined selects uncontrolled behavior.
defaultOpen boolean false Initial open state for uncontrolled use.
modal boolean true Make outside content inert and lock document scroll while the menu is open.
dir MenuDirection undefined Reading direction: flips submenu arrow keys and submenu placement. undefined inherits DirectionProvider/LocaleProvider, then "ltr".
loop boolean false Wrap arrow-key navigation from the last item to the first and back.
disabled boolean false Disable the primary action and the trigger, and close the menu when it becomes disabled.
anchor SplitButtonAnchor "group" Element the menu is positioned against: the whole group or only the trigger.
ariaLabel string undefined Accessible name of the group when no aria-labelledby supplies one.
ariaLabelledby string undefined Space-separated ids that label the group.
ariaDescribedby string undefined Space-separated ids that describe the group.

Events

Event Payload Description
update:open [value: boolean] Fired with the requested open value (supports v-model:open).
openChange [value: boolean, previous: boolean, nativeEvent: Event | null] Fired after any distinct open-state request with the previous value and native event.

Slots

Slot Slot props Description
default SplitButtonSlotState Primary action, trigger, and content. Receives the menu open state and availability.

SplitButtonRoot

Source: src/families/actions/split-button/split-button-root.vue

Props

Prop Type Default Description
id string | null undefined Consumer-owned base id. null and undefined select a deterministic fallback.
open boolean undefined Controlled open state of the menu. undefined selects uncontrolled behavior.
defaultOpen boolean false Initial open state for uncontrolled use.
modal boolean true Make outside content inert and lock document scroll while the menu is open.
dir MenuDirection undefined Reading direction: flips submenu arrow keys and submenu placement. undefined inherits DirectionProvider/LocaleProvider, then "ltr".
loop boolean false Wrap arrow-key navigation from the last item to the first and back.
disabled boolean false Disable the primary action and the trigger, and close the menu when it becomes disabled.
anchor SplitButtonAnchor "group" Element the menu is positioned against: the whole group or only the trigger.
ariaLabel string undefined Accessible name of the group when no aria-labelledby supplies one.
ariaLabelledby string undefined Space-separated ids that label the group.
ariaDescribedby string undefined Space-separated ids that describe the group.

Events

Event Payload Description
update:open [value: boolean] Fired with the requested open value (supports v-model:open).
openChange [value: boolean, previous: boolean, nativeEvent: Event | null] Fired after any distinct open-state request with the previous value and native event.

Slots

Slot Slot props Description
default SplitButtonSlotState Primary action, trigger, and content. Receives the menu open state and availability.

SplitButtonAction

Source: src/families/actions/split-button/split-button-action.vue

Props

Prop Type Default Description
type "button" | "reset" | "submit" "button" Native button submission behavior.
disabled boolean false Disable the primary action while the trigger stays available.
ariaLabel string undefined Accessible name when no visible label supplies one.

Events

Event Payload Description
press [nativeEvent: MouseEvent] Fired after user activation reaches the enabled primary action.
keydown [nativeEvent: KeyboardEvent] Fired before keyboard handling; call preventDefault() to skip the built-in keys.

Slots

Slot Slot props Description
default SplitButtonSlotState Primary action contents. Receives the menu open state and availability.

Exposed

Member Type Description
element HTMLButtonElement | null Rendered native button.
focus (options?: FocusOptions) => void Move focus to the primary action.

SplitButtonTrigger

Source: src/families/actions/split-button/split-button-trigger.vue

Props

Prop Type Default Description
disabled boolean false Remove the trigger from activation and sequential keyboard focus.
ariaLabel string undefined Accessible name of the menu button. Required when the trigger renders only an icon.
openOn "click" | "pointerdown" "pointerdown" Primary mouse button event that toggles the menu. pointerdown opens on press (native menu feel, press-drag-release selection); click waits for release.

Events

Event Payload Description
pointerdown [nativeEvent: PointerEvent] Fired before a primary-button pointer-down toggles the menu; preventable.
keydown [nativeEvent: KeyboardEvent] Fired before keyboard handling; call preventDefault() to skip the built-in keys.

Slots

Slot Slot props Description
default SplitButtonSlotState Trigger contents. Receives the menu open state and availability.

Exposed

Member Type Description
element HTMLButtonElement | null Rendered native button.
focus (options?: FocusOptions) => void Move focus to the trigger.

Behavior

@vizejs/ui/split-button pairs a primary action with a WAI-ARIA APG menu button inside one named role="group". It is built on the shared menu core (see ../../menus/menu/menu.behavior.md): split-button-root.vue publishes data-menu-kind="split-button", split-button-action.vue is the primary native button, and split-button-trigger.vue is the menu button. Items, groups, submenus, and content are the Menu SFCs re-exported under SplitButton* names. Every row is proven by the named test in split-button.test.ts or split-button-ssr.test.ts.

# State Input Outcome Proven by
S1 closed render; press the primary action a named role="group" holds the action then the trigger; only the trigger carries aria-haspopup="menu"; pressing the action emits press and never opens the menu renders a named group with a primary action and a menu button
S2 closed / open primary mouse pointer-down on the trigger toggles; opening prevents trigger focus and focuses the content; aria-expanded, aria-controls, and every part's data-state follow mouse pointer-down on the trigger toggles the menu and focuses the content
S3 closed secondary button, ctrl-press, touch tap, click secondary and ctrl presses are ignored; touch taps and openOn="click" toggle on click secondary buttons and ctrl-clicks do not toggle; touch taps and openOn click toggle on click
S4 closed Enter / Space / ArrowDown / ArrowUp on trigger opens with the first or last item focused; Escape closes and returns focus to the trigger trigger keyboard opens on the first or last item and Escape returns focus to it
S5 closed Alt+ArrowDown on the primary action opens with the first item focused; closing returns focus to the action; plain ArrowDown is not consumed Alt+ArrowDown on the primary action opens the menu and focus returns to the action
S6 closed prevented keydown / pointerdown emits state is unchanged preventable keydown and pointerdown emits keep the menu closed
S7 open select an item, including from a submenu every level closes, focus returns to the trigger, and the primary press does not fire selecting an item closes every level and returns focus to the trigger
S8 open → disabled root disabled becomes true the menu closes; both buttons are natively disabled; the group publishes aria-disabled and data-disabled a disabled root disables both buttons and closes an open menu
S9 part disabled disabled on the action or on the trigger only the other part keeps working the action and the trigger can be disabled independently
S10 open anchor="group" (default) / anchor="trigger" the menu is positioned against the whole group or against the trigger the menu is positioned against the group by default and the trigger on request
S11 SSR two requests + hydrate byte-identical markup with data-menu-kind="split-button" and warning-free hydration renders identical split button markup across SSR requests and hydrates cleanly
S12 SSR closed / disabled render no menu, no aria-controls; a disabled root renders two disabled buttons a closed or disabled split button renders no menu on the server

Public surface

Kind Surface
parts root (group), action (primary button), trigger (menu button); menu parts are documented in the Menu contract
data data-vize-ui (split-button, split-button-action, split-button-trigger), data-state (open / closed), data-disabled
emits root: update:open, openChange; action: press, keydown; trigger: pointerdown, keydown
expose root: the Menu root surface plus disabled and element; action and trigger: element, focus()
naming the group needs aria-label or aria-labelledby; an icon-only trigger needs aria-label because it also names the menu through aria-labelledby