A set of checkable buttons, known as radio buttons, where no more than one of the buttons can be checked at a time.
Installation
Install the dependencies
pnpm add cn class-variance-authority github:sveltery/baseCopy the component
Copy the following files into src/lib/components/ui/radio-group.
<script lang="ts">
import { RadioGroup as Primitive, type RadioGroupProps } from '@sveltery/base/radio-group';
import { cn } from 'cn';
let { children, class: classProp, value = $bindable(), ...props }: RadioGroupProps = $props();
</script>
<Primitive data-slot="radio-group" bind:value {...props} class={cn('grid gap-2 w-full', classProp)}>
{@render children?.()}
</Primitive><script lang="ts">
import { Radio as Primitive, type RadioRootProps } from '@sveltery/base/radio';
import { cn } from 'cn';
let { class: classProp, ...props }: Omit<RadioRootProps, 'children'> = $props();
</script>
<Primitive.Root
data-slot="radio-group-item"
{...props}
class={cn('border-input dark:bg-input/30 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 dark:aria-invalid:border-destructive/50 flex size-4 rounded-full focus-visible:ring-3 aria-invalid:ring-3 group-has-[:focus-visible]/field-label:ring-0 group-has-[:focus-visible]/field-label:not-data-checked:border-input group-has-[:focus-visible]/field-label:data-checked:border-primary group/radio-group-item peer relative aspect-square shrink-0 border outline-none after:absolute after:-inset-x-3 after:-inset-y-2 disabled:cursor-not-allowed disabled:opacity-50', classProp)}
>
<Primitive.Indicator data-slot="radio-group-indicator" class="flex size-4 items-center justify-center">
<span class="bg-primary-foreground absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 rounded-full"></span>
</Primitive.Indicator>
</Primitive.Root>export { default as RadioGroup } from './RadioGroup.svelte';
export { default as RadioGroupItem } from './RadioGroupItem.svelte';
export type { RadioGroupProps, RadioGroupState } from '@sveltery/base/radio-group';
export type { RadioRootProps as RadioGroupItemProps } from '@sveltery/base/radio';Update the import paths
Change the imports to match your project setup.
Usage
<script lang="ts">
import { RadioGroup, RadioGroupItem } from '$lib/components/ui/radio-group';
import { Label } from '$lib/components/ui/label';
</script><RadioGroup name="density" defaultValue="comfortable">
<div class="flex items-center gap-2">
<RadioGroupItem value="default" id="r1" />
<Label for="r1">Default</Label>
</div>
<div class="flex items-center gap-2">
<RadioGroupItem value="comfortable" id="r2" />
<Label for="r2">Comfortable</Label>
</div>
</RadioGroup>Bind value to read or change the selection.
<script lang="ts">
let density = $state("comfortable");
</script>
<RadioGroup bind:value={density}>...</RadioGroup>API reference
RadioGroup is styled on top of the Sveltery Base RadioGroup and Radio primitives, which own the selection, arrow-key navigation and the hidden inputs that take part in form submission. RadioGroup keeps the HTML form semantics: name submits the checked value, defaultValue sets the initial selection, and bind:value shares it with the parent. See the Base UI Radio Group documentation and the Radio documentation for the behavior they port.