POS menu visibility
Show the product description in the POS menu.
Overview
A choice list is a question and its answers. The header names the question; the items list the answers; the optional inline message handles validation errors. Three item types ship in the same wrapper — radio button, checkbox, and radio card — and each one tells the user a different thing about how the selection works.
Reach for choice list, not a bare radio group
Use the choice list whenever the question deserves a label, a description, or error feedback. A bare RadioGroup works fine for embedded picks (an inline filter, a dense settings row), but the moment the question needs an explainer, reach for the choice list — it carries that copy in the right place at the right size.
Anatomy
Three regions stacked vertically: the header up top, the items in the middle, and the optional inline error at the bottom. 16 px gap between every adjacent pair, regardless of which region is showing.
POS menu visibility
Show the product description in the POS menu.
Label
Inter Tight Medium 16 / 24, foreground. The question. Sentence case, never a sentence — labels, not paragraphs.
Subline (optional)
Inter Tight Regular 14 / 20, text.secondary. One line. Use for a constraint, a price, a soft hint.
Items
Radio buttons (vertical, 20 px gap), checkboxes (vertical, 20 px gap), or radio cards (horizontal on desktop, 16 px gap; vertical on mobile).
Inline error (optional)
20 px
error_outlineicon + helper copy intext.danger.bolder(red.900). Sits below the last item; the items themselves carry the error tone above it.
Types
Three item types share the same wrapper. Pick by the selection model — exclusive vs not — and by how much each option needs to explain itself.
Type · radio button
Mutually exclusive. One option is always selected. 3–5 options.
Payout schedule
When the daily settlement should land.
Type · checkbox
Multi-select. Zero or more options. Use when picks aren't exclusive.
Receipt options
What to include on customer-facing receipts.
Type · radio card
Same selection model as radio button, with richer per-option content. Lay out horizontally on desktop, stack on mobile.
Repayment plan
Pick the plan that fits your daily revenue.
Header
Header is optional. Use a label whenever the question isn't already obvious from the page; add the subline when the label needs a constraint, a price, or a soft hint that doesn't fit on each item.
Label + subline
POS menu visibility
Show the product description in the POS menu.
Label only
POS menu visibility
No header
Use only when the question is already established by surrounding structure — a section heading, a wizard step.
Disabled
POS menu visibility
Available once your POS is connected.
Error
When the user submits without picking (or with an invalid pick), the choice list flips into error mode: every item gets the danger ring, and a single inline message appears at the bottom. Don't paint required-but-untouched groups red preemptively.
Error · radio button
Pickup or delivery
Pick one to continue.
Choose pickup or delivery to continue.
Apply the error tone to the whole group at once. The inline message sits below the last option with a 20 px error icon and a single line of helper copy in red.900.
Behavior
- Selection model is fixed by type. Radio button + radio card = mutually exclusive, one always selected. Checkbox = multi-select, zero or more. Don't mix types in the same list.
- Whole row is the click target. Clicking the title, the help text, or the radio card's body all toggle the selection — same as clicking the mark. Hit area extends the row width.
- Error tone applies to the whole group. When validation fails, every item ring goes red and a single inline message names the problem. Don't mark just one item — the user picked nothing, not the wrong thing.
- Radio cards lay out horizontally on desktop, stack on mobile. They need room to breathe — three lines of supporting text and a ~20 px radio mark per card. Below 600 px viewport, the cards stack and take the full row.
- Default to a sensible pick for radio types. The list never ships empty. For checkboxes, default state is whatever the user last saved (or all-off, never all-on for opt-ins).
Accessibility
- Wrap in
<fieldset>with<legend>so screen readers announce the question before the options. The label and subline both live inside the legend; the subline gets a visually-hidden colon between them. - Native inputs underneath.
<input type="radio">with a sharedname, or<input type="checkbox">for the multi case. The browser handles arrow-key nav (radios) and Space-to-toggle (checkboxes) for free. - Subline links via
aria-describedbyon the fieldset, pointing at the subline's id. Screen readers announce it together with the legend. - Error linking:
aria-invalid="true"on the fieldset andaria-describedbyextended to include the error message. Wrap the message inaria-live="polite"so it announces when it appears. - Disabled options stay in the DOM with
aria-disabled="true"so screen readers announce them. Pair with helper copy explaining why — “Available once your POS is connected.” - Tap target ≥ 44 px. The mark is 20 px; the row stretches the click area to the full row height + width.
Code
One ChoiceList wrapper, three item primitives. The wrapper handles header, error, and the fieldset/legend semantics; the items handle their own marks and labels.
tsx
import { ChoiceList, Radio, Checkbox, RadioCard } from "@flatpay-dk/ui";
// Radio buttons
const [schedule, setSchedule] = useState("next");
<ChoiceList
label="Payout schedule"
subline="When the daily settlement should land."
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="Lands by 09:00 the morning after." />
<Radio value="weekly" title="Weekly" help="Bundled into one transfer every Monday." />
</ChoiceList>
// Checkboxes (multi-select)
const [options, setOptions] = useState(["vat", "policy"]);
<ChoiceList
label="Receipt options"
subline="What to include on customer-facing receipts."
value={options}
onValueChange={setOptions}
type="checkbox"
>
<Checkbox value="vat" title="Include VAT breakdown" />
<Checkbox value="lines" title="Include itemised lines" />
<Checkbox value="policy" title="Include refund policy" />
</ChoiceList>
// Radio cards (richer per-option content)
<ChoiceList
label="Repayment plan"
subline="Pick the plan that fits your daily revenue."
value={plan}
onValueChange={setPlan}
orientation="horizontal"
>
<RadioCard
value="17"
title="17% of daily sales"
supporting="about 7 months to complete repayments"
meta="75.000 kr service fee"
/>
<RadioCard value="15" title="15% of daily sales" supporting="…" meta="…" />
<RadioCard value="12" title="12% of daily sales" supporting="…" meta="…" />
</ChoiceList>
// Error state
<ChoiceList
label="Pickup or delivery"
value={method}
onValueChange={setMethod}
error="Choose pickup or delivery to continue."
>
<Radio value="pickup" title="Pickup" />
<Radio value="delivery" title="Delivery" />
</ChoiceList>Best practices
Choice lists carry a lot of forms. Small habits compound into predictable, scannable surfaces.
Payout schedule
When the daily settlement should land.
Do
Lead with the question. The label is the first thing the user reads — make it answerable in one line.
Choose how often you want your daily payout to be deposited into your linked bank account
Don't
Don't dump instructions into the label. The label is a question; instructions go in the subline (one line) or page copy above.
Do
Use radio cards when each option needs three lines of supporting copy. The card carries a price, a duration, a constraint.
Don't
Don't mix item types in one list. A checkbox next to two radios reads as a bug, not a feature.
Pickup or delivery
Choose pickup or delivery to continue.
Do
One inline error per group, named action — 'Choose pickup or delivery'. The user knows what to do next.
Pickup or delivery (required)
Required field
Don't
Don't preemptively paint required groups red. Save the error tone for after the user submits without picking.
Props
ChoiceList
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | — | The question. Inter Tight Medium 16/24. Renders as <legend>. |
| subline | string | — | Optional one-line clarification. Inter Tight Regular 14/20 in text.secondary. Linked via aria-describedby. |
| type | "radio" | "checkbox" | "radio-card" | "radio" | Selection model. radio + radio-card are mutually exclusive; checkbox is multi-select. |
| orientation | "vertical" | "horizontal" | "vertical" | Item layout. Vertical for radio + checkbox; horizontal is reserved for radio cards on desktop (stacks below 600 px). |
| value | string | string[] | — | Currently selected value (radio) or values (checkbox). Make the list controlled — never internally empty for radio types. |
| onValueChange | (value: string | string[]) => void | — | Fires on selection change. Receives the new value(s), not the event. |
| error | string | — | Helper text in danger tone below the last item. Pairs every item with the error border. |
| name | string | — | Form field name. Required for form submission; auto-generated if omitted. |
| disabled | boolean | false | Disables every item. For per-item disable, use disabled on the individual Radio/Checkbox/RadioCard. |