Components · Forms

Radio button

A radio lets the user pick exactly one option from a small set (3–5). The whole row is the click target — mark, title, and help text. By default, one option is always selected.

Documentedby Derek Fidler

Payout schedule

When the daily settlement should land in your bank.

Same dayAvailable before 18:00 CET on weekdays.
Next business dayThe default. Lands by 09:00 the morning after.
WeeklyBundled into one transfer every Monday morning.

Overview

Radios are for mutually exclusive picks where every option is worth seeing at a glance — payout schedule, payment method, plan tier. Use them when the set has 3 to 5 options. With 2, a switch reads cleaner; with 6 or more, a select keeps the form short. Radios always belong to a group, never on their own.

A selection is always made

By default a group ships pre-selected with a sensible default — the most common choice, the safest, or the one the page is currently committing to. An empty group is a form bug, not a state.

Anatomy

Four parts inside the row, plus the group wrapper that holds them together. The whole row — mark, title, help text — is the click target; clicking the title selects the option just like clicking the ring.

Payout schedule

When the daily settlement should land.

Same dayAvailable before 18:00 CET on weekdays.
Next business dayLands by 09:00 the morning after.
  1. Mark

    20 px ring, 1.5 px stroke, in foreground. When checked, an 8 px filled dot sits centred. Use a 4 px optical offset above the title's cap height so they align.

  2. Title

    Inter Tight Regular, 16 / 24, in foreground. The thing the user is choosing. Sentence case, never a sentence — labels, not paragraphs.

  3. Help text (optional)

    Inter Tight Regular, 14 / 20, in text.secondary. One line. Use for a constraint, a delivery time, or a small consequence — not for marketing.

  4. Group (fieldset + legend)

    <fieldset> with a <legend> that names the question. 20 px gap between siblings; 12 px gap between legend and the first option.

States

Five visible states, each rendered checked and unchecked. Held still here for review — the real component handles transitions natively.

  • Default

    1.5 px ring in foreground. Empty centre. The resting state for an unselected option.

  • Checked

    Same ring; an 8 px filled dot in foreground sits centred. The selected option in a group.

  • Hover

    A soft halo extends 6 px beyond the ring on pointer entry. Cursor is anywhere on the row, not just the mark.

  • Hover · checked

    Halo + dot. Hovering the active option doesn't deselect it; clicking it does nothing (no toggle).

  • Focus

    2 px ring at 2 px offset, drawn around the mark. Only on :focus-visible — keyboard nav.

  • Focus · checked

    Focus ring + filled dot. Arrow keys move focus and selection together inside the group.

  • Disabled

    Ring at 40% foreground. Mark drops to 50% opacity. Removed from the tab order.

  • Disabled · checked

    Ring + dot both at the disabled tone. The state still reads, just doesn't respond to input.

  • Error

    Ring in red.800 — pairs with a danger-toned helper line below the title.

  • Error · checked

    Same red ring; the dot follows. Error styling never replaces the helper text.

Layout

Vertical is the default; the eye scans down one column and lands on the answer. Horizontal works only when every label is one or two words and the group is part of a wider row of fields.

Vertical · default

Bank accountending 4242
Card on fileending 0102
Pay by transfersettles in 1–2 days

The default. Stacks vertically; titles align to the left edge so the eye scans down a clean column.

Horizontal · short labels

EUR
GBP
DKK
USD

Use only when every label is one or two words. Three or more options on the same line scan worse than a vertical stack.

With help text

Use help text to disambiguate options the title can't carry on its own — a delivery window, a constraint, a price. Keep it to one line. If the help text needs two lines, the option is too complex for a radio; reach for a radio card or split the question.

Standard3–5 working days · free
Express1–2 working days · 4,99 €
Same-dayOrder before 14:00 · 9,99 €

Error

The error tone applies to the whole group — every ring goes red, and a single helper message sits below the last option. Don't paint untouched required groups red preemptively; only after the user submits without a choice.

Error

Pickup or delivery

Pick one to continue.

Pickup
Delivery

Choose pickup or delivery to continue.

Apply the error tone only when the user submits without a choice. Don't paint untouched groups red just because they're required.

Behavior

  • The whole row is the click target. Clicking the title or the help text selects the option, just like clicking the ring. The hover halo widens to cover the row, not just the mark.
  • A radio can't be unchecked by clicking it. That's the difference from a checkbox — selecting another option deselects the current one. Provide a “None” option if the user needs to opt out.
  • Default is the safe pick. Pre-select the option most users will keep — “Next business day” for payouts, the lower-risk shipping option, the more conservative permission. Never pre-select something destructive or surprising.
  • Order options consistently. When there's a natural progression (low → high, soonest → latest, free → expensive), order by it. Otherwise lead with the default and follow with frequency.
  • Don't hide the group behind progressive disclosure. Radios are for choices the user is making now. If the choice shouldn't be visible until a precondition is met, show it when the precondition is met — don't lurk it behind a “More options” toggle.

