Radio Group
A shadcn-style radio group component built with Ark UI primitives.
Framework
import {
Radio,
RadioGroup,
RadioGroupLabel,
} from "@/components/ui/radio-group";
const RadioGroupDefault = () => (
<RadioGroup className="max-w-xs" defaultValue="next">
<RadioGroupLabel>Framework</RadioGroupLabel>
<Radio value="next">Next.js</Radio>
<Radio value="vite">Vite</Radio>
<Radio value="astro">Astro</Radio>
</RadioGroup>
);
export default RadioGroupDefault;
Installation
npx shadcn@latest add @ark-cn/radio-groupInstall the dependency required by this primitive:
npm install @ark-ui/reactCopy the component source into your app:
TSXcomponents/ui/radio-group.tsx
"use client";
import {
RadioGroupContext,
RadioGroup as RadioGroupPrimitive,
useRadioGroup,
useRadioGroupContext,
} from "@ark-ui/react/radio-group";
import { cn } from "@/lib/utils";
export type RadioGroupProps = RadioGroupPrimitive.RootProps;
export const RadioGroup = ({ className, ...props }: RadioGroupProps) => (
<RadioGroupPrimitive.Root
className={cn(
"flex flex-col gap-2 text-foreground",
"data-[orientation=horizontal]:flex-row data-[orientation=horizontal]:flex-wrap data-[orientation=horizontal]:gap-4",
className,
)}
data-slot="radio-group"
{...props}
/>
);
export type RadioGroupLabelProps = RadioGroupPrimitive.LabelProps;
export const RadioGroupLabel = ({
className,
...props
}: RadioGroupLabelProps) => (
<RadioGroupPrimitive.Label
className={cn(
"text-sm font-medium text-foreground leading-none select-none",
"data-disabled:pointer-events-none data-disabled:opacity-50",
className,
)}
data-slot="radio-group-label"
{...props}
/>
);
export type RadioProps = RadioGroupPrimitive.ItemProps;
export const Radio = ({ className, children, ...props }: RadioProps) => (
<RadioGroupPrimitive.Item
className={cn(
"flex cursor-pointer items-center gap-2 rounded-md outline-none",
"data-disabled:cursor-not-allowed data-disabled:opacity-50 data-readonly:cursor-default",
className,
)}
data-slot="radio-group-item"
{...props}
>
<RadioGroupPrimitive.ItemHiddenInput />
<RadioGroupPrimitive.ItemText
className={cn(
"inline-flex items-center gap-2 text-foreground text-sm",
"data-disabled:opacity-50",
)}
data-slot="radio-group-item-text"
>
<RadioGroupPrimitive.ItemControl
className={cn(
"group/radio-control relative flex size-4 shrink-0 items-center justify-center rounded-full border border-input bg-background shadow-xs/5 outline-none transition-[color,box-shadow]",
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
"data-focus-visible:border-ring data-focus-visible:ring-[3px] data-focus-visible:ring-ring/50",
"data-readonly:cursor-default data-disabled:cursor-not-allowed data-disabled:opacity-50",
"data-invalid:border-destructive data-invalid:ring-destructive/20 dark:bg-input/30",
"data-[state=checked]:border-primary",
)}
data-slot="radio-group-item-control"
>
<span
aria-hidden
className="size-2 scale-0 rounded-full bg-primary opacity-0 transition-all group-data-[state=checked]/radio-control:scale-100 group-data-[state=checked]/radio-control:opacity-100"
/>
</RadioGroupPrimitive.ItemControl>
{children}
</RadioGroupPrimitive.ItemText>
</RadioGroupPrimitive.Item>
);
export const RadioGroupItem = Radio;
export type RadioGroupRootProviderProps = RadioGroupPrimitive.RootProviderProps;
export const RadioGroupRootProvider = ({
className,
...props
}: RadioGroupRootProviderProps) => (
<RadioGroupPrimitive.RootProvider
className={cn(
"flex flex-col gap-2 text-foreground",
"data-[orientation=horizontal]:flex-row data-[orientation=horizontal]:flex-wrap data-[orientation=horizontal]:gap-4",
className,
)}
data-slot="radio-group"
{...props}
/>
);
export type RadioGroupItemControlProps = RadioGroupPrimitive.ItemControlProps;
export const RadioGroupItemControl = ({
className,
...props
}: RadioGroupItemControlProps) => (
<RadioGroupPrimitive.ItemControl
className={cn(className)}
data-slot="radio-group-item-control-raw"
{...props}
/>
);
export type RadioGroupItemTextProps = RadioGroupPrimitive.ItemTextProps;
export const RadioGroupItemText = ({
className,
...props
}: RadioGroupItemTextProps) => (
<RadioGroupPrimitive.ItemText
className={cn(className)}
data-slot="radio-group-item-text-raw"
{...props}
/>
);
export type RadioGroupItemHiddenInputProps =
RadioGroupPrimitive.ItemHiddenInputProps;
export const RadioGroupItemHiddenInput = (
props: RadioGroupItemHiddenInputProps,
) => <RadioGroupPrimitive.ItemHiddenInput {...props} />;
export type RadioGroupItemPrimitiveProps = RadioGroupPrimitive.ItemProps;
export const RadioGroupItemPrimitive = ({
className,
...props
}: RadioGroupItemPrimitiveProps) => (
<RadioGroupPrimitive.Item
className={cn(className)}
data-slot="radio-group-item-raw"
{...props}
/>
);
export type RadioGroupIndicatorProps = RadioGroupPrimitive.IndicatorProps;
export const RadioGroupIndicator = ({
className,
...props
}: RadioGroupIndicatorProps) => (
<RadioGroupPrimitive.Indicator
className={cn(className)}
data-slot="radio-group-indicator"
{...props}
/>
);
export type {
RadioGroupValueChangeDetails,
UseRadioGroupProps,
UseRadioGroupReturn,
} from "@ark-ui/react/radio-group";
export { RadioGroupContext, useRadioGroup, useRadioGroupContext };
Update import aliases to match your project setup.
Usage
import * as RadioGroup from "@/components/ui/radio-group"Read exported parts in src/components/ui/radio-group.tsx and compose the primitive according to the Ark UI pattern for this component.
Examples
Default
Framework
import {
Radio,
RadioGroup,
RadioGroupLabel,
} from "@/components/ui/radio-group";
const RadioGroupDefault = () => (
<RadioGroup className="max-w-xs" defaultValue="next">
<RadioGroupLabel>Framework</RadioGroupLabel>
<Radio value="next">Next.js</Radio>
<Radio value="vite">Vite</Radio>
<Radio value="astro">Astro</Radio>
</RadioGroup>
);
export default RadioGroupDefault;
Controlled
Value: next
import { useState } from "react";
import { Radio, RadioGroup } from "@/components/ui/radio-group";
const RadioGroupControlled = () => {
const [value, setValue] = useState<string | null>("next");
return (
<div className="flex max-w-xs flex-col gap-2">
<p className="text-muted-foreground text-xs">
Value: <span className="font-medium text-foreground">{value}</span>
</p>
<RadioGroup
value={value}
onValueChange={({ value: next }) => setValue(next)}
>
<Radio value="next">Next.js</Radio>
<Radio value="vite">Vite</Radio>
<Radio value="astro">Astro</Radio>
</RadioGroup>
</div>
);
};
export default RadioGroupControlled;
Disabled group
Framework (group disabled)
import {
Radio,
RadioGroup,
RadioGroupLabel,
} from "@/components/ui/radio-group";
const RadioGroupDisabledGroup = () => (
<RadioGroup className="max-w-xs" defaultValue="next" disabled>
<RadioGroupLabel>Framework (group disabled)</RadioGroupLabel>
<Radio value="next">Next.js</Radio>
<Radio value="vite">Vite</Radio>
<Radio value="astro">Astro</Radio>
</RadioGroup>
);
export default RadioGroupDisabledGroup;
Disabled item
Framework
import {
Radio,
RadioGroup,
RadioGroupLabel,
} from "@/components/ui/radio-group";
const RadioGroupDisabledItem = () => (
<RadioGroup className="max-w-xs" defaultValue="next">
<RadioGroupLabel>Framework</RadioGroupLabel>
<Radio value="next">Next.js</Radio>
<Radio disabled value="vite">
Vite (disabled)
</Radio>
<Radio value="astro">Astro</Radio>
</RadioGroup>
);
export default RadioGroupDisabledItem;
Orientation
Axis
import {
Radio,
RadioGroup,
RadioGroupLabel,
} from "@/components/ui/radio-group";
const RadioGroupOrientation = () => (
<RadioGroup className="max-w-md" defaultValue="a" orientation="horizontal">
<RadioGroupLabel>Axis</RadioGroupLabel>
<Radio value="a">Option A</Radio>
<Radio value="b">Option B</Radio>
<Radio value="c">Option C</Radio>
</RadioGroup>
);
export default RadioGroupOrientation;
Root provider
Framework
import { Button } from "@/components/ui/button";
import {
Radio,
RadioGroupLabel,
RadioGroupRootProvider,
useRadioGroup,
} from "@/components/ui/radio-group";
const RadioGroupRootProviderDemo = () => {
const radio = useRadioGroup({ defaultValue: "React" });
const frameworks = ["React", "Solid", "Vue"] as const;
return (
<div className="flex max-w-xs flex-col gap-3">
<RadioGroupRootProvider value={radio}>
<RadioGroupLabel>Framework</RadioGroupLabel>
{frameworks.map((framework) => (
<Radio key={framework} value={framework}>
{framework}
</Radio>
))}
</RadioGroupRootProvider>
<Button
onClick={() => {
radio.setValue("Solid");
}}
type="button"
variant="outline"
>
Set to Solid
</Button>
</div>
);
};
export default RadioGroupRootProviderDemo;
With fieldset
import {
Fieldset,
FieldsetDescription,
FieldsetError,
FieldsetLegend,
} from "@/components/ui/fieldset";
import { Radio, RadioGroup } from "@/components/ui/radio-group";
const RadioGroupFieldsetDemo = () => (
<Fieldset className="max-w-sm">
<FieldsetLegend>Select a framework</FieldsetLegend>
<RadioGroup defaultValue="React">
<Radio value="React">React</Radio>
<Radio value="Solid">Solid</Radio>
<Radio value="Vue">Vue</Radio>
</RadioGroup>
<FieldsetDescription>
Choose your preferred framework for the docs examples.
</FieldsetDescription>
<FieldsetError>Please select a framework</FieldsetError>
</Fieldset>
);
export default RadioGroupFieldsetDemo;
With description
import { Radio, RadioGroup } from "@/components/ui/radio-group";
const RadioGroupWithDescriptionDemo = () => (
<RadioGroup className="max-w-sm" defaultValue="r-1">
<Radio value="r-1">
<span className="flex flex-col gap-1">
<span className="font-medium">Free</span>
<span className="text-muted-foreground text-xs">
Basic features for personal use.
</span>
</span>
</Radio>
<Radio value="r-2">
<span className="flex flex-col gap-1">
<span className="font-medium">Pro</span>
<span className="text-muted-foreground text-xs">
Advanced tools for professionals.
</span>
</span>
</Radio>
</RadioGroup>
);
export default RadioGroupWithDescriptionDemo;
Card style
import { Radio, RadioGroup } from "@/components/ui/radio-group";
const RadioGroupCardStyleDemo = () => (
<RadioGroup className="max-w-md gap-3" defaultValue="email">
<Radio
className="flex w-full items-stretch rounded-lg border border-border p-3 transition-colors hover:bg-accent/50 data-[state=checked]:border-primary/48 data-[state=checked]:bg-accent/50"
value="email"
>
<span className="flex flex-col gap-1 ps-0.5">
<span className="font-medium">Email</span>
<span className="text-muted-foreground text-xs">
Receive notifications via email.
</span>
</span>
</Radio>
<Radio
className="flex w-full items-stretch rounded-lg border border-border p-3 transition-colors hover:bg-accent/50 data-[state=checked]:border-primary/48 data-[state=checked]:bg-accent/50"
value="sms"
>
<span className="flex flex-col gap-1 ps-0.5">
<span className="font-medium">SMS</span>
<span className="text-muted-foreground text-xs">
Receive notifications via text message.
</span>
</span>
</Radio>
</RadioGroup>
);
export default RadioGroupCardStyleDemo;
Form
import { type FormEvent, useState } from "react";
import { Button } from "@/components/ui/button";
import {
Radio,
RadioGroup,
RadioGroupLabel,
} from "@/components/ui/radio-group";
const RadioGroupFormDemo = () => {
const [submitted, setSubmitted] = useState<string | null>(null);
return (
<form
className="flex max-w-xs flex-col gap-3"
onSubmit={(event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
setSubmitted(String(formData.get("frameworks") ?? ""));
}}
>
<RadioGroup name="frameworks" defaultValue="next">
<RadioGroupLabel>Frameworks</RadioGroupLabel>
<Radio value="next">Next.js</Radio>
<Radio value="vite">Vite</Radio>
<Radio value="astro">Astro</Radio>
</RadioGroup>
<Button type="submit">Submit</Button>
{submitted ? (
<p className="text-muted-foreground text-xs">
Selected:{" "}
<span className="font-medium text-foreground">{submitted}</span>
</p>
) : null}
</form>
);
};
export default RadioGroupFormDemo;
Read-only
Read-only
import {
Radio,
RadioGroup,
RadioGroupLabel,
} from "@/components/ui/radio-group";
const RadioGroupReadOnlyDemo = () => (
<RadioGroup className="max-w-xs" defaultValue="vite" readOnly>
<RadioGroupLabel>Read-only</RadioGroupLabel>
<Radio value="next">Next.js</Radio>
<Radio value="vite">Vite</Radio>
<Radio value="astro">Astro</Radio>
</RadioGroup>
);
export default RadioGroupReadOnlyDemo;
Invalid
Invalid state
import {
Radio,
RadioGroup,
RadioGroupLabel,
} from "@/components/ui/radio-group";
const RadioGroupInvalidDemo = () => (
<RadioGroup className="max-w-xs" defaultValue="next" invalid>
<RadioGroupLabel>Invalid state</RadioGroupLabel>
<Radio value="next">Next.js</Radio>
<Radio value="vite">Vite</Radio>
<Radio value="astro">Astro</Radio>
</RadioGroup>
);
export default RadioGroupInvalidDemo;
With field
Pick one framework for the project.
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import { Radio, RadioGroup } from "@/components/ui/radio-group";
const RadioGroupWithFieldDemo = () => (
<Field className="max-w-xs gap-3">
<FieldLabel>Stack</FieldLabel>
<RadioGroup defaultValue="next" name="stack-field">
<Radio value="next">Next.js</Radio>
<Radio value="vite">Vite</Radio>
<Radio value="astro">Astro</Radio>
</RadioGroup>
<FieldDescription>Pick one framework for the project.</FieldDescription>
</Field>
);
export default RadioGroupWithFieldDemo;
Context hook
useRadioGroupContext: b
import {
Radio,
RadioGroup,
useRadioGroupContext,
} from "@/components/ui/radio-group";
const RadioGroupContextHookDemo = () => {
const ValueText = () => {
const context = useRadioGroupContext();
return (
<p className="text-muted-foreground text-xs">
useRadioGroupContext:{" "}
<span className="font-medium text-foreground">{context.value}</span>
</p>
);
};
return (
<RadioGroup className="max-w-xs" defaultValue="b">
<ValueText />
<Radio value="a">Option A</Radio>
<Radio value="b">Option B</Radio>
</RadioGroup>
);
};
export default RadioGroupContextHookDemo;
Context render
RadioGroupContext: next
import {
Radio,
RadioGroup,
RadioGroupContext,
} from "@/components/ui/radio-group";
const RadioGroupContextRenderDemo = () => (
<RadioGroup className="max-w-xs" defaultValue="next">
<RadioGroupContext>
{(context) => (
<p className="text-muted-foreground text-xs">
RadioGroupContext:{" "}
<span className="font-medium text-foreground">{context.value}</span>
</p>
)}
</RadioGroupContext>
<Radio value="next">Next.js</Radio>
<Radio value="vite">Vite</Radio>
</RadioGroup>
);
export default RadioGroupContextRenderDemo;
API reference
This component mirrors the upstream Ark UI primitive.
See the ARK UI documentation for the full API.
Accessibility
See the Ark UI documentation for clarification.