Skip to content

BbRadioGroup

Exactly one choice out of a set, with native arrow keys and a required prop.

import { BbRadioGroup } from 'bitboss-ui';

On this page

Use it for

Reach for BbRadioGroup when exactly one option out of a small, visible set is the answer: a priority, a shipping speed, a plan. Up to about seven options that a reader wants to compare at a glance belong here.

Use something else when

  • BbSelect: the set runs past that, or the list is searchable, paged or fetched
  • BbCheckboxGroup: the choices are not exclusive
  • BbRadio: an escape hatch for options that have to live inside markup of your own
  • BbTabs: the segmented strip switches what the page shows (Board / Table) rather than picking a value a form submits

Also see: Options and items.

Everything this group shares with the other two is on BbCheckboxGroup. That covers the items mapping, the fieldset props and selectable. It also covers the provider and enforce-coherence, the label slot and the layout. The field surface is BbCheckbox's. What follows is only what an exclusive group has to answer.

Passthrough

Hover or tap a part to outline it. The toggles turn on loading, errors and warnings where the component has them; only parts get a row.

Shown under the label as lasting context.
Guidance while the control is focused.

One exclusive choice

The model holds one resolved value, not an array, and it starts empty as null.

PrioritySets the response time the customer is promised.

Model: null

items and the accessors work as they do on the checkbox group. An array of strings needs nothing, and neither does an array of { label, value } objects. Objects of any other shape need item-text and item-value. There is no multiple prop here and no max.

The thing to design around is that the user cannot go back to empty. Radios have no un-choose gesture: once an option is picked, only another option can replace it. If "no answer" is still legitimate after the first click, make it an option of its own. A "No preference" row whose value is null does the job.

Seed the model with null when nothing is chosen, and with a real value when a sensible default exists. A pre-selected default that quietly commits the user to something they never read is worse than an empty group.

One tab stop, then the arrows

The options are native radios, so the browser's grouping behaviour comes for free. The group adds the ARIA role the native markup lacks.

Team
Role

Selected: Engineering, Developer

Tab reaches the whole group once, landing on the chosen option or on the first one. Inside it, the arrow keys move focus and the selection together, and wrap at the ends. That coupling is what makes a radio group an exclusive choice. It is why a locked option has to be skipped rather than merely greyed.

The options container carries role="radiogroup", which a <fieldset> cannot supply. The component claims the role only when legend or the legend slot gives it a name (an aria-label does not count). An unnamed radiogroup is worse than no role at all. That is another reason legend is required, and why hide-legend is the right way to drop it visually.

readonly is the one field state that behaves differently here. The radio role supports no aria-readonly, so readonly can only be announced on the group. A hand-composed set of BbRadio has no group to announce it on.

Coming from v2

name is no longer required. Omit it and the group generates one, unique to that instance and stable for its lifetime. Two unnamed groups on one page never clear each other's selection. Pass a name when the value has to post under a known key in a native form submission. The relaxation is specific to the group: a set of bare BbRadio still needs an explicit shared name.

Locked options

selectable rejects an option per item, and in a radio group a rejected option disappears from the keyboard path entirely.

Role for the new memberYou are an Admin, so you can grant any role below Admin.

Inviting as Developer

Arrow keys skip disabled radios, so a keyboard user never lands on one. That makes the description do real work: name the rule that locks them, once, for the set. Keep the locked options visible when the ladder above the user is information, and filter them out of items when it is not.

A disabled field inside your item objects is plain data, not a switch: lock options with selectable. The predicate and the migration from v2 are on BbCheckboxGroup.

To freeze the choice, use readonly: it stays focusable and submitted. disabled on the group locks every option, the chosen one included, and keeps the value out of a native post.

Requiring a choice

This is the only one of the three groups with a required prop, and it is for a group outside a form.

Stock status

Inside a BbForm the group joins the form under its name, and rules="required", a rule you register once, holds the submit until an option is chosen. Submit without choosing to see the message. The rule also marks the legend. The required prop alone would only mark it: the form posts novalidate, so the attribute is a hint that checks nothing.

Outside a form, required reaches the native radios: assistive technology announces the group as required, and a plain <form> blocks the submit. Pass errors yourself after a failed submit.

On a checkbox or switch group the same question becomes "at least one", which is a rule on the array rather than a flag. See validating the whole set.

Pick a value with the tabs look

A strip that picks a value a form or a filter submits (a status filter, a period) is still a radio group, so give the group a tab strip's look instead of reaching for BbTabs, which switches what the page shows. variant="pill" draws a muted strip with a raised pill under the pick, and variant="line" a baseline with a bar under it.

Status
Period

The strip, its options and the indicator measure exactly as a tab strip's, and the indicator slides the same way. It stays a radio group: the arrow keys pick, "2 of 4" is announced, and the legend, errors, required and compact work. The dot is hidden (an icon slot shows before the label), and the strip never wraps: it scrolls. list-layout="vertical" stacks it into a column and "vertical-stretch" spans the field; the other layouts and item-layout do not apply.

pt:indicator reaches the pill or the bar, and pt:<part>:pill or pt:<part>:line styles a part in one look only. The strip's tokens are named and valued as a tab strip's (--list-bg, --indicator-bg, --active-fg, …): set them on pt:list for one group. For a look of your own app-wide, register radioVariants: ['brand'] in the build plugin and set the same tokens on .bb-radio-group__list--brand.

Selection tiles and CSS

A radio group is the component people most often want to stop looking like radio buttons. Compose the two per-option slots, then style the option through pt:item.

Appearance

icon replaces the dot and label replaces the text, and both receive the option's item alongside the current state. A tile is built from your own data, with no second array to keep in step. Everything underneath stays a real radio: the name, the exclusivity and the arrow keys are untouched.

Two props do the layout, so pt only has to give the tile its look. list-layout="tiles" makes the options equal-size boxes that share the row and wrap as the field narrows, and item-layout="vertical" stacks the picture over the caption. The default horizontal list sizes each option to its content.

A tile is its input, edge to edge: the whole tile, padding included, is the click and hover target (a tooltip whose activator is the tile's input shows anywhere on it), and a link or a button inside keeps its own click. The tile's text is not selectable with the mouse, and a click listener on a plain element inside the label slot receives nothing: listen on the group's @change. The same holds for BbCheckboxGroup and BbSwitchGroup tiles.

Style the tile through pt:item and its states: pt:item:checked paints the chosen tile and pt:item:focus-visible the one with keyboard focus, so nothing names a library class or reaches for :has(). The focus ring belongs on the tile because the icon slot replaced the dot that drew it.

Coming from v2

This group's CSS block used to be the shared bb-cr-container. The options are now .bb-radio-group__list and each row is .bb-radio-group__item. Nothing renders the old prefix, so those rules stop matching; npx bitboss-ui check reports each leftover class in your stylesheets. The full before-and-after table is on BbCheckboxGroup, along with the labelPosition → legendPosition rename that applies here too.

The group exposes no custom properties of its own. The dot is BbBaseRadioIcon, which owns --accent, --size, --space, --border-color and --ring-color on its own element. That is the surface documented on BbRadio, and where v2's removed color prop went.