Skip to content
ShineUI
Install

ToggleButton

A button that remembers.

<sui-toggle-button>SelectionStable

About

Two-state press button: outline when off, navy when on, each state with its own label and optional icon, such as "Voided hidden" and "Voided shown". Use it for a view option the user flips in place. Clicks stay native and the state change is suiChange. Form-associated: posts name=value while on.

Usage

<sui-toggle-button label-off="Voided hidden" label-on="Voided shown" icon-off="fa-regular fa-eye-slash" icon-on="fa-regular fa-eye" name="show-voided"></sui-toggle-button> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-toggle-button"); el.addEventListener("suiChange", (e) => console.log(e.detail)); </script>
import { SuiToggleButton } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { return <SuiToggleButton labelOff="Voided hidden" labelOn="Voided shown" iconOff="fa-regular fa-eye-slash" iconOn="fa-regular fa-eye" name="show-voided" onSuiChange={(e) => console.log(e.detail)} />; }
<script setup> import { SuiToggleButton } from "@shineui/vue"; import "@shineui/core/shineui.css"; const onChange = (e) => console.log(e.detail); </script> <template> <SuiToggleButton label-off="Voided hidden" label-on="Voided shown" icon-off="fa-regular fa-eye-slash" icon-on="fa-regular fa-eye" name="show-voided" @sui-change="onChange" /> </template>
import { Component } from "@angular/core"; import { SuiToggleButton } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiToggleButton], template: `<sui-toggle-button label-off="Voided hidden" label-on="Voided shown" icon-off="fa-regular fa-eye-slash" icon-on="fa-regular fa-eye" name="show-voided" (suiChange)="onChange($event)"></sui-toggle-button>`, }) export class Example { onChange(e: CustomEvent) { console.log(e.detail); } }

Basic

The control is a real button with aria-pressed, so Space and Enter toggle it and a screen reader announces the pressed state. Each state has its own label and icon.

<sui-toggle-button label-off="Voided hidden" label-on="Voided shown" icon-off="fa-regular fa-eye-slash" icon-on="fa-regular fa-eye" name="show-voided" value="yes"></sui-toggle-button>

Sizes

sm is 28px, md is 34px and lg is 42px, the same control heights the input family uses.

<div style="display:flex;flex-wrap:wrap;gap:12px;align-items:center"> <sui-toggle-button size="sm" label-off="Filters" label-on="Filters on" icon-off="fa-solid fa-filter" icon-on="fa-solid fa-filter"></sui-toggle-button> <sui-toggle-button size="md" label-off="Filters" label-on="Filters on" icon-off="fa-solid fa-filter" icon-on="fa-solid fa-filter"></sui-toggle-button> <sui-toggle-button size="lg" label-off="Filters" label-on="Filters on" icon-off="fa-solid fa-filter" icon-on="fa-solid fa-filter"></sui-toggle-button> </div>

Pressed and disabled

Outline when off, navy when on. A disabled button keeps its current label and is barred from submission.

<div style="display:flex;flex-wrap:wrap;gap:12px;align-items:center"> <sui-toggle-button label-off="Voided hidden" label-on="Voided shown" icon-off="fa-regular fa-eye-slash" icon-on="fa-regular fa-eye" checked></sui-toggle-button> <sui-toggle-button label-off="Voided hidden" label-on="Voided shown" icon-off="fa-regular fa-eye-slash" icon-on="fa-regular fa-eye"></sui-toggle-button> <sui-toggle-button label-off="Reconciled only" label-on="Reconciled only" disabled checked></sui-toggle-button> <sui-toggle-button label-off="Reconciled only" label-on="Reconciled only" disabled></sui-toggle-button> </div>

States

required means the button must be on for the form to be valid, and the browser names the field from the on-state label. There is no aria-required on it, because the button role does not support that attribute.

<div style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;align-items:start;max-width:520px"> <sui-toggle-button label-off="Not reviewed" label-on="Reviewed" required help="Mark the reconciliation reviewed before closing."></sui-toggle-button> <sui-toggle-button label-off="Not reviewed" label-on="Reviewed" required error="Mark the reconciliation reviewed."></sui-toggle-button> </div>