Radio vs. checkbox vs. select vs. switch

Picking the right control matters more than picking the right styling. The four are not interchangeable.

  • Radio

    3–5 mutually exclusive options, all worth seeing. One must be selected. Default ships pre-selected.

  • Checkbox

    Multi-select, or a single yes/no that's either part ofa list (terms, opt-ins) or genuinely binary (“Save card for next time”).

  • Select

    6+ mutually exclusive options, or fewer when space is at a premium and the options aren't worth previewing — country, currency, time zone.

  • Switch

    A single binary that takes effect immediately — turning a feature on or off. Not for choosing between options.

Accessibility

  • Use native inputs. <input type="radio"> with a shared nameon every option in the group. The browser handles arrow-key nav, focus, and form submission for free; don't reach for a custom implementation.
  • Group with <fieldset> + <legend> so screen readers announce the question before the options. Alternative for non-form contexts: role="radiogroup" + aria-labelledby.
  • Labels associate explicitly. Wrap each input in its <label> or use an id + htmlFor pair so clicking the title selects the option.
  • Keyboard: / move and select. Tab leaves the group. Don't override — the native pattern is what users expect.
  • Focus ring is keyboard-only. :focus-visibleshows the ring; mouse clicks don't. Never replace the ring with a colour change inside the radio.
  • Error messages link to the group, not each option. Use aria-describedby on the fieldset pointing at the helper text, and announce the message via aria-live="polite" when it appears.
  • Touch targets: the entire row is clickable. The 20 px mark is the visual; the row gives the user a 44 px+ tap target.

Code

The component composes from RadioGroup + Radio primitives in @flatpay-dk/ui. The group renders the fieldset/legend; each radio is a single option with title and optional help.

tsx

import { RadioGroup, Radio } from "@flatpay-dk/ui";

// Default group with a pre-selected option
const [schedule, setSchedule] = useState("next");

<RadioGroup
  legend="Payout schedule"
  description="When the daily settlement should land in your bank."
  value={schedule}
  onValueChange={setSchedule}
>
  <Radio
    value="same"
    title="Same day"
    help="Available before 18:00 CET on weekdays."
  />
  <Radio
    value="next"
    title="Next business day"
    help="The default. Lands by 09:00 the morning after."
  />
  <Radio
    value="weekly"
    title="Weekly"
    help="Bundled into one transfer every Monday morning."
  />
</RadioGroup>

// Disabled option
<Radio
  value="instant"
  title="Instant"
  help="Coming soon for European accounts."
  disabled
/>

// Error state — applies to the whole group
<RadioGroup
  legend="Pickup or delivery"
  value={method}
  onValueChange={setMethod}
  error="Choose pickup or delivery to continue."
>
  <Radio value="pickup" title="Pickup" />
  <Radio value="delivery" title="Delivery" />
</RadioGroup>

Best practices

Small habits that keep radios scannable and predictable across forms.

Same day
Next business day
Weekly

Do

Stack vertically. The eye scans down a single column and lands on the answer in one fixation.

Same day
Next business day
Weekly

Don't

Don't horizontal-pack long labels. The eye loses the column and the help text wraps unpredictably.

Privacy

PrivateOnly you can see it.
TeamEveryone in your org.
PublicAnyone with the link.

Do

Always pre-select the safe default. The form is never empty; the user always knows what ships if they don't change anything.

Privacy

PrivateOnly you can see it.
TeamEveryone in your org.
PublicAnyone with the link.

Don't

Don't ship a group with nothing selected. The user has to guess what 'no choice' even submits as.

Daily payout summary

Do

Use a switch for a single binary. 'On / off' fits a switch; 'Yes / No' as the only choice doesn't need two radios.

Send me product updates

Looks like a setting that can't be turned off.

Don't

Don't ship a single radio. It looks like a setting that can't be turned off — and it can't, because there's only one option.

Props

RadioGroup

PropTypeDefaultDescription
legendstringThe question the group answers. Renders as <legend> inside <fieldset> for native semantics.
descriptionstringOptional one-line clarification under the legend. 14 px in text.secondary.
valuestringThe currently selected radio's value. Make the group controlled — the group is never internally empty.
onValueChange(value: string) => voidFired when the user picks a different option. Receives the new value, not the event.
errorstringHelper text rendered in the danger tone below the last option. Pairs every ring with the error border.
orientation"vertical" | "horizontal""vertical"Stacking direction. Use horizontal only for short single-word labels.
namestringForm field name. Required when the group submits as part of a form. Auto-generated if omitted.

Radio

PropTypeDefaultDescription
valuestringRequired. The value submitted when this option is selected.
titlestringRequired. The option's label. Inter Tight Regular, 16 / 24, sentence case.
helpstringOptional one-line clarification. 14 / 20 in text.secondary.
disabledbooleanfalseRemoves the option from the tab order and dims the visual to 50%. Pair with helper text explaining why.
...restInputHTMLAttributesAll standard input attributes pass through (id, name, autoFocus, etc.).