Skip to content
edmi-ui

Radio Group

Pick exactly one option from a short list.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
Terminal window
npx shadcn@latest add @edmi-ui/radio-group
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/radio-group
Terminal window
yarn dlx shadcn@latest add @edmi-ui/radio-group
Terminal window
bunx shadcn@latest add @edmi-ui/radio-group

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/radio-group
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/radio-group
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/radio-group
Terminal window
bunx shadcn-vue@latest add @edmi-ui/radio-group

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/radio-group.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/radio-group.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/radio-group.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/radio-group.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

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>
registry/ui/radio-group.tsx
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 };
registry/ui/radio-group/RadioGroup.vue
<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>
registry/ui/radio-group/RadioGroupItem.vue
<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>
registry/ui/radio-group/index.ts
export { default as RadioGroup } from "./RadioGroup.vue";
export { default as RadioGroupItem } from "./RadioGroupItem.vue";
src/lib/registry/ui/radio-group/radio-group.svelte
<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}
/>
src/lib/registry/ui/radio-group/radio-group-item.svelte
<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>
src/lib/registry/ui/radio-group/index.ts
import Root from "./radio-group.svelte";
import Item from "./radio-group-item.svelte";
export {
Item,
Item as RadioGroupItem,
Root,
//
Root as RadioGroup,
};

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.

StackBlitz ↗StackBlitz ↗StackBlitz ↗

Sunken (-1)

Flat (0)

Raised (+1)

Floating (+2)

Sunken (-1)

Flat (0)

Raised (+1)

Floating (+2)

Sunken (-1)

Flat (0)

Raised (+1)

Floating (+2)

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"
  • Checked radio has a primary border and a 9px primary dot; pair with a bordered label for choice cards.