Components
TypedRadioGroup
A headless, type-safe radio group component with full TypeScript inference and customizable rendering.
Introduction
TypedRadioGroup is a headless, type-safe wrapper around the base RadioGroup component. It provides full TypeScript inference for option values while giving you complete control over rendering through render props.
Key features:
- Full type-safety for
string,number, andbooleanvalues - Headless design with render props for complete customization
- Accessible by default (keyboard navigation, ARIA)
- Controlled and uncontrolled usage
Installation
npx shadcn@latest add https://typedora-ui.netlify.app/r/typed-radio-group.jsonpnpm dlx shadcn@latest add https://typedora-ui.netlify.app/r/typed-radio-group.jsonnpx shadcn@latest add https://typedora-ui.netlify.app/r/typed-radio-group.jsonbunx --bun shadcn@latest add https://typedora-ui.netlify.app/r/typed-radio-group.jsonUsage
Basic Usage
import { } from "@/components/typed-radio-group";
export function () {
return (
<>
<
={[
{ : "Apple", : "apple" },
{ : "Banana", : "banana" },
{ : "Orange", : "orange" },
]}
={(value) => { }}
/>
</>
);
}Number Values
import { } from "@/components/typed-radio-group";
export function () {
return (
<>
<
={[
{ : "Small", : 1 },
{ : "Medium", : 2 },
{ : "Large", : 3 },
]}
={(value) => { }}
/>
</>
);
}Boolean Values
import { } from "@/components/typed-radio-group";
export function () {
return (
<>
<
={[
{ : "Yes", : true },
{ : "No", : false },
]}
={(value) => { }}
/>
</>
);
}Card Style
import { } from "lucide-react";
import { } from "@/components/typed-radio-group";
import { } from "@/lib/utils";
export function () {
return (
< ="not-prose">
<
="free"
="grid grid-cols-3 gap-4"
={[
{ : "free", : "Free", : "Basic features" },
{ : "pro", : "Pro", : "Advanced features" },
{ : "team", : "Team", : "For teams" },
]}
={({ , , , }) => (
<
={.}
={(
"relative flex flex-col p-4 border rounded-lg cursor-pointer",
? "border-primary bg-primary/5" : "border-border",
)}
>
< {...} ="sr-only" />
{ && (
< ="absolute top-3 right-3 size-4 text-primary" />
)}
< ="grid grow gap-2">
< ="font-semibold">{.}</>
<
="text-muted-foreground text-xs"
={`${.}-description`}
>
{.}
</>
</>
</>
)}
/>
</>
);
}Custom Keys (id/name)
By default, TypedRadioGroup uses value and label keys. You can customize this using valueKey and labelKey props:
"use client";
import { } from "react";
import { } from "@/components/typed-radio-group";
import { } from "@/components/ui/label";
// Example data using id/name keys instead of value/label
const = [
{
: "admin",
: "Administrator",
: "Full access to all features",
},
{ : "editor", : "Editor", : "Can edit content" },
{ : "viewer", : "Viewer", : "Read-only access" },
] as ;
export function () {
const [, ] = <(typeof )[number]["id"]>();
return (
< ="space-y-4">
<>Select Role</>
<
={}
="id"
="name"
={}
={}
={({ , , , }) => (
<
={.}
="flex items-start gap-3 cursor-pointer p-3 rounded-lg border hover:bg-accent"
>
< {...} ="mt-1">
< />
</>
< ="flex flex-col">
< ="font-medium">{.}</>
< ="text-sm text-muted-foreground">
{.}
</>
</>
</>
)}
/>
{ && (
< ="text-sm text-muted-foreground">
Selected role: <>{}</>
</>
)}
</>
);
}Disabled Options
import { } from "@/components/typed-radio-group";
export function () {
return (
<>
<
={[
{ : "Available", : "available" },
{ : "Coming Soon", : "coming-soon", : true },
{ : "Premium", : "premium" },
]}
/>
</>
);
}Controlled Component
"use client";
import { } from "react";
import { } from "@/components/typed-radio-group";
export function () {
const [, ] = <"apple" | "banana">("apple");
return (
< ="space-y-4">
<
={}
={
[
{ : "Apple", : "apple" },
{ : "Banana", : "banana" },
] as
}
={}
/>
<>Selected: {}</>
</>
);
}Selected: apple
API Reference
TypedRadioGroupProps
| Prop | Type |
|---|---|
options | array |
valueKey? | TValueKey |
labelKey? | TLabelKey |
value? | ExtractOptionValue<TOption, TValueKey> |
defaultValue? | ExtractOptionValue<TOption, TValueKey> |
onValueChange? | function |
renderItem? | function |
renderGroup? | function |
className? | string |
disabled? | boolean |
name? | string |
required? | boolean |
orientation? | "vertical" | "horizontal" |
loop? | boolean |
RadioOption
| Prop | Type |
|---|---|
value | T |
label | string |
disabled? | boolean |
RadioItemRenderProps
| Prop | Type |
|---|---|
option | TOption |
isSelected | boolean |
isDisabled | boolean |
itemProps | object |
Item | function |
Indicator | function |
RadioGroupRenderProps
| Prop | Type |
|---|---|
children | ReactNode |
Keyboard Interactions
| Key | Description |
|---|---|
Tab | Moves focus to the checked radio item or first item |
Space | Checks the focused radio item |
ArrowDown | Moves focus and checks the next item |
ArrowRight | Moves focus and checks the next item |
ArrowUp | Moves focus and checks the previous item |
ArrowLeft | Moves focus and checks the previous item |