Radio Group
Native radio buttons with a brutal face, grouped in a fieldset whose legend names the choice.
stableRadio.tsx
Examples
With descriptions
radio-group/basic.tsx
import { Radio, RadioGroup } from "@streamuilib/react";
export default function RadioGroupBasic() {
return (
<RadioGroup legend="Billing" className="w-full max-w-xs">
<Radio name="billing" value="monthly" label="Monthly" description="$24 per seat, cancel anytime" defaultChecked />
<Radio name="billing" value="yearly" label="Yearly" description="$19 per seat, billed once" />
<Radio name="billing" value="invoice" label="Invoice" description="Enterprise only" disabled />
</RadioGroup>
);
}Horizontal
radio-group/horizontal.tsx
import { Radio, RadioGroup } from "@streamuilib/react";
export default function RadioGroupHorizontal() {
return (
<RadioGroup legend="T-shirt size" orientation="horizontal">
{["S", "M", "L", "XL"].map((size) => (
<Radio key={size} name="shirt-size" value={size} label={size} defaultChecked={size === "M"} />
))}
</RadioGroup>
);
}Installation
bun add @streamuilib/reactusage.tsx
import { Radio, RadioGroup } from "@streamuilib/react";
<RadioGroup legend="Billing">
<Radio name="billing" value="monthly" label="Monthly" />
<Radio name="billing" value="yearly" label="Yearly" />
</RadioGroup>API
| Prop | Type | Default | Description |
|---|---|---|---|
| legend | ReactNode | — | The group's visible name. Required. |
| orientation | "vertical" | "horizontal" | "vertical" | Layout of the options. |
| ...props | ComponentProps<"fieldset"> | — | Native fieldset attributes, e.g. disabled. |
| Prop | Type | Default | Description |
|---|---|---|---|
| name | string | — | Shared by every radio in the group. |
| value | string | — | Submitted with the form when chosen. |
| label | ReactNode | — | Visible label; the whole row is clickable. |
| description | ReactNode | — | Secondary line under the label. |
| ...props | Omit<ComponentProps<"input">, "type"> | — | Native input attributes, e.g. defaultChecked. |
Accessibility
- Real radio inputs: arrow keys move between options and Tab moves in and out of the group, as browsers expect.
- The fieldset's legend is announced with each option, so users know what they're choosing.
- The visible circle mirrors the input's focus ring, and the dot only shows when it's checked.
Source
packages/react/src/Radio.tsx
import type { ComponentProps, ReactNode } from "react";
import { cn } from "./cn";
export type RadioProps = Omit<ComponentProps<"input">, "type"> & {
label?: ReactNode;
/** Secondary line under the label. */
description?: ReactNode;
};
/**
* A native radio with a brutal face. Group radios by giving them the same
* `name`, inside a RadioGroup (or any fieldset with a legend).
*/
export function Radio({ label, description, className, ...props }: RadioProps) {
return (
<label
className={cn(
"inline-flex cursor-pointer items-start gap-3 has-disabled:cursor-not-allowed has-disabled:opacity-50",
className,
)}
>
<input type="radio" className="peer sr-only" {...props} />
<span
aria-hidden
className={cn(
"mt-px grid size-6 shrink-0 place-items-center rounded-full border-2 border-ink bg-paper-raised shadow-brutal-sm transition-colors duration-150",
"peer-checked:bg-lime peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-blue",
"[&>span]:scale-0 peer-checked:[&>span]:scale-100",
)}
>
<span className="size-2.5 rounded-full bg-ink transition-transform duration-150 motion-reduce:transition-none" />
</span>
{(label || description) && (
<span className="flex flex-col gap-0.5">
{label && <span className="font-semibold">{label}</span>}
{description && <span className="text-sm text-ink-muted">{description}</span>}
</span>
)}
</label>
);
}
export type RadioGroupProps = ComponentProps<"fieldset"> & {
/** Visible group label, rendered as the fieldset's legend. */
legend: ReactNode;
orientation?: "vertical" | "horizontal";
};
/** A fieldset with a legend: screen readers announce the group name with each radio. */
export function RadioGroup({ legend, orientation = "vertical", className, children, ...props }: RadioGroupProps) {
return (
<fieldset className={cn("min-w-0", className)} {...props}>
<legend className="mb-3 font-bold">{legend}</legend>
<div className={cn("flex gap-3", orientation === "vertical" ? "flex-col" : "flex-wrap gap-x-6")}>{children}</div>
</fieldset>
);
}