Read-only

readonly keeps the button named, in the tab order and pressed, a click changes nothing, and it still posts.

<div style="display:flex;gap:12px;flex-wrap:wrap"> <sui-toggle-button label-on="Following" label-off="Follow" checked readonly></sui-toggle-button> </div>

In a form

An on button posts name=value, where value defaults to "on". An off button posts nothing, the way an unchecked checkbox does.

Submit to see what posts.
<form id="tb-form" style="display:grid;gap:14px;justify-items:start;max-width:420px"> <sui-toggle-button name="voided" value="shown" label-off="Voided hidden" label-on="Voided shown" icon-off="fa-regular fa-eye-slash" icon-on="fa-regular fa-eye" checked></sui-toggle-button> <sui-toggle-button name="reviewed" required label-off="Not reviewed" label-on="Reviewed"></sui-toggle-button> <sui-button type="submit" variant="brand" label="Close the period"></sui-button> <output id="tb-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('#tb-form'); const out = root.querySelector('#tb-out'); 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

The click stays native. suiChange carries the new pressed state, and the element writes checked before it emits.

Showing 24 invoices.
<div style="display:flex;flex-wrap:wrap;gap:12px;align-items:center"> <sui-toggle-button id="tb-ev" label-off="Voided hidden" label-on="Voided shown" icon-off="fa-regular fa-eye-slash" icon-on="fa-regular fa-eye"></sui-toggle-button> <span id="tb-ev-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Showing 24 invoices.</span> </div>
// setup(root): properties and events setup(root) { const el = root.querySelector('#tb-ev'); const log = root.querySelector('#tb-ev-log'); el.addEventListener('suiChange', (e) => { log.textContent = e.detail ? 'Showing 27 invoices, voided included.' : 'Showing 24 invoices.'; }); }

Keyboard

KeyAction
TabMoves to the button.
Space EnterPresses or releases it and fires suiChange; aria-pressed follows.

API

<sui-toggle-button>

Properties

PropertyAttributeTypeDefaultDescription
checkedcheckedbooleanfalsePressed state, exposed as aria-pressed. Mutable: the element toggles it on click, then emits suiChange. Reflected, so sui-toggle-button[checked] matches the button as it is now.
disableddisabledbooleanfalseDisables the button. It keeps its current label, posts nothing and never holds the form.
errorerrorstringstring | undefinedError message under the button; replaces the help line and sets aria-invalid.
helphelpstringstring | undefinedHelp text under the button. Replaced by error when set.
iconOfficon-offstringstring | undefinedFont Awesome class shown while off, e.g. "fa-regular fa-eye-slash".
iconOnicon-onstringstring | undefinedFont Awesome class shown while on, e.g. "fa-regular fa-eye".
invalidinvalidbooleanfalseDanger border without a message (validation handled elsewhere), as on sui-checkbox.
labelOfflabel-offstringstring | undefinedLabel shown while off; defaults to Off, or the locale's word for it.
labelOnlabel-onstringstring | undefinedLabel shown while on; defaults to On, or the locale's word for it.
namenamestringstring | undefinedForm control name. Reflected: the form entry is built from the content attribute.
readOnlyreadonlybooleanfalseDisplay only: the button keeps its name, its tab stop and its pressed state but a click changes nothing, and it still posts. Neither a read-only nor a disabled button holds the form on valueMissing.
requiredrequiredbooleanfalseThe button must be on for the surrounding form to be valid. The browser names the field from labelOn in its message; there is no aria-required, because the button role does not support it.
sizesize"lg" | "md" | "sm"'md'Control height: sm 28px, md 34px (default), lg 42px, the same stops the input family uses.
valuevaluestring'on'Value posted with the form while on; defaults to "on".

Events

EventDetailReactVueDescription
suiChangebooleanonSuiChange@sui-changeFires on toggle; detail is the new pressed state.

Methods

MethodSignatureDescription
setFocus()setFocus() => Promise<void>Moves keyboard focus to the button, as el.focus() does.

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