Skip to content
ShineUI
Install

SelectButton

A track of near equals.

<sui-select-button>SelectionStable

About

Segmented control in a sunken track. Single mode is exclusive, like radios; multiple lets several options stay pressed and marks each with an orange check. The track hugs its options and never stretches to fill a column. Use it for two to five short choices such as a report view. Form-associated: single posts name=value, multiple one entry per pick.

Usage

<sui-select-button label="Report view" name="view" value="Summary"></sui-select-button> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-select-button"); el.options = ["Summary", "Detailed", "Comparative"]; /* SuiSelectButtonOptionInput[] */ el.addEventListener("suiChange", (e) => console.log(e.detail)); </script>
import { SuiSelectButton } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { const options = ["Summary", "Detailed", "Comparative"]; /* SuiSelectButtonOptionInput[] */ return <SuiSelectButton label="Report view" name="view" value="Summary" options={options} onSuiChange={(e) => console.log(e.detail)} />; }
<script setup> import { SuiSelectButton } from "@shineui/vue"; import "@shineui/core/shineui.css"; const options = ["Summary", "Detailed", "Comparative"]; // SuiSelectButtonOptionInput[] const onChange = (e) => console.log(e.detail); </script> <template> <SuiSelectButton label="Report view" name="view" value="Summary" :options="options" @sui-change="onChange" /> </template>
import { Component } from "@angular/core"; import { SuiSelectButton } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiSelectButton], template: `<sui-select-button label="Report view" name="view" value="Summary" [options]="options" (suiChange)="onChange($event)"></sui-select-button>`, }) export class Example { options = ["Summary", "Detailed", "Comparative"]; // SuiSelectButtonOptionInput[] onChange(e: CustomEvent) { console.log(e.detail); } }

Basic

Single mode is a role="radiogroup" of aria-checked radios with one tab stop. Moving the focus moves the selection, the way native radios do.

<sui-select-button id="sbn-basic" label="Report view" name="view" value="Summary"></sui-select-button>
// setup(root): properties and events setup(root) { root.querySelector('#sbn-basic').options = views; }

Multiple

multiple gives a role="group" of aria-pressed buttons, and a pick draws an orange check.

<sui-select-button id="sbn-multi" label="Delivery channels" name="channel" multiple></sui-select-button>
// setup(root): properties and events setup(root) { const el = root.querySelector('#sbn-multi'); el.options = channels; el.value = ['Email', 'Client portal']; }

Sizes, shape and icons

md is a 30px option and sm is 26px. The track takes the 8px control radius, like every button, and pill gives the round look. The track hugs its options and wraps when cramped, and a disabled option stays in the track but the keyboard skips it.

<div style="display:flex;flex-direction:column;gap:14px;align-items:flex-start"> <sui-select-button id="sbn-md" label="Period" value="Quarter"></sui-select-button> <sui-select-button id="sbn-sm" label="Period, small" size="sm" value="Quarter"></sui-select-button> <sui-select-button id="sbn-round" label="Period, round" pill value="Quarter"></sui-select-button> <sui-select-button id="sbn-icons" label="Layout" value="table"></sui-select-button> </div>
// setup(root): properties and events setup(root) { ['#sbn-md', '#sbn-sm', '#sbn-round'].forEach((id) => { root.querySelector(id).options = periods; }); root.querySelector('#sbn-icons').options = layouts; }

As a view switch

An href per option makes each segment a real link, so a middle or a modified click opens its page in a new tab. The track becomes a navigation with aria-current on the selected segment. A plain click still emits suiChange; this page cancels the click and routes in place, as a single-page host does.

Showing the list.
<div style="display:flex;flex-wrap:wrap;gap:18px;align-items:center"> <sui-select-button id="sbn-views" aria-label="Tasks view" value="list"></sui-select-button> <span id="sbn-views-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Showing the list.</span> </div>
// setup(root): properties and events setup(root) { const el = root.querySelector('#sbn-views'); const log = root.querySelector('#sbn-views-log'); el.options = taskViews; // A plain click on a segment link stays on this page; a middle or a modified click opens a tab. el.addEventListener('click', (e) => { const plain = e.button === 0 && !e.ctrlKey && !e.metaKey && !e.shiftKey && !e.altKey; if (plain && e.target.closest('a[href]')) e.preventDefault(); }); el.addEventListener('suiChange', (e) => { const view = taskViews.find((v) => v.value === e.detail); log.textContent = `Showing the ${view.label.toLowerCase()}. Routed in place to ${view.href}.`; }); }

States

The label and the message line frame the track. An error paints the track in the danger tone and sets aria-invalid, and a disabled track posts nothing and never holds the form.

<div style="display:flex;flex-direction:column;gap:18px;align-items:flex-start"> <sui-select-button id="sbn-help" label="Report view" help="Comparative adds the prior period column."></sui-select-button> <sui-select-button id="sbn-req" label="Report view" required help="Pick the view to run."></sui-select-button> <sui-select-button id="sbn-err" label="Report view" required error="Report view is required."></sui-select-button> <sui-select-button id="sbn-off" label="Report view" value="Summary" disabled help="Locked by the saved report."></sui-select-button> </div>
// setup(root): properties and events setup(root) { ['#sbn-help', '#sbn-req', '#sbn-err', '#sbn-off'].forEach((id) => { root.querySelector(id).options = views; }); }

Read-only and invalid

readonly keeps the options named and in the tab order and refuses a pick while the track still posts. invalid paints the track in the danger tone without a message.

<div style="display:grid;gap:18px"> <sui-select-button id="sb-ro" label="Report" name="report-ro" value="Detailed" readonly></sui-select-button> <sui-select-button id="sb-invalid" label="Report" invalid></sui-select-button> </div>
// setup(root): properties and events setup(root) { root.querySelector('#sb-ro').options = views; root.querySelector('#sb-invalid').options = views; }

In a form

Single mode posts one entry under the host name and multiple posts one entry per picked value. An unpicked single track posts an empty string rather than dropping the key.

Submit to see what posts.
<form id="sbn-form" style="display:grid;gap:18px;justify-items:start;max-width:460px"> <sui-select-button id="sbn-form-view" label="Report view" name="view" required></sui-select-button> <sui-select-button id="sbn-form-channels" label="Delivery channels" name="channel" multiple></sui-select-button> <sui-button type="submit" variant="brand" label="Run report"></sui-button> <output id="sbn-out" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Submit to see what posts.</output> </form>
// setup(root): properties and events setup(root) { const form = root.querySelector('#sbn-form'); const out = root.querySelector('#sbn-out'); const view = root.querySelector('#sbn-form-view'); view.options = views; view.value = 'Summary'; const ch = root.querySelector('#sbn-form-channels'); ch.options = channels; ch.value = ['Email']; form.addEventListener('submit', (e) => { e.preventDefault(); const entries = [...new FormData(form)].map(([k, v]) => `${k}=${v}`); out.textContent = entries.length ? entries.join(' ') : 'Nothing posted.'; }); }

Events

suiChange carries the value: a string in single mode and the full array when multiple. Arrowing across a single track emits on every step, because the selection follows the focus.

Idle.
<div style="display:flex;flex-wrap:wrap;gap:18px;align-items:center"> <sui-select-button id="sbn-ev" label="Period" name="sbn-period-ev"></sui-select-button> <span id="sbn-ev-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 el = root.querySelector('#sbn-ev'); const log = root.querySelector('#sbn-ev-log'); el.options = periods; el.addEventListener('suiChange', (e) => { log.textContent = `Period is ${e.detail}.`; }); }

Keyboard

KeyAction
TabOne tab stop: the selected option in single mode, else the first enabled option.
ArrowRight ArrowDownFocuses the next enabled option, wrapping to the first; in single mode also selects it. With linked options it moves focus only. Under dir="rtl" the pair swaps, so the arrow that points along the reading direction is always the forward one.
ArrowLeft ArrowUpFocuses the previous enabled option, wrapping to the last; in single mode also selects it. With linked options it moves focus only.
Home EndFocuses the first or last enabled option; in single mode also selects it. With linked options it moves focus only.
Space EnterPresses the focused option: picks it, or toggles it with multiple. On a linked option, follows the link.

API

<sui-select-button>

Properties

PropertyAttributeTypeDefaultDescription
disableddisabledbooleanfalseDisables the track. A disabled track posts nothing and never holds the form.
errorerrorstringstring | undefinedError message; paints the track in the danger tone and sets aria-invalid.
helphelpstringstring | undefinedHelp text under the track. Replaced by error when set.
invalidinvalidbooleanfalseDanger border on the track without a message, for a form that draws its summary elsewhere.
labellabelstringstring | undefinedLabel above the track; also names the radiogroup.
multiplemultiplebooleanfalseNon-exclusive picks with orange check glyphs: a role="group" of aria-pressed buttons. Single mode is a role="radiogroup" of aria-checked radios.
namenamestringstring | undefinedForm control name. Reflected: the form entry is built from the content attribute.
optionsproperty onlySuiSelectButtonOptionInput[][]Options: strings or numbers, or { label, value, icon, disabled, href } objects. A disabled option stays in the track but the keyboard skips it. An option with href is a real link, so the track is a navigation rather than a radio group. Property, not attribute.
pillpillbooleanfalsePill track and options. Off by default: the track and its options take the control radius, so a view switch beside buttons and selects reads as one row of shapes.
readOnlyreadonlybooleanfalseDisplay only: the options keep their names and their tab stop but a click, Space or an arrow key changes nothing, and the track still posts. Neither a read-only nor a disabled track holds the form on valueMissing.
requiredrequiredbooleanfalseA selection is required for the surrounding form to be valid.
sizesize"md" | "sm"'md'Option height: md 30px (default), sm 26px.
valuevalueSuiChoiceValueSuiKey[] | number | string | undefinedThe picked option's value, or the array of picked values when multiple, each as its option gives it (a number stays a number). Written as an attribute it is text, and it still picks the option whose value reads the same. Mutable; written before suiChange fires.

Events

EventDetailReactVueDescription
suiChangeSuiChoiceValueSuiKey[] | number | stringonSuiChange@sui-changeFires when the selection changes; detail is the new value: the picked option's value, or the array of picked values when multiple. On a linked segment it fires before the browser follows the link; a host that routes itself cancels the segment's click instead.

Methods

MethodSignatureDescription
setFocus()setFocus() => Promise<void>Moves keyboard focus to the segment that holds the tab stop, as el.focus() does. sui-form calls this to reach the first invalid control.

Types

SuiChoiceValue typeWhat a choice element holds: one option's value, or, where it takes several (multiple), the array of the picked values. A value keeps the type its option gave it, so a numeric id comes back a number through value, the events and a framework binding; only the form posts text.
No members.
SuiKey typeA record's or an item's key wherever the library identifies one: a row, a tree node, a list item, a selected value. Numbers stay numbers, so a numeric id round-trips through selection, the events and a framework binding without turning into a string.
No members.
SuiSelectButtonOption interfaceOne segment.
labelstringSegment text.
valueSuiKeyWhat value holds, as given: a number stays a number through value, suiChange and a framework binding. The form posts its text.
icon?stringFont Awesome class drawn before the label.
disabled?booleanKeeps the segment in the track but greys it out; the keyboard skips it.
href?stringMakes the segment a real link, for a view switch that is also a route: a middle or a modified click opens it in a new tab, and a plain click still picks it and emits suiChange. A host that routes itself calls preventDefault() on that click, as on any link it takes over, to keep the browser from following. With any linked segment the track is a navigation with aria-current on the selected one, not a radio group, and the arrows move focus only. Ignored with multiple, since a toggle is not a link.
SuiSelectButtonOptionInput typeAn option: a string or a number stands for itself as label and value.
No members.

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.

See also