Radio Group
Allows users to select a single option from a list of mutually exclusive choices.
<script lang="ts">
import { Label, RadioGroup } from "bits-ui";
</script>
<RadioGroup.Root class="flex flex-col gap-4 text-sm font-medium">
<div
class="text-foreground group flex select-none items-center transition-all"
>
<RadioGroup.Item
id="amazing"
value="amazing"
class="border-border-input bg-background hover:border-dark-40 data-[state=checked]:border-6 data-[state=checked]:border-foreground size-5 shrink-0 cursor-default rounded-full border transition-all duration-100 ease-in-out"
/>
<Label.Root for="amazing" class="pl-3">Amazing</Label.Root>
</div>
<div
class="text-foreground group flex select-none items-center transition-all"
>
<RadioGroup.Item
id="average"
value="average"
class="border-border-input bg-background hover:border-dark-40 data-[state=checked]:border-6 data-[state=checked]:border-foreground size-5 shrink-0 cursor-default rounded-full border transition-all duration-100 ease-in-out"
/>
<Label.Root for="average" class="pl-3">Average</Label.Root>
</div>
<div
class="text-foreground group flex select-none items-center transition-all"
>
<RadioGroup.Item
id="terrible"
value="terrible"
class="border-border-input bg-background hover:border-dark-40 data-[state=checked]:border-6 data-[state=checked]:border-foreground size-5 shrink-0 cursor-default rounded-full border transition-all duration-100 ease-in-out"
/>
<Label.Root for="terrible" class="pl-3">Terrible</Label.Root>
</div>
</RadioGroup.Root>
@import url("https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap");
@import "tailwindcss";
@plugin "tailwindcss-animate";
@custom-variant dark (&:is(.dark *));
@font-face {
font-family: "Cal Sans";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("/CalSans-SemiBold.woff2") format("woff2");
}
:root {
/* Colors */
--background: hsl(0 0% 100%);
--background-alt: hsl(0 0% 100%);
--foreground: hsl(0 0% 9%);
--foreground-alt: hsl(0 0% 32%);
--muted: hsl(240 5% 96%);
--muted-foreground: hsla(0 0% 9% / 0.4);
--border: hsl(240 6% 10%);
--border-input: hsla(240 6% 10% / 0.17);
--border-input-hover: hsla(240 6% 10% / 0.4);
--border-card: hsla(240 6% 10% / 0.1);
--dark: hsl(240 6% 10%);
--dark-10: hsla(240 6% 10% / 0.1);
--dark-40: hsla(240 6% 10% / 0.4);
--dark-04: hsla(240 6% 10% / 0.04);
--accent: hsl(204 94% 94%);
--accent-foreground: hsl(204 80% 16%);
--destructive: hsl(347 77% 50%);
--tertiary: hsl(37.7 92.1% 50.2%);
--line: hsl(0 0% 100%);
/* black */
--contrast: hsl(0 0% 0%);
/* Shadows */
--shadow-mini: 0px 1px 0px 1px rgba(0, 0, 0, 0.04);
--shadow-mini-inset: 0px 1px 0px 0px rgba(0, 0, 0, 0.04) inset;
--shadow-popover: 0px 7px 12px 3px hsla(var(--dark-10));
--shadow-kbd: 0px 2px 0px 0px rgba(0, 0, 0, 0.07);
--shadow-btn: 0px 1px 0px 1px rgba(0, 0, 0, 0.03);
--shadow-card: 0px 2px 0px 1px rgba(0, 0, 0, 0.04);
--shadow-date-field-focus: 0px 0px 0px 3px rgba(24, 24, 27, 0.17);
}
.dark {
/* Colors */
--background: hsl(0 0% 5%);
--background-alt: hsl(0 0% 8%);
--foreground: hsl(0 0% 95%);
--foreground-alt: hsl(0 0% 70%);
--muted: hsl(240 4% 16%);
--muted-foreground: hsla(0 0% 100% / 0.4);
--border: hsl(0 0% 96%);
--border-input: hsla(0 0% 96% / 0.17);
--border-input-hover: hsla(0 0% 96% / 0.4);
--border-card: hsla(0 0% 96% / 0.1);
--dark: hsl(0 0% 96%);
--dark-40: hsl(0 0% 96% / 0.4);
--dark-10: hsl(0 0% 96% / 0.1);
--dark-04: hsl(0 0% 96% / 0.04);
--accent: hsl(204 90% 90%);
--accent-foreground: hsl(204 94% 94%);
--destructive: hsl(350 89% 60%);
--line: hsl(0 0% 9.02%);
--tertiary: hsl(61.3 100% 82.2%);
/* white */
--contrast: hsl(0 0% 100%);
/* Shadows */
--shadow-mini: 0px 1px 0px 1px rgba(0, 0, 0, 0.3);
--shadow-mini-inset: 0px 1px 0px 0px rgba(0, 0, 0, 0.5) inset;
--shadow-popover: 0px 7px 12px 3px hsla(0deg 0% 0% / 30%);
--shadow-kbd: 0px 2px 0px 0px rgba(255, 255, 255, 0.07);
--shadow-btn: 0px 1px 0px 1px rgba(0, 0, 0, 0.2);
--shadow-card: 0px 2px 0px 1px rgba(0, 0, 0, 0.4);
--shadow-date-field-focus: 0px 0px 0px 3px rgba(244, 244, 245, 0.1);
}
@theme inline {
--color-background: var(--background);
--color-background-alt: var(--background-alt);
--color-foreground: var(--foreground);
--color-foreground-alt: var(--foreground-alt);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-border: var(--border-card);
--color-border-input: var(--border-input);
--color-border-input-hover: var(--border-input-hover);
--color-border-card: var(--border-card);
--color-dark: var(--dark);
--color-dark-10: var(--dark-10);
--color-dark-40: var(--dark-40);
--color-dark-04: var(--dark-04);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-tertiary: var(--tertiary);
--color-line: var(--line);
--color-contrast: var(--contrast);
--shadow-mini: var(--shadow-mini);
--shadow-mini-inset: var(--shadow-mini-inset);
--shadow-popover: var(--shadow-popover);
--shadow-kbd: var(--shadow-kbd);
--shadow-btn: var(--shadow-btn);
--shadow-card: var(--shadow-card);
--shadow-date-field-focus: var(--shadow-date-field-focus);
--text-xxs: 10px;
--radius-card: 16px;
--radius-card-lg: 20px;
--radius-card-sm: 10px;
--radius-input: 9px;
--radius-button: 5px;
--radius-5px: 5px;
--radius-9px: 9px;
--radius-10px: 10px;
--radius-15px: 15px;
--spacing-input: 3rem;
--spacing-input-sm: 2.5rem;
--breakpoint-desktop: 1440px;
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
--animate-caret-blink: caret-blink 1s ease-out infinite;
--animate-scale-in: scale-in 0.2s ease;
--animate-scale-out: scale-out 0.15s ease;
--animate-fade-in: fade-in 0.2s ease;
--animate-fade-out: fade-out 0.15s ease;
--animate-enter-from-left: enter-from-left 0.2s ease;
--animate-enter-from-right: enter-from-right 0.2s ease;
--animate-exit-to-left: exit-to-left 0.2s ease;
--animate-exit-to-right: exit-to-right 0.2s ease;
--font-sans: "Inter", "sans-serif";
--font-mono: "Source Code Pro", "monospace";
--font-alt: "Courier", "sans-serif";
--font-display: "Cal Sans", "sans-serif";
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--bits-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--bits-accordion-content-height);
}
to {
height: 0;
}
}
@keyframes caret-blink {
0%,
70%,
100% {
opacity: 1;
}
20%,
50% {
opacity: 0;
}
}
@keyframes enter-from-right {
from {
opacity: 0;
transform: translateX(200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes enter-from-left {
from {
opacity: 0;
transform: translateX(-200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes exit-to-right {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(200px);
}
}
@keyframes exit-to-left {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(-200px);
}
}
@keyframes scale-in {
from {
opacity: 0;
transform: rotateX(-10deg) scale(0.9);
}
to {
opacity: 1;
transform: rotateX(0deg) scale(1);
}
}
@keyframes scale-out {
from {
opacity: 1;
transform: rotateX(0deg) scale(1);
}
to {
opacity: 0;
transform: rotateX(-10deg) scale(0.95);
}
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
}
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-border-card, currentColor);
}
* {
@apply border-border;
}
html {
-webkit-text-size-adjust: 100%;
font-variation-settings: normal;
scrollbar-color: var(--bg-muted);
}
body {
@apply bg-background text-foreground;
font-feature-settings:
"rlig" 1,
"calt" 1;
}
::selection {
background: #fdffa4;
color: black;
}
}
@layer components {
*:not(body):not(.focus-override) {
outline: none !important;
&:focus-visible {
@apply focus-visible:ring-foreground focus-visible:ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-1;
}
}
.link {
@apply hover:text-foreground/80 focus-visible:ring-foreground focus-visible:ring-offset-background rounded-xs focus-visible:outline-hidden inline-flex items-center gap-1 font-medium underline underline-offset-4 focus-visible:ring-2 focus-visible:ring-offset-2;
}
}
Structure
<script lang="ts">
import { RadioGroup } from "bits-ui";
</script>
<RadioGroup.Root>
<RadioGroup.Item>
{#snippet children({ checked })}
{#if checked}
✅
{/if}
{/snippet}
</RadioGroup.Item>
</RadioGroup.Root>
Reusable Components
It's recommended to use the RadioGroup primitives to create your own custom components that can be used throughout your application.
In the example below, we're creating a custom MyRadioGroup component that takes in an array of items and renders a radio group with those items along with a Label component for each item.
<script lang="ts">
import { RadioGroup, Label, type WithoutChildrenOrChild, useId } from "bits-ui";
type Item = {
value: string;
label: string;
disabled?: boolean;
};
type Props = WithoutChildrenOrChild<RadioGroup.RootProps> & {
items: Item[];
};
let { value = $bindable(""), ref = $bindable(null), items, ...restProps }: Props = $props();
</script>
<RadioGroup.Root bind:value bind:ref {...restProps}>
{#each items as item}
{@const id = useId()}
<div>
<RadioGroup.Item {id} value={item.value} disabled={item.disabled}>
{#snippet children({ checked })}
{#if checked}
✅
{/if}
{/snippet}
</RadioGroup.Item>
<Label.Root for={id}>{item.label}</Label.Root>
</div>
{/each}
</RadioGroup.Root>
You can then use the MyRadioGroup component in your application like so:
<script lang="ts">
import MyRadioGroup from "$lib/components/MyRadioGroup.svelte";
const myItems = [
{ value: "apple", label: "Apple" },
{ value: "banana", label: "Banana" },
{ value: "coconut", label: "Coconut", disabled: true },
];
</script>
<MyRadioGroup items={myItems} name="favoriteFruit" />
Managing Value State
This section covers how to manage the value state of the component.
Two-Way Binding
Use bind:value for simple, automatic state synchronization:
<script lang="ts">
import { RadioGroup } from "bits-ui";
let myValue = $state("");
</script>
<button onclick={() => (myValue = "A")}> Select A </button>
<RadioGroup.Root bind:value={myValue}>
<!-- ... -->
</RadioGroup.Root>
Fully Controlled
Use a Function Binding for complete control over the state's reads and writes.
<script lang="ts">
import { RadioGroup } from "bits-ui";
let myValue = $state("");
function getValue() {
return myValue;
}
function setValue(newValue: string) {
myValue = newValue;
}
</script>
<RadioGroup.Root bind:value={getValue, setValue}>
<!-- ... -->
</RadioGroup.Root>
HTML Forms
If you set the name prop on the RadioGroup.Root component, a hidden input element will be rendered to submit the value of the radio group to a form.
<RadioGroup.Root name="favoriteFruit">
<!-- ... -->
</RadioGroup.Root>
Required
To make the hidden input element required you can set the required prop on the RadioGroup.Root component.
<RadioGroup.Root required>
<!-- ... -->
</RadioGroup.Root>
Disabling Items
You can disable a radio group item by setting the disabled prop to true.
<RadioGroup.Item value="apple" disabled>Apple</RadioGroup.Item>
Orientation
The orientation prop is used to determine the orientation of the radio group, which influences how keyboard navigation will work.
When the orientation is set to 'vertical', the radio group will navigate through the items using the ArrowUp and ArrowDown keys. When the orientation is set to 'horizontal', the radio group will navigate through the items using the ArrowLeft and ArrowRight keys.
<RadioGroup.Root orientation="vertical">
<!-- ... -->
</RadioGroup.Root>
<RadioGroup.Root orientation="horizontal">
<!-- ... -->
</RadioGroup.Root>
API Reference
The radio group component used to group radio items under a common name for form submission.
| Property | Type | Description |
|---|---|---|
value $bindable | string | The value of the currently selected radio item. You can bind to this value to control the radio group's value from outside the component. Default: undefined |
onValueChange | function | A callback that is fired when the radio group's value changes. Default: undefined |
disabled | boolean | Whether or not the radio group is disabled. This prevents the user from interacting with it. Default: false |
required | boolean | Whether or not the radio group is required. Default: false |
name | string | The name of the radio group used in form submission. If provided, a hidden input element will be rendered to submit the value of the radio group. Default: undefined |
loop | boolean | Whether or not the radio group should loop through the items when navigating with the arrow keys. Default: false |
orientation | enum | The orientation of the radio group. This will determine how keyboard navigation will work within the component. Default: 'vertical' |
ref $bindable | HTMLDivElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
| Data Attribute | Value | Description |
|---|---|---|
data-orientation | enum | The orientation of the radio group. |
data-radio-group-root | '' | Present on the root element. |
An radio item, which must be a child of the RadioGroup.Root component.
| Property | Type | Description |
|---|---|---|
value required | string | The value of the radio item. This should be unique for each radio item in the group. Default: undefined |
disabled | boolean | Whether the radio item is disabled. Default: false |
ref $bindable | HTMLButtonElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
| Data Attribute | Value | Description |
|---|---|---|
data-disabled | '' | Present when the radio item is disabled. |
data-value | '' | The value of the radio item. |
data-state | enum | The radio item's checked state. |
data-orientation | enum | The orientation of the parent radio group. |
data-radio-group-item | '' | Present on the radio item element. |