Skip to content
ShineUI
Install

ChoiceCards

Choices that need a sentence.

<sui-choice-cards>SelectionStable

About

Card-style choices for options that need a sentence of explanation, an icon or a price. Single mode has radio semantics and multiple has checkbox semantics. variant="tile" lays large tiles out in a columns grid for plan pickers; list stacks rows. Form-associated: single posts name=value, multiple posts one entry per picked value.

Usage

<sui-choice-cards label="Payment method" name="method" value="ach"></sui-choice-cards> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-choice-cards"); el.options = [ { label: "ACH", value: "ach", description: "Free. Lands in 2 to 3 days.", icon: "fa-solid fa-building-columns" }, { label: "Card", value: "card", description: "2.9% fee. Posts the same day.", icon: "fa-regular fa-credit-card" }, { label: "Check", value: "check", description: "Recorded when the deposit clears.", icon: "fa-regular fa-money-bill-1", meta: "Manual" }, ]; /* SuiChoiceCardOption[] */ el.addEventListener("suiChange", (e) => console.log(e.detail)); </script>
import { SuiChoiceCards } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { const options = [ { label: "ACH", value: "ach", description: "Free. Lands in 2 to 3 days.", icon: "fa-solid fa-building-columns" }, { label: "Card", value: "card", description: "2.9% fee. Posts the same day.", icon: "fa-regular fa-credit-card" }, { label: "Check", value: "check", description: "Recorded when the deposit clears.", icon: "fa-regular fa-money-bill-1", meta: "Manual" }, ]; /* SuiChoiceCardOption[] */ return <SuiChoiceCards label="Payment method" name="method" value="ach" options={options} onSuiChange={(e) => console.log(e.detail)} />; }
<script setup> import { SuiChoiceCards } from "@shineui/vue"; import "@shineui/core/shineui.css"; const options = [ { label: "ACH", value: "ach", description: "Free. Lands in 2 to 3 days.", icon: "fa-solid fa-building-columns" }, { label: "Card", value: "card", description: "2.9% fee. Posts the same day.", icon: "fa-regular fa-credit-card" }, { label: "Check", value: "check", description: "Recorded when the deposit clears.", icon: "fa-regular fa-money-bill-1", meta: "Manual" }, ]; // SuiChoiceCardOption[] const onChange = (e) => console.log(e.detail); </script> <template> <SuiChoiceCards label="Payment method" name="method" value="ach" :options="options" @sui-change="onChange" /> </template>
import { Component } from "@angular/core"; import { SuiChoiceCards } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiChoiceCards], template: `<sui-choice-cards label="Payment method" name="method" value="ach" [options]="options" (suiChange)="onChange($event)"></sui-choice-cards>`, }) export class Example { options = [ { label: "ACH", value: "ach", description: "Free. Lands in 2 to 3 days.", icon: "fa-solid fa-building-columns" }, { label: "Card", value: "card", description: "2.9% fee. Posts the same day.", icon: "fa-regular fa-credit-card" }, { label: "Check", value: "check", description: "Recorded when the deposit clears.", icon: "fa-regular fa-money-bill-1", meta: "Manual" }, ]; // SuiChoiceCardOption[] onChange(e: CustomEvent) { console.log(e.detail); } }

Basic

Single mode is a role="radiogroup" of role="radio" cards with aria-checked, one of which is the tab stop. A card can carry an icon, a description and a short meta note.

<div style="max-width:480px"> <sui-choice-cards id="cc-basic" label="Payment method" name="method" value="ach"></sui-choice-cards> </div>
// setup(root): properties and events setup(root) { root.querySelector('#cc-basic').options = methods; }

Tile variant

variant="tile" lays large tiles out in a columns grid, with meta drawn in the tile header. Use it for plan pickers.

<div style="max-width:640px"> <sui-choice-cards id="cc-tile" label="Subscription plan" name="plan" value="practice" variant="tile" columns="3" help="Change the plan any time. Billing prorates."></sui-choice-cards> </div>
// setup(root): properties and events setup(root) { root.querySelector('#cc-tile').options = plans; }

Multiple

multiple gives checkbox semantics: a role="group" of role="checkbox" cards, every one a tab stop. A picked card draws a check in its mark.

<div style="max-width:480px"> <sui-choice-cards id="cc-multi" label="Add-ons" name="addon" multiple help="Add-ons bill monthly with the plan."></sui-choice-cards> </div>
// setup(root): properties and events setup(root) { const el = root.querySelector('#cc-multi'); el.options = addOns; el.value = ['payroll']; }

States

The label and the message line sit outside the group, so a screen reader does not read the error again while the user arrows through the cards. A disabled option cannot be picked and posts nothing.

<div style="display:grid;gap:18px;max-width:480px"> <sui-choice-cards id="cc-req" label="Payment method" name="cc-method-req" required help="Pick how this customer pays."></sui-choice-cards> <sui-choice-cards id="cc-err" label="Payment method" name="cc-method-err" required error="Payment method is required."></sui-choice-cards> <sui-choice-cards id="cc-off" label="Payment method" name="cc-method-off" value="ach" disabled help="Locked while a payment is in flight."></sui-choice-cards> </div>
// setup(root): properties and events setup(root) { ['#cc-req', '#cc-err', '#cc-off'].forEach((id) => { root.querySelector(id).options = methods; }); }

Read-only and invalid

readonly keeps the cards named and in the tab order and refuses a pick while the set still posts. invalid paints every card in the danger tone without a message. The tile grid holds at most columns tiles and never one under 160px, so a phone shows one column.

<div style="display:grid;gap:18px;max-width:420px"> <sui-choice-cards id="cc-ro" label="Payment method" name="method-ro" value="ach" readonly></sui-choice-cards> <sui-choice-cards id="cc-invalid" label="Payment method" invalid></sui-choice-cards> </div>
// setup(root): properties and events setup(root) { root.querySelector('#cc-ro').options = methods; root.querySelector('#cc-invalid').options = methods; }

In a form

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

Submit to see what posts.
<form id="cc-form" style="display:grid;gap:18px;max-width:480px"> <sui-choice-cards id="cc-form-method" label="Payment method" name="method" required></sui-choice-cards> <sui-choice-cards id="cc-form-addons" label="Add-ons" name="addon" multiple></sui-choice-cards> <sui-button type="submit" variant="brand" label="Save billing"></sui-button> <output id="cc-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('#cc-form'); const out = root.querySelector('#cc-out'); const method = root.querySelector('#cc-form-method'); method.options = methods; method.value = 'ach'; const add = root.querySelector('#cc-form-addons'); add.options = addOns; add.value = ['payroll', 'time']; 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.

Idle.
<div style="display:grid;gap:12px;max-width:480px"> <sui-choice-cards id="cc-ev" label="Payment method" name="cc-method-ev"></sui-choice-cards> <span id="cc-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('#cc-ev'); const log = root.querySelector('#cc-ev-log'); el.options = methods; el.addEventListener('suiChange', (e) => { log.textContent = `Picked ${e.detail}.`; }); }

Keyboard

KeyAction
TabSingle mode has one tab stop: the picked card, or the first enabled one. With multiple, every card is a tab stop.
ArrowDown ArrowRightFocuses the next enabled card, wrapping to the first; in single mode also picks it.
ArrowUp ArrowLeftFocuses the previous enabled card, wrapping to the last; in single mode also picks it.
Space EnterPicks the focused card; with multiple, toggles it.

API

<sui-choice-cards>

Properties

PropertyAttributeTypeDefaultDescription
columnscolumnsnumber3Grid columns for the tile variant: the most the row holds. A tile never goes under 160px, so a narrow screen shows fewer columns and a phone one.
disableddisabledbooleanfalseDisables every card. A disabled set posts nothing and never holds the form.
errorerrorstringstring | undefinedError message; paints every card in the danger tone and sets aria-invalid.
helphelpstringstring | undefinedHelp text under the cards. Replaced by error when set.
invalidinvalidbooleanfalseDanger border on every card without a message, for a form that draws its summary elsewhere.
labellabelstringstring | undefinedGroup label (12.5px / 620).
multiplemultiplebooleanfalseCheckbox semantics: a role="group" of role="checkbox" cards, any number of which can be picked. A picked card draws a check in its mark. Single mode is a role="radiogroup" of role="radio" cards.
namenamestringstring | undefinedForm control name. Reflected: the form entry is built from the content attribute.
optionsproperty onlySuiChoiceCardOption[][]Options { label, value, description, icon, meta, disabled }[]. meta is a short trailing note such as a price. A disabled option cannot be picked and posts nothing. Property, not attribute.
readOnlyreadonlybooleanfalseDisplay only: every card keeps its name and its tab stop but a click, Space, Enter or an arrow key changes nothing, and the set still posts. Neither a read-only nor a disabled set holds the form on valueMissing.
requiredrequiredbooleanfalseA selection is required for the surrounding form to be valid.
valuevalueSuiChoiceValueSuiKey[] | number | string | undefinedThe picked card'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 card whose value reads the same. Mutable; written before suiChange fires.
variantvariant"list" | "tile"'list'list stacks rows (default); tile renders large tiles in a grid with meta in the tile header.

Events

EventDetailReactVueDescription
suiChangeSuiChoiceValueSuiKey[] | number | stringonSuiChange@sui-changeFires when the selection changes; detail is the new value: the picked card's value, or the array of picked values when multiple.

Methods

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

Types

SuiChoiceCardOption interface
labelstring
valueSuiKeyWhat value holds, as given: a number stays a number through value, suiChange and a framework binding. The form posts its text.
description?string
icon?string
meta?string
disabled?boolean
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.

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