Payout schedule
When the daily settlement should land in your bank.
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.
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.
Title
Inter Tight Regular, 16 / 24, in foreground. The thing the user is choosing. Sentence case, never a sentence — labels, not paragraphs.
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.
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
The default. Stacks vertically; titles align to the left edge so the eye scans down a clean column.
Horizontal · short labels
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.
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.
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 sharednameon 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 anid+htmlForpair 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-describedbyon the fieldset pointing at the helper text, and announce the message viaaria-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.
Do
Stack vertically. The eye scans down a single column and lands on the answer in one fixation.
Don't
Don't horizontal-pack long labels. The eye loses the column and the help text wraps unpredictably.
Privacy
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
Don't
Don't ship a group with nothing selected. The user has to guess what 'no choice' even submits as.
Do
Use a switch for a single binary. 'On / off' fits a switch; 'Yes / No' as the only choice doesn't need two radios.
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
| Prop | Type | Default | Description |
|---|---|---|---|
| legend | string | — | The question the group answers. Renders as <legend> inside <fieldset> for native semantics. |
| description | string | — | Optional one-line clarification under the legend. 14 px in text.secondary. |
| value | string | — | The currently selected radio's value. Make the group controlled — the group is never internally empty. |
| onValueChange | (value: string) => void | — | Fired when the user picks a different option. Receives the new value, not the event. |
| error | string | — | Helper 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. |
| name | string | — | Form field name. Required when the group submits as part of a form. Auto-generated if omitted. |
Radio
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | — | Required. The value submitted when this option is selected. |
| title | string | — | Required. The option's label. Inter Tight Regular, 16 / 24, sentence case. |
| help | string | — | Optional one-line clarification. 14 / 20 in text.secondary. |
| disabled | boolean | false | Removes the option from the tab order and dims the visual to 50%. Pair with helper text explaining why. |
| ...rest | InputHTMLAttributes | — | All standard input attributes pass through (id, name, autoFocus, etc.). |