SplitButton

A primary action paired with a menu button for related actions.

Usage

SplitButton pairs one primary action with a separate button that opens related actions. Use it when one action is the common choice and the other actions are closely related alternatives. The primary button runs independently from the menu button. The examples use report export and project sharing to show practical actions, then show primary-content choices and disabled states. The size, visual variant, and menu-width comparisons keep every other component input constant.

Import SplitButton from @udixio/ui-react, @udixio/ui-angular, or @udixio/ui-svelte. The three adapters share the same actions data model.

Export a report

Download PDF is the common action. The menu offers CSV, Excel, and a shareable report link. The live status shows which action was requested; replace these callbacks with the application’s export and clipboard logic.

Choose an export format

Primary-button content

The Material 3 specs allow the leading button to contain an icon, label text, or both. The guidelines recommend a brief one- or two-word label with a matching icon. These three examples hold the action, size, and variant constant while comparing icon plus label, label only, and icon only. The icon-only case supplies accessibleLabel.

Icon and label
Label only
Icon only, accessible name “Download PDF”

Choose an export format

Compact project sharing

An icon-only Share action fits a compact project toolbar. Its accessible name is Share; the menu keeps the related actions visible and named. This uses the spec-supported icon-only form for a space-constrained toolbar; where space permits, prefer the brief visible label and matching icon recommended by the guidelines.

Project sharing

Open the sharing dialog or choose a related action.

Sizes

All five sizes use the same PDF-export action, filled variant, and default menu-button width. Only size changes. The default menu width scales with the SplitButton size.

xSmall
small
medium
large
xLarge

Visual variants

The same medium PDF-export action is shown with each visual variant. Only variant changes; size, content, menu label, and actions stay fixed.

elevated
filled
tonal
outlined

The trailing IconButton supports narrow, default, and wide widths through menuButtonProps.width. The same medium, filled report-download action is used for each width; only menuButtonProps.width changes.

narrow
default
wide

Disabled SplitButton

When no report is selected, disable the whole control. This blocks the primary action, menu trigger, and every related export action.

Choose a report to enable export.

One unavailable menu action

Here, workspace policy disables public-link sharing while email invites and access management remain available. Set disabled on that action record only.

Public links are disabled by this workspace’s administrator.

Choose how to share the project

Anatomy and variants

Material 3 defines two adjacent buttons: the leading button runs the primary action and can show a label, an icon, or both; the trailing button always shows the menu icon and opens related options. This implementation composes the shared Button and IconButton primitives and supports the five sizes xSmall, small, medium, large, and xLarge, plus the elevated, filled, tonal, and outlined visual treatments. The default is small and filled. menuButtonProps forwards supported IconButton options such as width, while SplitButton supplies and owns the trailing menu icon and accessible label. primaryButtonProps forwards additional Button options to the leading part.

Actions and state

actions is a non-empty array of { id, label, icon?, href?, disabled? } records. Each action needs a unique, non-empty id and visible label. onPrimaryAction runs the main action. Selecting a menu item emits onActionSelect(action, index) in React and Svelte, or actionSelect with { action, index } in Angular; the menu then closes. Action links keep native link semantics.

Use open with onOpenChange in React, open with openChange in Angular, or bind:open in Svelte for controlled visibility. defaultOpen initializes uncontrolled state. The visible primary label names the primary button; an icon-only primary action also needs accessibleLabel. menuLabel is required and names both the trailing menu button and the popup.

Accessibility and motion

Both parts are native buttons with their own keyboard focus. The trailing button exposes aria-haspopup="menu", aria-expanded, and aria-controls. Enter and Space open the menu and focus its first available action. Arrow Down opens at the first action and Arrow Up at the last; inside the menu, the shared Menu implementation handles arrow navigation, Home, End, and type-ahead. Escape and outside press close the menu; Escape restores focus to its trigger. Each part retains a 48px minimum touch target.

Each button morphs only its own inner corners when that button is hovered, focused, or pressed. Opening the menu turns the trailing chevron and keeps only the trailing button in its selected state: its inner corners stay expanded and its state layer shows the selected overlay. The primary button keeps its own shape. The shared controller honors prefers-reduced-motion by applying the final state immediately. The popup aligns to the inline end of the split button and uses the Menu component’s scrollable surface.

Design decisions and limits

ClassificationDecision
Material 3 guidanceTwo independent button parts; the trailing part always shows a menu icon and opens related actions; leading content may be a label, icon, or both; guidelines recommend a brief label with a matching icon; five sizes and four visual styles.
WAI-ARIA guidanceUse a native menu button with aria-haspopup="menu", aria-expanded, a labelled menu, and the menu-button keyboard pattern.
Udixio API choiceUse a shared action-record array, following FabMenu; use menuLabel for the trigger and popup; control visibility with open/defaultOpen.
UnsupportedText treatment, nested menus, and rich framework-specific menu content are not included.

The primary action is a callback, so use a separate Link for primary navigation. The popup is positioned relative to the component and does not portal or flip based on viewport space; keep it out of clipping containers.

Sources