Radio Group
A set of options where only one can be picked.
Native <input type="radio">
Selected: comfortable
<script lang="ts">
import { Label } from '#lib/components/ui/label/index.js';
import * as RadioGroup from '#lib/components/ui/radio-group/index.js';
let plan = $state('comfortable');
</script>
<RadioGroup.Root bind:value={plan} aria-label="Spacing">
<Label><RadioGroup.Item value="default" /> Default</Label>
<Label><RadioGroup.Item value="comfortable" /> Comfortable</Label>
<Label><RadioGroup.Item value="compact" /> Compact</Label>
</RadioGroup.Root>
<p>Selected: {plan}</p>
<style>
p {
margin: var(--dui-space-3) 0 0;
color: var(--dui-color-muted-foreground);
font-size: var(--dui-text-sm);
}
</style>
Installation
npx distill-ui add radio-group- Set up the tokens once, if you haven't: see manual installation.
- Copy these files into your project:
import { getContext, setContext } from 'svelte';
// The group's shared state. Items are native radio inputs that share one `name`, so the
// browser handles arrow keys and tab order; the context only keeps `value` in sync.
type RadioGroupContext = { name: string; value: string; disabled: boolean };
const key = Symbol('distill-radio-group');
export const setRadioGroupContext = (ctx: RadioGroupContext) => setContext(key, ctx);
export const getRadioGroupContext = () => getContext<RadioGroupContext>(key);
import Item from './radio-group-item.svelte';
import Root from './radio-group.svelte';
export { Root, Item };
<script lang="ts">
import type { HTMLInputAttributes } from 'svelte/elements';
import { getRadioGroupContext } from './context.js';
let {
ref = $bindable(null),
value,
disabled = false,
class: className,
...restProps
}: Omit<HTMLInputAttributes, 'type' | 'value' | 'name' | 'checked'> & {
ref?: HTMLInputElement | null;
value: string;
} = $props();
const group = getRadioGroupContext();
</script>
<input
{...restProps}
type="radio"
bind:this={ref}
name={group.name}
{value}
checked={group.value === value}
disabled={group.disabled || disabled}
onchange={() => (group.value = value)}
class={['item', className]}
/>
<!--
Options you can set from a parent or on the radio group item itself:
--dui-radio-size, --dui-radio-border, --dui-radio-bg, --dui-radio-checked-fg, --dui-radio-checked-bg
-->
<style>
.item {
/* The size is reused below for the mark, so it is set once here. */
--size: var(--dui-radio-size, 1rem);
appearance: none;
display: inline-grid;
place-content: center;
flex-shrink: 0;
width: var(--size);
height: var(--size);
margin: 0;
border: 1px solid var(--dui-radio-border, var(--dui-color-input));
border-radius: var(--dui-radius-full);
background: var(--dui-radio-bg, var(--dui-color-control));
box-shadow: var(--dui-shadow-xs);
cursor: pointer;
outline: none;
transition: box-shadow var(--dui-duration-fast) var(--dui-ease-out);
/* The dot. */
&::before {
content: '';
width: calc(var(--size) / 2);
height: calc(var(--size) / 2);
border-radius: var(--dui-radius-full);
background: var(--dui-radio-checked-fg, var(--dui-color-primary-foreground));
scale: 0;
}
&:checked {
border-color: var(--dui-radio-checked-bg, var(--dui-color-primary));
background: var(--dui-radio-checked-bg, var(--dui-color-primary));
&::before {
scale: 1;
}
}
&:focus-visible {
border-color: var(--dui-color-ring);
box-shadow: 0 0 0 var(--dui-ring-width)
color-mix(in oklch, var(--dui-color-ring) 50%, transparent);
}
&[aria-invalid='true'] {
border-color: var(--dui-color-destructive);
box-shadow: 0 0 0 var(--dui-ring-width)
color-mix(in oklch, var(--dui-color-destructive) 20%, transparent);
}
&:disabled {
cursor: not-allowed;
opacity: 0.5;
}
}
</style>
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { setRadioGroupContext } from './context.js';
let {
value = $bindable(''),
name,
disabled = false,
orientation = 'vertical',
class: className,
children,
...restProps
}: HTMLAttributes<HTMLDivElement> & {
value?: string;
name?: string;
disabled?: boolean;
orientation?: 'horizontal' | 'vertical';
} = $props();
const id = $props.id();
setRadioGroupContext({
get name() {
return name ?? `radio-group-${id}`;
},
get value() {
return value;
},
set value(v) {
value = v;
},
get disabled() {
return disabled;
}
});
</script>
<div
{...restProps}
role="radiogroup"
aria-disabled={disabled || undefined}
aria-orientation={orientation}
class={['radio-group', className]}
data-orientation={orientation}
>
{@render children?.()}
</div>
<!--
Options you can set from a parent or on the radio group itself:
--dui-radio-group-gap
-->
<style>
.radio-group {
display: grid;
gap: var(--dui-radio-group-gap, var(--dui-space-3));
&[data-orientation='horizontal'] {
display: flex;
flex-wrap: wrap;
}
}
</style>
Customizing
Set any of these on the component or on a parent element. See Customizing for how they work.
| Custom property | Default |
|---|---|
--dui-radio-size | 1rem |
--dui-radio-border | var(--dui-color-input) |
--dui-radio-bg | var(--dui-color-control) |
--dui-radio-checked-fg | var(--dui-color-primary-foreground) |
--dui-radio-checked-bg | var(--dui-color-primary) |
--dui-radio-group-gap | var(--dui-space-3) |