Skip to content

A single radio button, for composing an exclusive group by hand inside your own markup.

import { BbRadio } from 'bitboss-ui';

On this page

Use it for

There are two reasons to reach for a lone BbRadio. Either the options must sit inside other markup, such as a pricing card per option, or one option needs a label the others do not have.

Use something else when

  • BbRadioGroup: almost always. It wraps the same buttons in a real <fieldset>, maps your items, wires name and the model, and gives you one validation channel for the set
  • BbSelect: the set is large or dynamic
  • BbCheckbox: it is an independent yes/no

Pass Through

Hover or tap a part to outline it. Toggles flip loading, errors and warnings when the component has them — only parts highlight.

Guidance while the control is focused.

A lone BbRadio is an escape hatch. This page is mostly about knowing whether you are standing in front of one.

The field states, the value contract and the layout are identical to BbCheckbox's, and are documented there. Carry one thing over: reverse inverted its meaning in v3, so an explicit value you are migrating has to be flipped.

An exclusive group by hand

Three things make a set of BbRadio behave as one group: a shared name, the same v-model, and a distinct value on each button.

Model value: 1

Selecting one deselects the others, because they share the model. And because they are native radios sharing a name, the browser gives you roving focus for free: Tab reaches the set once, and the arrow keys move focus and selection between the buttons.

value is what lands in the model. Any serializable value works and is matched structurally, but prefer stable primitive ids: they survive a refetch and are cheap to submit.

What you do not get for free is the grouping semantics. The container is yours, and so are its role="radiogroup" and its accessible name. A set of radios with no wrapper is announced as three unrelated buttons.

Coming from v2

name is no longer required. Omit it and the component generates one, unique to that button and stable for its lifetime, so a lone radio is never left unnamed. Watch the group case, though: generated names differ per button. Without an explicit shared name no native group forms, and you lose exclusivity and the arrow keys with it.

Options inside other markup

The first reason to hand-compose: each option carries content that sits beside the radio rather than inside it.

Free
€24 / mo
€96 / mo

BbRadioGroup maps one label and one value per item. When an option needs a second column, a price or a thumbnail say, that mapping runs out and you want a BbRadio per option inside your own card.

Keep description on the radio rather than in the card. It is wired into the input's aria-describedby, so it is read as part of the option. The same text in a sibling <p> is read as unrelated page content.

Per-option labels

The second reason: one option needs a badge, emphasis or a link that the others do not.

The label slot replaces the rendered label and receives { text, hasErrors, hasWarnings }, where text is the label prop. Keep passing label even when you override it: that string is still the accessible name.

Reserve this for the option that genuinely differs. If every option needs the same decoration you have a uniform mapping again, and BbRadioGroup does it with less markup.

Errors and locked options

errors lives on a button and the error belongs to the group, which is where the missing fieldset starts to cost you something.

Choose a sign-in method.

Put the message on one button, the last, so it reads as closing the set. Repeated errors render the same sentence three times and announce it three times. The price is that the message stays tied to that one option. If a hand-composed group needs validation at all, move to BbRadioGroup.

disabled and readonly differ exactly as they do on BbCheckbox. There is one radio-specific catch: role="radio" supports no aria-readonly, so a lone readonly radio cannot announce that it is locked. Say it in description, or use BbRadioGroup, whose radiogroup does carry the attribute.

Custom markup

At the far end of the same slope, BbRadioGroup then BbRadio then this, BbBaseRadioIcon gives you the library's dot inside a surface that is entirely yours.

Workspace plan

BbBaseRadioIcon draws and nothing more: the ring and the scaling inner dot, with no input, no events and no state. You pass checked, disabled, readonly, has-errors, has-warning and focus-visible. There is no indeterminate here, because a radio is binary per option.

It is aria-hidden and cannot be focused, so it must sit on real native radios sharing one name. Those are what keep exclusivity and the arrow keys working. Hide the input with sr-only rather than display: none, which would remove it from the tab order and from the form. Pass focus-visible yourself, or a keyboard user gets no ring.

There is a smaller step before that one. The icon slot on BbRadio replaces only the dot visual and keeps the input, the label and the field chrome. It receives the live state: checked, focused, focusVisible, disabled, readonly, hasErrors, value and text.

The custom properties live on the dot element, so target that element rather than an ancestor:

css
.bb-radio .bb-base-radio-icon {
    --size: 20px; /* ring diameter, default 16px */
    --space: 3px; /* gap between ring and dot, default 2px */
    --color: #16a34a; /* the dot */
    --ring-color: var(--bb-ring);
}

This is where v2's color prop went, removed with no replacement. Recolor through --color, or through the theme's --bb-primary when the whole product should follow.