On this pageOverview
Radio Group
A single-selection component with roving tabindex keyboard navigation. Arrow keys simultaneously move focus and select the option — there is no separate focus-then-select step. RadioGroup uses the Submodel pattern and supports both vertical and horizontal orientation.
See it in an app
Check out how RadioGroup is wired up in a real Foldkit app.
The view function is generic over your option type. Pass a typed options array and an optionToConfig callback that maps each option to a value and a content callback receiving attribute groups.
12GB / 6 CPUs — Perfect for small projects
16GB / 8 CPUs — For growing teams
32GB / 12 CPUs — Dedicated infrastructure
// Pseudocode walkthrough of the Foldkit integration points. Each labeled
// block below is an excerpt — fit them into your own Model, init, Message,
// update, and view definitions.
import { Effect } from 'effect'
import { Command, Ui } from 'foldkit'
import { m } from 'foldkit/message'
import { evo } from 'foldkit/struct'
import { Class, div, p, span } from './html'
// Add a field to your Model for the RadioGroup Submodel:
const Model = S.Struct({
radioGroup: Ui.RadioGroup.Model,
// ...your other fields
})
// In your init function, initialize the RadioGroup Submodel with a unique id:
const init = () => [
{
radioGroup: Ui.RadioGroup.init({ id: 'plan' }),
// ...your other fields
},
[],
]
// Embed the RadioGroup Message in your parent Message:
const GotRadioGroupMessage = m('GotRadioGroupMessage', {
message: Ui.RadioGroup.Message,
})
// Inside your update function's M.tagsExhaustive({...}), delegate to RadioGroup.update:
GotRadioGroupMessage: ({ message }) => {
const [nextRadioGroup, commands] = Ui.RadioGroup.update(
model.radioGroup,
message,
)
return [
// Merge the next state into your Model:
evo(model, { radioGroup: () => nextRadioGroup }),
// Forward the Submodel's Commands through your parent Message:
commands.map(
Command.mapEffect(
Effect.map(message => GotRadioGroupMessage({ message })),
),
),
]
}
type Plan = 'Startup' | 'Business' | 'Enterprise'
const plans: ReadonlyArray<Plan> = ['Startup', 'Business', 'Enterprise']
const descriptions: Record<Plan, string> = {
Startup: '12GB / 6 CPUs — Perfect for small projects',
Business: '16GB / 8 CPUs — For growing teams',
Enterprise: '32GB / 12 CPUs — Dedicated infrastructure',
}
// Inside your view function, render the radio group:
Ui.RadioGroup.view<Message, Plan>({
model: model.radioGroup,
toParentMessage: message => GotRadioGroupMessage({ message }),
options: plans,
ariaLabel: 'Server plan',
optionToConfig: (plan, { isSelected }) => ({
value: plan,
content: attributes =>
div(
[
...attributes.option,
Class(
'rounded-lg border p-4 cursor-pointer data-[checked]:border-blue-600',
),
],
[
span([...attributes.label, Class('text-sm font-medium')], [plan]),
p(
[...attributes.description, Class('text-sm text-gray-500')],
[descriptions[plan]],
),
],
),
}),
attributes: [Class('flex flex-col gap-3')],
})Pass orientation: 'Horizontal' to switch to left/right arrow navigation. Set the orientation at init time or override it per render in the view config.
12GB / 6 CPUs — Perfect for small projects
16GB / 8 CPUs — For growing teams
32GB / 12 CPUs — Dedicated infrastructure
RadioGroup is headless — the optionToConfig callback controls all option markup and styling. Use the data attributes below to style selected, focused, and disabled states.
| Attribute | Condition |
|---|---|
data-checked | Present on the selected option. |
data-active | Present on the option that has focus (roving tabindex). |
data-disabled | Present on disabled options. |
RadioGroup uses roving tabindex — only the active option is in the tab order. Arrow keys move focus and select simultaneously. Disabled options are skipped during keyboard navigation.
| Key | Description |
|---|---|
| Arrow Down / Right | Move focus and select the next option (wraps). |
| Arrow Up / Left | Move focus and select the previous option (wraps). |
| Home | Move focus and select the first option. |
| End | Move focus and select the last option. |
| Space | Select the focused option. |
The group element receives role="radiogroup" and aria-orientation. Each option receives role="radio" with aria-checked, aria-labelledby, and aria-describedby.
Configuration object passed to RadioGroup.init().
| Name | Type | Default | Description |
|---|---|---|---|
id | string | - | Unique ID for the radio group instance. |
selectedValue | string | - | Initially selected option value. |
orientation | 'Vertical' | 'Horizontal' | 'Vertical' | Layout orientation. Controls which arrow keys navigate between options. |
Configuration object passed to RadioGroup.view().
| Name | Type | Default | Description |
|---|---|---|---|
model | RadioGroup.Model | - | The radio group state from your parent Model. |
toParentMessage | (childMessage: RadioGroup.Message) => ParentMessage | - | Wraps RadioGroup Messages in your parent Message type for Submodel delegation. |
options | ReadonlyArray<RadioOption> | - | The list of options. The generic RadioOption type narrows the value passed to optionToConfig. |
optionToConfig | (option, context) => OptionConfig | - | Maps each option to its value and content callback. The context provides isSelected, isActive, and isDisabled. |
ariaLabel | string | - | Accessible label for the radio group. |
orientation | 'Vertical' | 'Horizontal' | - | Overrides the orientation set at init. Controls arrow key direction and aria-orientation. |
isDisabled | boolean | false | Disables all options. |
isOptionDisabled | (option, index) => boolean | - | Disables individual options. |
onSelected | (value, index) => Message | - | Alternative to Submodel delegation — fires your own Message on selection instead of the internal SelectedOption. Use with RadioGroup.select() to update the Model. |
name | string | - | Form field name. When provided, a hidden input is included with the selected value. |
attributes | ReadonlyArray<Attribute<Message>> | - | Additional attributes for the radio group container. |
className | string | - | CSS class for the radio group container. |
Attribute groups provided to each option’s content callback.
| Name | Type | Default | Description |
|---|---|---|---|
option | ReadonlyArray<Attribute<Message>> | - | Spread onto the radio option element. Includes role, aria-checked, tabindex, and click/keyboard handlers. |
label | ReadonlyArray<Attribute<Message>> | - | Spread onto the label element. Includes an id for aria-labelledby. |
description | ReadonlyArray<Attribute<Message>> | - | Spread onto a description element. Includes an id for aria-describedby. |