Radio
Description
Section titled “Description”.bp-radio wraps a native <input type="radio"> with its visible label. The custom circle and animated dot are rendered entirely in CSS — no JavaScript required. Groups use a native <fieldset> + <legend> for built-in accessibility. Error state is surfaced via aria-invalid="true" on the fieldset.
Single radio
<label class="bp-radio"><input class="bp-radio__input" type="radio" name="single" value="a" /><span class="bp-radio__label">Option A</span></label>Group — vertical (default)
<fieldset class="bp-radio-group"><legend class="bp-radio-group__legend">Contact preference</legend><label class="bp-radio"> <input class="bp-radio__input" type="radio" name="contact" value="email" checked /> <span class="bp-radio__label">Email</span></label><label class="bp-radio"> <input class="bp-radio__input" type="radio" name="contact" value="phone" /> <span class="bp-radio__label">Phone</span></label><label class="bp-radio"> <input class="bp-radio__input" type="radio" name="contact" value="post" /> <span class="bp-radio__label">Post</span></label></fieldset>Group — horizontal
<fieldset class="bp-radio-group bp-radio-group--horizontal"><legend class="bp-radio-group__legend">Size</legend><label class="bp-radio"> <input class="bp-radio__input" type="radio" name="size" value="sm" /> <span class="bp-radio__label">Small</span></label><label class="bp-radio"> <input class="bp-radio__input" type="radio" name="size" value="md" checked /> <span class="bp-radio__label">Medium</span></label><label class="bp-radio"> <input class="bp-radio__input" type="radio" name="size" value="lg" /> <span class="bp-radio__label">Large</span></label></fieldset>Group — error state
<fieldset class="bp-radio-group" aria-invalid="true" aria-describedby="contact-err"><legend class="bp-radio-group__legend">Contact preference</legend><label class="bp-radio"> <input class="bp-radio__input" type="radio" name="contact-err" value="email" /> <span class="bp-radio__label">Email</span></label><label class="bp-radio"> <input class="bp-radio__input" type="radio" name="contact-err" value="phone" /> <span class="bp-radio__label">Phone</span></label><p class="bp-radio-group__error" id="contact-err" role="alert">Please select an option.</p></fieldset>Public API
Section titled “Public API”| Variable | Default | Description |
|---|---|---|
--radio-size | 1.125rem | Diameter of the radio circle |
--radio-dot-size | 0.5rem | Diameter of the inner checked dot |
--radio-color | var(--bp-primary) | Checked border and dot fill color |
--radio-border | 1px solid var(--bp-color-border) | Unchecked border |
--radio-bg | var(--bp-color-bg-elevated) | Unchecked background color |
--radio-gap | var(--bp-space-2) | Gap between circle and label text |
--radio-group-gap | var(--bp-space-3) | Gap between items in a vertical group |
--radio-group-gap-horizontal | var(--bp-space-6) | Gap between items in a horizontal group |
--radio-label-color | var(--bp-color-text) | Label text color |
--radio-error-color | var(--bp-color-error) | Error message and border color |
Customization
Section titled “Customization”Custom accent color and size
<style>.demo-radio--custom { --radio-color: var(--bp-color-success); --radio-size: 1.375rem; --radio-dot-size: 0.625rem;}</style><fieldset class="bp-radio-group demo-radio--custom"><legend class="bp-radio-group__legend">Availability</legend><label class="bp-radio"> <input class="bp-radio__input" type="radio" name="avail" value="yes" checked /> <span class="bp-radio__label">Available</span></label><label class="bp-radio"> <input class="bp-radio__input" type="radio" name="avail" value="no" /> <span class="bp-radio__label">Unavailable</span></label></fieldset> ✓ No axe violations tested 2026-05-12
WCAG criteria covered:
Accessibility
Section titled “Accessibility”- Always wrap radio groups in a
<fieldset>with a<legend>. The legend is read by screen readers as the group label before announcing each option. - Individual radios use a wrapping
<label>for implicit association — nofor/idwiring needed. - Error state is signalled via
aria-invalid="true"on the<fieldset>andaria-describedbypointing to the.bp-radio-group__errorparagraph. The error paragraph carriesrole="alert"so screen readers announce it on insertion. - Keyboard: native radio behavior is preserved —
Tabmoves focus into the group,Arrowkeys cycle options within the group. - The custom circle and dot are purely decorative CSS — the underlying
<input>remains in the accessibility tree and reports the correct checked state. - Do not rely on border color alone to convey error — always pair with the visible error message.
Browser APIs
Section titled “Browser APIs”| API | Availability | Used for | Without it | Polyfill |
|---|---|---|---|---|
appearance: none | Widely available Baseline 2020 | Removing the browser-native radio chrome | Native radio circle renders instead of custom | None needed |
:focus-visible | Widely available Baseline 2022 | Showing focus ring only on keyboard navigation | Focus ring shows on mouse click too (:focus fallback) | None needed |
:has() | Newly available Baseline 2023 | Disabling cursor on the whole .bp-radio wrapper when input is disabled | Cursor stays pointer on label text when input is disabled | None needed — cosmetic only |
Internals
Section titled “Internals”--_size,--_dot-size,--_color,--_border,--_bg,--_gap,--_label-color,--_group-gap,--_error-color— private resolved values, do not set directly.- The dot is a
::beforepseudo-element on.bp-radio__label, not on the<input>. Inputs cannot reliably host pseudo-elements across browsers. - The dot is positioned with
position: absoluteand a calculatedleftoffset:calc(-1 * gap - size/2 - dot-size/2), which centers it inside the adjacent input circle regardless of token values. - Checked/unchecked transition uses
transform: scale(0 → 1)for the dot andborder-colorfor the ring — both GPU-composited, no layout triggered. - The error cascade uses
fieldset[aria-invalid="true"] .bp-radio__inputto apply error styling to all inputs in the group from a single attribute on the fieldset.