RadioGroup
A set of options where exactly one can be chosen. Arrow keys move through them and choose; Tab leaves the group.
The preview needs JavaScript. The code is in the Code tab.
<script>
const options = [
{ value: 'monthly', label: 'Monthly' },
{ value: 'yearly', label: 'Yearly, two months free' },
{ value: 'team', label: 'Team (contact us)', disabled: true },
]
</script>
<template>
<div>
<RadioGroup :options="options" value="monthly">
<span slot="label">Billing period</span>
<span slot="hint">You can change this later.</span>
</RadioGroup>
<RadioGroup :options="options" orientation="horizontal" error="Choose a billing period.">
<span slot="label">Billing period</span>
</RadioGroup>
</div>
</template>
Installation
npx @pixelscriptio/tessera add RadioGrouppnpm exec tessera add RadioGroupyarn tessera add RadioGroupbunx @pixelscriptio/tessera add RadioGroup- Works with Tessera 0.9.0 or newer.
Copy and paste the code into your project
Put it in
system/components.system/components/RadioGroup.tess <contract> { "name": "RadioGroup", "role": "radiogroup", "description": "A set of options where exactly one can be chosen. Arrow keys move through them and choose; Tab leaves the group.", "since": "0.8.0", "parts": ["root", "label", "options", "radio", "control", "text", "hint", "error"], "variants": { "orientation": ["vertical", "horizontal"] // first value is the default }, "states": ["hover", "focus-visible", "checked", "disabled"], "behavior": "radio-group", "slots": { "label": "the question the group answers, required: it names the group", "hint": "help under the options" }, "events": { "choose": "fired with { value, source } when the person chooses an option, where source is keyboard or pointer" }, "preview": { "props": { "options": [ { "value": "monthly", "label": "Monthly" }, { "value": "yearly", "label": "Yearly, two months free" }, { "value": "team", "label": "Team" } ], "value": "monthly" }, "slots": { "label": "Billing period", "hint": "You can change this later." } } } </contract> <script> // `options` is a list of { value, label, disabled }. const { options, value, disabled, error } = props({ options: [], value: '', disabled: false, error: '', }) // The group owns the choice, starts from the prop and follows it when it changes. const current = state(value.value) effect(() => { current.value = value.value }) const uid = `t-radiogroup-${Math.random().toString(36).slice(2, 9)}` // What each radio binds: checked, whether Tab stops on it (the checked one, or the first that can be chosen), disabled. const items = derived(() => { const list = options.value ?? [] const chosen = list.findIndex((o) => o.value === current.value && !o.disabled) const first = list.findIndex((o) => !o.disabled) const stop = chosen >= 0 ? chosen : first return list.map((o, i) => ({ value: o.value, label: o.label, disabled: !!(o.disabled || disabled.value), checked: i === chosen, tab: i === stop ? 0 : -1, })) }) function choose(detail) { current.value = detail.value emit('choose', { value: detail.value, source: detail.source }) } </script> <template> <div class="root" role="radiogroup" :aria-labelledby="uid + '-label'" :aria-describedby="uid + '-hint ' + uid + '-error'" :aria-invalid="error ? 'true' : null" :aria-disabled="disabled ? 'true' : null" @choose="choose($event)" > <span class="label" :id="uid + '-label'"><slot name="label" /></span> <div class="options"> <button each="o in items" key="o.value" class="radio" type="button" role="radio" :aria-checked="o.checked ? 'true' : 'false'" :tabindex="o.tab" :disabled="o.disabled" :data-value="o.value" > <span class="control"><span class="dot"></span></span> <span class="text">{o.label}</span> </button> </div> <span class="hint" :id="uid + '-hint'"><slot name="hint" /></span> <span class="error" :id="uid + '-error'" aria-live="polite">{error}</span> </div> </template> <style> .root { display: flex; flex-direction: column; gap: $space.2; color: $color.text.default; font-family: var(--t-font-family-sans); font-size: $font.size.sm; } .label { font-weight: $font.weight.bold; } .options { display: flex; flex-direction: column; gap: $space.1; } .root[data-orientation='horizontal'] .options { flex-direction: row; flex-wrap: wrap; gap: $space.4; } .radio { display: inline-flex; align-items: center; gap: $space.2; padding: $space.1 0; border: 0; background: transparent; color: $color.text.default; font: inherit; text-align: left; cursor: pointer; &:focus-visible { outline: 2px solid $color.focus.ring; outline-offset: 2px; } &:disabled { opacity: $opacity.disabled; cursor: not-allowed; } } .control { display: inline-flex; flex: none; align-items: center; justify-content: center; width: $space.4; height: $space.4; border: 1px solid $color.border.strong; border-radius: $radius.full; background: $color.surface.default; transition-duration: $motion.duration.fast; } .radio:hover:not(:disabled) .control { border-color: $color.text.default; } .dot { width: $space.2; height: $space.2; border-radius: $radius.full; background: transparent; } .radio[aria-checked='true'] .control { border-color: $color.action.primary.bg; background: $color.action.primary.bg; } .radio[aria-checked='true'] .dot { background: $color.action.primary.fg; } .hint { color: $color.text.muted; } .error { color: $color.action.danger.text; } .hint:empty, .error:empty { display: none; } </style>Check your design tokens
Your design system needs these.
tessera addreports the ones that are missing; by hand, runnpx @pixelscriptio/tessera check.color.action.danger.textcolor.action.primary.bgcolor.action.primary.fgcolor.border.strongcolor.focus.ringcolor.surface.defaultcolor.text.defaultcolor.text.mutedfont.size.smfont.weight.boldmotion.duration.fastopacity.disabledradius.fullspace.1space.2space.4
Usage
Using RadioGroup
A set of options where exactly one can be chosen.
- The label names the group and is required. It should be the question the options answer: "Billing period", not "Choose one".
- Give it
options, a list of{ value, label }(adddisabled: trueto one that cannot be chosen now), andvaluefor the one that is chosen. With novaluenothing is chosen, and Tab stops on the first option. - The arrow keys move through the options and choose the one they land on, wrapping at the ends. Space chooses the one with focus. Tab leaves the group, so it is one stop in the page, not one per option.
choosefires with{ value, source }when the person chooses an option, wheresourceiskeyboardorpointer. Listen with@choose.- Use
errorfor what is wrong, such as "Choose a billing period". It is announced, and read with the group. - Prefer a Select when there are more than about five options. For a setting that is on or off, use a Switch.
orientation="horizontal"lays the options out in a row.
The preview needs JavaScript. The code is in the Code tab.
<script>
const options = [
{ value: 'monthly', label: 'Monthly' },
{ value: 'yearly', label: 'Yearly, two months free' },
{ value: 'team', label: 'Team (contact us)', disabled: true },
]
</script>
<template>
<div>
<RadioGroup :options="options" value="monthly">
<span slot="label">Billing period</span>
<span slot="hint">You can change this later.</span>
</RadioGroup>
<RadioGroup :options="options" orientation="horizontal" error="Choose a billing period.">
<span slot="label">Billing period</span>
</RadioGroup>
</div>
</template>
API reference
Variants
| Variant | Values |
|---|---|
orientation | vertical default horizontal |
Slots
| Slot | Content |
|---|---|
label | the question the group answers, required: it names the group |
hint | help under the options |
Events
| Event | When |
|---|---|
choose | fired with { value, source } when the person chooses an option, where source is keyboard or pointer |
Anatomy
| Role | radiogroup |
| Behavior | radio-group |
| Parts | root label options radio control text hint error |
| States | hover focus-visible checked disabled |
| Since | 0.8.0 |
Design tokens it uses
Your design system needs these. tessera add checks that it has them, and can add the ones it lacks.
color.action.danger.text color.action.primary.bg color.action.primary.fg color.border.strong color.focus.ring color.surface.default color.text.default color.text.muted font.size.sm font.weight.bold motion.duration.fast opacity.disabled radius.full space.1 space.2 space.4
Checksum
04c15ae344c8e1301eee5cea626d63e1dedffd307edccd615915595b7f8ab935