Skip to content
ShineUI
Install

SplitButton

A main verb with close relatives.

<sui-split-button>ButtonsStable

About

Reach for this when one verb has close variants of the same act: Save, Save and new, Save and send. The main half runs the common case; the chevron opens a menu with the rest. Unrelated actions belong in a sui-menu or a toolbar, not here.

Usage

<sui-split-button variant="accent" label="Save"></sui-split-button> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-split-button"); el.items = [ { label: "Save and new", icon: "fa-regular fa-file", shortcut: "Ctrl N" }, { label: "Save and send", icon: "fa-regular fa-paper-plane" }, { separator: true }, { label: "Delete draft", icon: "fa-regular fa-trash-can", danger: true }, ]; /* SuiMenuItem[] */ el.addEventListener("suiPick", (e) => console.log(e.detail)); </script>
import { SuiSplitButton } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { const items = [ { label: "Save and new", icon: "fa-regular fa-file", shortcut: "Ctrl N" }, { label: "Save and send", icon: "fa-regular fa-paper-plane" }, { separator: true }, { label: "Delete draft", icon: "fa-regular fa-trash-can", danger: true }, ]; /* SuiMenuItem[] */ return <SuiSplitButton variant="accent" label="Save" items={items} onSuiPick={(e) => console.log(e.detail)} />; }
<script setup> import { SuiSplitButton } from "@shineui/vue"; import "@shineui/core/shineui.css"; const items = [ { label: "Save and new", icon: "fa-regular fa-file", shortcut: "Ctrl N" }, { label: "Save and send", icon: "fa-regular fa-paper-plane" }, { separator: true }, { label: "Delete draft", icon: "fa-regular fa-trash-can", danger: true }, ]; // SuiMenuItem[] const onChange = (e) => console.log(e.detail); </script> <template> <SuiSplitButton variant="accent" label="Save" :items="items" @sui-pick="onChange" /> </template>
import { Component } from "@angular/core"; import { SuiSplitButton } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiSplitButton], template: `<sui-split-button variant="accent" label="Save" [(open)]="open" [items]="items" (suiPick)="onChange($event)"></sui-split-button>`, }) export class Example { open = false; items = [ { label: "Save and new", icon: "fa-regular fa-file", shortcut: "Ctrl N" }, { label: "Save and send", icon: "fa-regular fa-paper-plane" }, { separator: true }, { label: "Delete draft", icon: "fa-regular fa-trash-can", danger: true }, ]; // SuiMenuItem[] onChange(e: CustomEvent) { console.log(e.detail); } }

Basic

items is a property: the shared menu row shape, with icons, shortcuts, badges, danger rows and separators.

<sui-split-button variant="accent" label="Save"></sui-split-button>
// setup(root): properties and events setup(root) { root.querySelector('sui-split-button').items = saveRows(); }

Open state

The menu opens under the control, lined up with the edge menu-align names. While open it paints in the top layer, so a card, a scroll box or a transformed parent cannot clip or move it. The chevron carries aria-haspopup="menu", aria-expanded and aria-controls.

<div style="min-height:210px"> <sui-split-button variant="accent" label="Save" open></sui-split-button> </div>
// setup(root): properties and events setup(root) { root.querySelector('sui-split-button').items = saveRows(); }

Roles and sizes

variant takes accent, brand and outline, and size the same 28, 34 and 42px heights as the rest of the control family. menu-align picks the edge the menu lines up with.

<div style="display:flex;flex-wrap:wrap;gap:12px;align-items:center"> <sui-split-button id="sb-brand" variant="brand" label="Publish" size="sm"></sui-split-button> <sui-split-button id="sb-outline" variant="outline" label="Export"></sui-split-button> <sui-split-button id="sb-lg" variant="accent" label="Send" size="lg" icon="fa-regular fa-paper-plane" menu-align="right"></sui-split-button> </div>
// setup(root): properties and events setup(root) { root.querySelector('#sb-brand').items = [{ label: 'Publish and notify' }, { label: 'Schedule' }]; root.querySelector('#sb-outline').items = [{ label: 'Export as CSV' }, { label: 'Export as PDF' }]; root.querySelector('#sb-lg').items = [{ label: 'Send and archive' }, { label: 'Send later' }]; }

Loading and disabled

Loading spins on the main half, blocks both halves and closes an open menu. Disabled blocks both halves.

<div style="display:flex;flex-wrap:wrap;gap:12px;align-items:center"> <sui-split-button id="sb-load" variant="accent" label="Save" loading></sui-split-button> <sui-split-button id="sb-off-accent" variant="accent" label="Save" disabled></sui-split-button> <sui-split-button id="sb-off-brand" variant="brand" label="Publish" disabled></sui-split-button> <sui-split-button id="sb-off-outline" variant="outline" label="Export" disabled></sui-split-button> </div>
// setup(root): properties and events setup(root) { root.querySelector('#sb-load').items = saveRows(); root.querySelector('#sb-off-accent').items = saveRows(); root.querySelector('#sb-off-brand').items = [{ label: 'Publish and notify' }]; root.querySelector('#sb-off-outline').items = [{ label: 'Export as CSV' }]; }

A row with href is a real link: suiPick fires and the menu closes on the click, then the browser follows. Keep the verbs in the menu and put the destination on the row that is one.

<div style="min-height:170px"> <sui-split-button id="sb-links" variant="outline" label="Export"></sui-split-button> </div>
// setup(root): properties and events setup(root) { root.querySelector('#sb-links').items = [ { label: 'Export as CSV', icon: 'fa-regular fa-file-lines' }, { label: 'Export as PDF', icon: 'fa-regular fa-file-pdf' }, { separator: true }, { label: 'Export history', icon: 'fa-regular fa-clock', href: '#/exports' }, { label: 'Export formats', icon: 'fa-regular fa-circle-question', href: 'https://example.com/help/exports', target: '_blank' }, ]; }

In a form

Only the main half takes part in the form: type, name, value and form go to it, and the chevron is always type="button", so opening the menu can never submit.

Not submitted.
<form id="sb-form" style="display:flex;flex-wrap:wrap;gap:12px;align-items:center"> <sui-input-text name="title" placeholder="Invoice title" aria-label="Invoice title"></sui-input-text> <sui-split-button id="sb-submit" variant="brand" label="Save" type="submit" name="action" value="save"></sui-split-button> <span id="sb-form-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Not submitted.</span> </form>
// setup(root): properties and events setup(root) { const log = root.querySelector('#sb-form-log'); root.querySelector('#sb-submit').items = [ { label: 'Save as draft', icon: 'fa-regular fa-file' }, { label: 'Save and close', icon: 'fa-regular fa-circle-check' }, ]; root.querySelector('#sb-form').addEventListener('submit', (e) => { e.preventDefault(); const b = e.submitter; log.textContent = b ? `Submitted with ${b.name}=${b.value}.` : 'Submitted.'; }); }

Events

The main half's click is the native host click; the chevron's click never reaches the host. suiOpen fires after the element opens itself, a cancelable suiClose with the reason as detail before it closes itself, and suiPick when a row runs; writing open from outside echoes neither.

Idle.
<div style="display:flex;flex-wrap:wrap;gap:12px;align-items:center"> <sui-split-button id="sb-events" variant="outline" label="Export"></sui-split-button> <span id="sb-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Idle.</span> </div>
// setup(root): properties and events setup(root) { const log = root.querySelector('#sb-log'); const el = root.querySelector('#sb-events'); el.items = [{ label: 'Export as CSV' }, { label: 'Export as PDF' }]; el.addEventListener('suiOpen', () => { log.textContent = 'Menu open.'; }); el.addEventListener('suiClose', (e) => { log.textContent = `Menu closed (${e.detail}).`; }); el.addEventListener('suiPick', (e) => { log.textContent = `Picked ${e.detail.label}.`; }); el.addEventListener('click', () => { log.textContent = 'Main action.'; }); }

Keyboard

KeyAction
Enter SpaceOn the main half, runs the action. On the chevron, opens or closes the menu. On a row, picks it; on a row with href the browser then follows the link.
ArrowDownOn either half, opens the menu and focuses the first row. In the menu, moves to the next row, wrapping past the last.
ArrowUpOn either half, opens the menu and focuses the last row. In the menu, moves to the previous row, wrapping past the first.
Home EndMove to the first or last row. Disabled rows and separators are skipped.
Any letterMoves to the next row whose label starts with that letter.
EscapeCloses the menu and returns focus to the chevron.
TabIn the menu, closes it and moves on to the control after the split button.
Shift+TabIn the menu, closes it and returns focus to the chevron.

API

<sui-split-button>

Properties

PropertyAttributeTypeDefaultDescription
disableddisabledbooleanfalseDisables both halves. Reflected.
formformstringstring | undefinedAssociates the main half with a form by id, for a split button that sits outside the form it submits: a dialog footer, or a sticky action bar.
iconiconstringstring | undefinedFont Awesome class for the main half.
itemsproperty onlySuiMenuItem[][]Menu rows (shared SuiMenuItem shape: label, icon, shortcut, badge, danger, disabled, separator, section, href, target). A row with href is a real link: Enter follows it, and suiPick fires and the menu closes on the click first. The menu is one level, so a row's nested items are not opened and the row says nothing about them. Property, not attribute.
label*labelstringMain verb, e.g. "Save".
loadingloadingbooleanfalseShows a spinner on the main half and blocks both halves. Closes the menu if it is open. Reflected.
menuAlignmenu-align"end" | "left" | "right" | "start"'start'Which edge of the control the menu lines up with. start and end follow the reading direction, and start is the default, so the menu opens from the control's leading edge in either direction; left and right name sides of the screen, as Ant's placement does.
menuWidthmenu-widthnumber200Width of the menu panel in px.
namenamestringstring | undefinedSubmitted name of the main half. Pair it with value, or the entry posts empty.
openopenbooleanfalseOpens the menu. Mutable: the element closes itself on pick, outside pointer, Tab and Escape, announcing each with a cancelable suiClose, so plain HTML works with no listener at all. It is what you asked for, never rewritten behind your back: a disabled or loading control draws no menu whatever open says, and the menu comes back when the control settles. An open menu paints in the browser's top layer, so a transformed, contained or clipping ancestor cannot move it or cut it off.
sizesize"lg" | "md" | "sm"'md'Control height: sm 28px, md 34px, lg 42px. Reflected.
typetype"button" | "reset" | "submit"'button'Native button type of the main half. submit participates in the surrounding form. Only the main half takes part in a form: type, name, value and form go to it, and the chevron is always type="button", so opening the menu can never submit.
valuevaluestringstring | undefinedValue submitted with the form when the main half submits it.
variantvariant"accent" | "brand" | "outline"'accent'Visual role of both halves. accent = the one primary action per view.

Events

EventDetailReactVueDescription
suiCloseSuiCloseReason"cancel" | "close-button" | "escape" | "outside" | "pick" | "scrim" | "trigger"onSuiClose@sui-closeFires before the element closes the menu itself; detail is why: 'pick' (a row ran), 'trigger' (the chevron was pressed again), 'outside' (a pointer landed elsewhere, or Tab left the menu: focus goes back to the chevron, and Tab carries on to whatever follows the control while Shift+Tab stops on the chevron), 'escape', or 'cancel' (the control went disabled or loading under an open menu). Cancelable: preventDefault() keeps it open, so a parent that binds open never goes stale and "a stray click outside must not close this" needs no prop of its own. On 'cancel' a veto means the menu is hidden while the control is inert and comes back when it settles, because menuOpen and not open alone decides what renders. It does not bubble.
suiOpenvoidonSuiOpen@sui-openFires after the element opens the menu itself (a chevron press, or ArrowDown/ArrowUp on either half). An external open write does not echo it. It does not bubble.
suiPickSuiMenuItemonSuiPick@sui-pickFires when a menu row is picked; detail is the item. item.command() also runs. The main half's click is the native host click; the chevron's click never reaches the host, so a click listener means the primary action ran and nothing else.

Methods

MethodSignatureDescription
setFocus()setFocus() => Promise<void>Moves keyboard focus to the main button.

Types

SuiCloseReason typeWhy a dismissible surface is closing. Detail of the cancelable suiClose. A consumer expresses "a stray backdrop click must not close this" by calling preventDefault() on the reason it does not accept, which is why none of these needs a prop of its own.
No members.
SuiMenuItem interface
label?string
icon?string
shortcut?string
badge?string | number
danger?boolean
disabled?boolean
separator?boolean
section?stringSection heading row (uppercase eyebrow).
items?SuiMenuItem[]Nested items (TieredMenu, Menubar dropdowns).
command?() => void
href?stringRenders the row as a real link, so middle-click, copy link and open in a new tab work and Enter follows it. The row keeps its styling, its place in the arrow-key order and its pick: command runs and suiPick fires on the click, then the browser navigates. Ignored on a disabled row and on a row with nested items; those stay buttons.
target?stringLink target for a row with href, e.g. _blank. A _blank row gets rel="noopener".
current?booleanThe row is the page the reader is on: it carries aria-current="page" and the bold face sui-side-nav gives its current item. For a navigation menu, the site pages behind a phone menu button; an action menu has no current row.
checked?booleanMakes the row one choice of a set, for a mode or an order picked from a menu: the chosen row draws a check and every row keeps room for one, and the rows read as menuitemradio with aria-checked. Leave it unset on an action row.

Theming

Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.

Every name above is listed with its value and its role in Foundations.

See also