Radio Group
Pick exactly one option from a short list.
import { Label } from "@/components/ui/label";import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
export default function Demo() { return ( <RadioGroup defaultValue="weekly" className="w-fit"> {["daily", "weekly", "drift"].map((v) => ( <Label key={v} className="flex items-center gap-2.5 text-sm capitalize"> <RadioGroupItem value={v} /> {v === "drift" ? "On drift only" : v} </Label> ))} </RadioGroup> );}<script setup lang="ts">import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
const options = [ { value: "daily", label: "Daily" }, { value: "weekly", label: "Weekly" }, { value: "drift", label: "On drift only" },];</script>
<template> <RadioGroup default-value="weekly" class="w-fit"> <label v-for="o in options" :key="o.value" class="flex items-center gap-2.5 text-sm"> <RadioGroupItem :value="o.value" /> {{ o.label }} </label> </RadioGroup></template><script lang="ts"> import { RadioGroup, RadioGroupItem } from "$lib/components/ui/radio-group";
const options = [ { value: "daily", label: "Daily" }, { value: "weekly", label: "Weekly" }, { value: "drift", label: "On drift only" }, ];</script>
<RadioGroup value="weekly" class="w-fit"> {#each options as o (o.value)} <label class="flex items-center gap-2.5 text-sm"> <RadioGroupItem value={o.value} /> {o.label} </label> {/each}</RadioGroup>Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/radio-grouppnpm dlx shadcn@latest add @edmi-ui/radio-groupyarn dlx shadcn@latest add @edmi-ui/radio-groupbunx shadcn@latest add @edmi-ui/radio-groupPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install @base-ui/reactTerminal window pnpm add @base-ui/reactTerminal window yarn add @base-ui/reactTerminal window bun add @base-ui/reactCopy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/radio-grouppnpm dlx shadcn-vue@latest add @edmi-ui/radio-groupyarn dlx shadcn-vue@latest add @edmi-ui/radio-groupbunx shadcn-vue@latest add @edmi-ui/radio-groupPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install reka-ui @vueuse/coreTerminal window pnpm add reka-ui @vueuse/coreTerminal window yarn add reka-ui @vueuse/coreTerminal window bun add reka-ui @vueuse/coreCopy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/radio-group.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/radio-group.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/radio-group.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/radio-group.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Copy the code from Component source into your project and update the import paths to match your aliases.
import { Label } from "@/components/ui/label";import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
<RadioGroup defaultValue="weekly" className="w-fit"> {["daily", "weekly", "drift"].map((v) => ( <Label key={v} className="flex items-center gap-2.5 text-sm capitalize"> <RadioGroupItem value={v} /> {v === "drift" ? "On drift only" : v} </Label> ))}</RadioGroup><script setup lang="ts">import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";</script>
<template> <RadioGroup default-value="weekly" class="w-fit"> <label :key="o.value" class="flex items-center gap-2.5 text-sm"> <RadioGroupItem :value="o.value" /> {{ o.label }} </label> </RadioGroup></template><script lang="ts"> import { RadioGroup, RadioGroupItem } from "$lib/components/ui/radio-group";</script>
<RadioGroup value="weekly" class="w-fit"> {#each options as o (o.value)} <label class="flex items-center gap-2.5 text-sm"> <RadioGroupItem value={o.value} /> {o.label} </label> {/each}</RadioGroup>Component source
Section titled “Component source”import { Radio as RadioPrimitive } from "@base-ui/react/radio";import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group";import { cn } from "cn";
function RadioGroup({ className, ...props }: RadioGroupPrimitive.Props) { return ( <RadioGroupPrimitive data-slot="radio-group" className={cn("grid w-full gap-2", className)} {...props} /> );}
function RadioGroupItem({ className, ...props }: RadioPrimitive.Root.Props) { return ( <RadioPrimitive.Root data-slot="radio-group-item" className={cn( "group/radio-group-item peer relative flex aspect-square size-[18px] shrink-0 items-center justify-center rounded-full border border-input bg-card outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:shadow-ring disabled:cursor-not-allowed disabled:opacity-50 data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50 aria-invalid:border-destructive aria-invalid:shadow-ring-error data-[invalid]:border-destructive data-[invalid]:shadow-ring-error data-[checked]:border-primary group-has-disabled/field:opacity-50", className, )} {...props} > <RadioPrimitive.Indicator data-slot="radio-group-indicator" className="flex items-center justify-center" > <span data-indicator className="block size-[9px] rounded-full bg-primary" /> </RadioPrimitive.Indicator> </RadioPrimitive.Root> );}
export { RadioGroup, RadioGroupItem };<script setup lang="ts">import type { RadioGroupRootEmits, RadioGroupRootProps } from "reka-ui"import type { HTMLAttributes } from "vue"import { reactiveOmit } from "@vueuse/core"import { RadioGroupRoot, useForwardPropsEmits } from "reka-ui"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<RadioGroupRootProps & { class?: HTMLAttributes["class"] }>()const emits = defineEmits<RadioGroupRootEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)</script>
<template> <RadioGroupRoot v-slot="slotProps" data-slot="radio-group" :class="cn('grid w-full gap-2', props.class)" v-bind="forwarded" > <slot v-bind="slotProps" /> </RadioGroupRoot></template><script setup lang="ts">import type { RadioGroupItemProps } from "reka-ui"import type { HTMLAttributes } from "vue"import { reactiveOmit } from "@vueuse/core"import { RadioGroupIndicator, RadioGroupItem, useForwardProps } from "reka-ui"import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<RadioGroupItemProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)</script>
<template> <RadioGroupItem data-slot="radio-group-item" v-bind="forwardedProps" :class=" cn( 'group/radio-group-item peer relative flex aspect-square size-[18px] shrink-0 items-center justify-center rounded-full border border-input bg-card outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:shadow-ring disabled:cursor-not-allowed disabled:opacity-50 data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50 aria-invalid:border-destructive aria-invalid:shadow-ring-error data-[state=checked]:border-primary group-has-disabled/field:opacity-50', props.class, ) " > <RadioGroupIndicator data-slot="radio-group-indicator" class="flex items-center justify-center" > <slot> <span data-indicator class="block size-[9px] rounded-full bg-primary" /> </slot> </RadioGroupIndicator> </RadioGroupItem></template>export { default as RadioGroup } from "./RadioGroup.vue";export { default as RadioGroupItem } from "./RadioGroupItem.vue";<script lang="ts"> import { RadioGroup as RadioGroupPrimitive } from "bits-ui"; import { cn } from "$lib/utils.js";
let { ref = $bindable(null), class: className, value = $bindable(""), ...restProps }: RadioGroupPrimitive.RootProps = $props();</script>
<RadioGroupPrimitive.Root bind:ref bind:value data-slot="radio-group" class={cn("grid w-full gap-2", className)} {...restProps}/><script lang="ts"> import { RadioGroup as RadioGroupPrimitive } from "bits-ui"; import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
let { ref = $bindable(null), class: className, ...restProps }: WithoutChildrenOrChild<RadioGroupPrimitive.ItemProps> = $props();</script>
<RadioGroupPrimitive.Item bind:ref data-slot="radio-group-item" class={cn( "group/radio-group-item peer relative flex aspect-square size-[18px] shrink-0 items-center justify-center rounded-full border border-input bg-card outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:shadow-ring disabled:cursor-not-allowed disabled:opacity-50 data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50 aria-invalid:border-destructive aria-invalid:shadow-ring-error data-[state=checked]:border-primary group-has-disabled/field:opacity-50", className )} {...restProps}> {#snippet children({ checked })} <div data-slot="radio-group-indicator" class="flex items-center justify-center"> {#if checked} <span data-indicator class="block size-[9px] rounded-full bg-primary"></span> {/if} </div> {/snippet}</RadioGroupPrimitive.Item>import Root from "./radio-group.svelte";import Item from "./radio-group-item.svelte";
export { Item, Item as RadioGroupItem, Root, // Root as RadioGroup,};Elevation ✦
Section titled “Elevation ✦”See the Elevation guide for the levels, roles and layered mode.
Radio items stay flat; for choice cards set elevation on the FieldLabel that wraps the Field (sunken -1, flat, raised +1, floating +2). A checked card keeps the ring.
Sunken (-1)
Flat (0)
Raised (+1)
Floating (+2)
Sunken (-1)
Flat (0)
Raised (+1)
Floating (+2)
Sunken (-1)
Flat (0)
Raised (+1)
Floating (+2)
import { Field, FieldContent, FieldDescription, FieldLabel, FieldTitle,} from "@/components/ui/field";import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
const options = [ { value: "weekly", title: "Weekly", description: "Rebalance every Monday." }, { value: "drift", title: "On drift", description: "Only when a weight drifts.", },];
const levels = [ { value: "sunken", label: "Sunken (-1)" }, { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" },] as const;
export default function Demo() { return ( <div className="flex flex-col gap-5"> {levels.map(({ value, label }) => ( <div key={value} className="flex flex-col gap-2"> <p className="text-xs font-medium text-muted-foreground">{label}</p> <RadioGroup defaultValue="weekly" className="max-w-sm"> {options.map((o) => ( <FieldLabel key={o.value} elevation={value} htmlFor={`${value}-${o.value}`} > <Field orientation="horizontal"> <FieldContent> <FieldTitle>{o.title}</FieldTitle> <FieldDescription>{o.description}</FieldDescription> </FieldContent> <RadioGroupItem id={`${value}-${o.value}`} value={o.value} /> </Field> </FieldLabel> ))} </RadioGroup> </div> ))} </div> );}<script setup lang="ts">import { Field, FieldContent, FieldDescription, FieldLabel, FieldTitle } from "@/components/ui/field";import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
const options = [ { value: "weekly", title: "Weekly", description: "Rebalance every Monday." }, { value: "drift", title: "On drift", description: "Only when a weight drifts." },];
const levels = [{ value: "sunken", label: "Sunken (-1)" }, { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }];</script>
<template> <div class="flex flex-col gap-5"> <div v-for="level in levels" :key="level.value" class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{{ level.label }}</p> <RadioGroup default-value="weekly" class="max-w-sm"> <FieldLabel v-for="o in options" :key="o.value" :elevation="level.value" :for="`${level.value}-${o.value}`"> <Field orientation="horizontal"> <FieldContent> <FieldTitle>{{ o.title }}</FieldTitle> <FieldDescription>{{ o.description }}</FieldDescription> </FieldContent> <RadioGroupItem :id="`${level.value}-${o.value}`" :value="o.value" /> </Field> </FieldLabel> </RadioGroup> </div> </div></template><script lang="ts"> import { Field, FieldContent, FieldDescription, FieldLabel, FieldTitle } from "$lib/components/ui/field"; import { RadioGroup, RadioGroupItem } from "$lib/components/ui/radio-group";
const options = [ { value: "weekly", title: "Weekly", description: "Rebalance every Monday." }, { value: "drift", title: "On drift", description: "Only when a weight drifts." }, ];
const levels = [{ value: "sunken", label: "Sunken (-1)" }, { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }] as const;</script>
<div class="flex flex-col gap-5"> {#each levels as level (level.value)} <div class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{level.label}</p> <RadioGroup value="weekly" class="max-w-sm"> {#each options as o (o.value)} <FieldLabel elevation={level.value} for={`${level.value}-${o.value}`}> <Field orientation="horizontal"> <FieldContent> <FieldTitle>{o.title}</FieldTitle> <FieldDescription>{o.description}</FieldDescription> </FieldContent> <RadioGroupItem id={`${level.value}-${o.value}`} value={o.value} /> </Field> </FieldLabel> {/each} </RadioGroup> </div> {/each}</div>Built on Base UI RadioGroup / Radio. Same anatomy as shadcn: RadioGroup, RadioGroupItem.
| Prop | Type | Default |
|---|---|---|
value / defaultValue |
string |
- |
onValueChange |
(value: string, event) => void |
- |
disabled |
boolean |
false |
RadioGroupItem value |
string |
required |
elevation |
"auto" | "sunken" | "flat" | "raised" | "floating" |
"auto" |
✦ Edmi additions
Section titled “✦ Edmi additions”- Checked radio has a primary border and a 9px primary dot; pair with a bordered label for choice cards.