Svelte 5 component · fractalsvelte-ui · Forms
RadioGroup
A semantic radiogroup container that coordinates single-option selection among child radio items. Manages value selection through a bindable value and provides full keyboard arrow navigation across options.
Install
RadioGroup is a Svelte 5 radio group component in @fractaldesign/fractalsvelte-ui. It works in SvelteKit and in any Svelte 5 app.
pnpm add @fractaldesign/fractalsvelte-uiImport
import { RadioGroup } from '@fractaldesign/fractalsvelte-ui';Usage
<RadioGroup>Click me</RadioGroup>Props
Also accepts every attribute of HTMLAttributes, spread onto
the root element.
| Name | Type | Default |
|---|---|---|
value bindable | string | number | undefined |
name | string | — |
label | string | — |
description | string | — |
helperText | string | — |
error | boolean | string | — |
orientation | Orientation 'horizontal' | 'vertical' | 'vertical' |
options | RadioOption[] | [] |
disabled | boolean | false |
size | Size 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' |
class | string | '' |
Snippets
| Name | Type | Default |
|---|---|---|
children | Snippet | — |
Source
Show the RadioGroup.svelte source
<script lang="ts">
import { setContext, type Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
import type { Size, Orientation } from '../../types';
import Radio from './Radio.svelte';
export interface RadioOption {
value: string | number;
label: string;
description?: string;
disabled?: boolean;
}
interface Props extends HTMLAttributes<HTMLDivElement> {
value?: string | number;
name?: string;
label?: string;
description?: string;
helperText?: string;
error?: boolean | string;
orientation?: Orientation;
options?: RadioOption[];
disabled?: boolean;
size?: Size;
children?: Snippet;
class?: string;
}
let {
value = $bindable(undefined),
name,
label,
description,
helperText,
error,
orientation = 'vertical',
options = [],
disabled = false,
size = 'md',
children,
class: className = '',
...restProps
}: Props = $props();
const autoName = 'f-rad-grp-' + Math.random().toString(36).slice(2, 9);
const groupName = $derived(name || autoName);
const hasError = $derived(Boolean(error));
const errorMessage = $derived(typeof error === 'string' ? error : undefined);
// Context for child Radio components
setContext('f-radio-group', {
name: () => groupName,
value: () => value,
disabled: () => disabled,
size: () => size,
select: (val: string | number) => {
value = val;
}
});
function handleKeyDown(e: KeyboardEvent) {
if (options.length === 0 || disabled) return;
const enabledOptions = options.filter((o) => !o.disabled);
if (enabledOptions.length === 0) return;
const currentIndex = enabledOptions.findIndex((o) => o.value === value);
if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
e.preventDefault();
const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % enabledOptions.length;
value = enabledOptions[nextIndex].value;
} else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
e.preventDefault();
const prevIndex =
currentIndex === -1
? enabledOptions.length - 1
: (currentIndex - 1 + enabledOptions.length) % enabledOptions.length;
value = enabledOptions[prevIndex].value;
}
}
</script>
<div
class="f-radio-group-container {className}"
data-orientation={orientation}
data-size={size}
data-disabled={disabled || undefined}
data-invalid={hasError || undefined}
{...restProps}
>
{#if label}
<span class="f-radio-group-label" id="{groupName}-label">
{label}
</span>
{/if}
{#if description}
<span class="f-radio-group-desc" id="{groupName}-desc">
{description}
</span>
{/if}
<div
class="f-radio-group"
role="radiogroup"
aria-labelledby={label ? `${groupName}-label` : undefined}
aria-describedby={description ? `${groupName}-desc` : undefined}
aria-orientation={orientation}
tabindex="0"
onkeydown={handleKeyDown}
>
{#if options.length > 0}
{#each options as opt}
<Radio
value={opt.value}
label={opt.label}
description={opt.description}
disabled={disabled || opt.disabled}
name={groupName}
/>
{/each}
{/if}
{#if children}
{@render children()}
{/if}
</div>
{#if errorMessage}
<p class="f-radio-group-error-msg" role="alert">{errorMessage}</p>
{:else if helperText}
<p class="f-radio-group-helper-msg">{helperText}</p>
{/if}
</div>
<style>
@layer fractalsvelte-ui.components {
.f-radio-group-container {
display: flex;
flex-direction: column;
gap: var(--f-space-xs);
font-family: inherit;
box-sizing: border-box;
}
.f-radio-group-label {
font-size: var(--f-text-sm);
font-weight: 600;
color: var(--f-text);
user-select: none;
}
.f-radio-group-desc {
font-size: var(--f-text-xs);
color: var(--f-text-muted);
margin-top: -2px;
}
.f-radio-group {
display: flex;
gap: var(--f-space-sm);
outline: none;
}
.f-radio-group-container[data-orientation='vertical'] .f-radio-group {
flex-direction: column;
}
.f-radio-group-container[data-orientation='horizontal'] .f-radio-group {
flex-direction: row;
flex-wrap: wrap;
align-items: center;
}
.f-radio-group:focus-visible {
outline: 2px solid var(--f-ring);
outline-offset: 4px;
border-radius: var(--f-radius-sm);
}
.f-radio-group-helper-msg {
margin: 0;
font-size: var(--f-text-xs);
color: var(--f-text-muted);
}
.f-radio-group-error-msg {
margin: 0;
font-size: var(--f-text-xs);
color: var(--f-danger-text);
font-weight: 500;
}
.f-radio-group-container[data-disabled='true'] {
opacity: 0.6;
cursor: not-allowed;
}
}
</style>Related components
RangeSlider A dual-thumb slider control designed for selecting lower and upper numeric boundaries.RotaryDial A precision hardware-grade rotational dial and knob control with 270-degree arc tracking, circular tick…SearchInput A specialized search field pre-configured with search iconography, quick clear action, and keyboard shortcut…Select A custom dropdown selection control featuring search filtering, clear actions, and complete keyboard…Slider A precision numeric range slider featuring a styled track, fill bar, and draggable thumb.Switch A tactile toggle switch control featuring smooth sliding spring transitions and optional track or thumb…