Radio Group
Drawn rings; choosing one inks a dot in the middle with a tight spiral.
Built on Base UI Radioimport { Field, FieldContent, FieldDescription, FieldLabel, FieldTitle } from "@/components/ui/field";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
export default function RadioGroupDemo() {
return (
<div className="flex w-full max-w-lg flex-col gap-10">
<RadioGroup defaultValue="blue" aria-label="Ink">
{[
["blue", "Blue ballpoint"],
["black", "Black fineliner"],
["pencil", "Pencil"],
].map(([value, label]) => (
<Label key={value}>
<RadioGroupItem value={value} seed={`radio-${value}`} />
{label}
</Label>
))}
</RadioGroup>
<RadioGroup defaultValue="cream" aria-label="Paper" className="grid-cols-1 gap-5 sm:grid-cols-2">
{[
["cream", "Cream", "Warm, like a notebook."],
["night", "Night", "Navy, for the lamp."],
].map(([value, title, description]) => (
<FieldLabel key={value}>
<Field orientation="horizontal">
<RadioGroupItem value={value} seed={`paper-${value}`} />
<FieldContent>
<FieldTitle>{title}</FieldTitle>
<FieldDescription>{description}</FieldDescription>
</FieldContent>
</Field>
</FieldLabel>
))}
</RadioGroup>
</div>
);
}Installation
pnpm dlx shadcn@latest add @ballpoint/ radio-groupOr copy the source
"use client";
import { Radio as RadioPrimitive } from "@base-ui/react/radio";
import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group";
import { cn } from "@/lib/utils";
import { usePen, type Pen } from "@/hooks/use-ink-box";
import { Stroke } from "@/lib/ink";
import { InkOutline } from "@/lib/ink-outline";
import { dotStroke } from "@/lib/ink-sketch";
function RadioGroup({ className, ...props }: RadioGroupPrimitive.Props) {
return <RadioGroupPrimitive data-slot="radio-group" className={cn("grid w-full gap-3", className)} {...props} />;
}
/** A drawn ring; choosing it inks a dot in the middle with a tight spiral. */
function RadioGroupItem({
className,
seed,
passes,
draw,
weight,
speed,
...props
}: RadioPrimitive.Root.Props & Pick<Pen, "passes" | "draw" | "weight" | "speed"> & { seed?: string | number }) {
const pen = usePen({ passes, draw, weight, speed });
return (
<RadioPrimitive.Root
data-slot="radio-group-item"
className={cn(
"peer group/radio relative inline-flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-full outline-none",
"after:absolute after:-inset-x-3 after:-inset-y-2",
"text-ink-3 transition-colors duration-(--dur-hover) not-data-checked:hover:text-ink data-checked:text-ink",
"aria-invalid:text-destructive data-invalid:text-destructive",
"focus-visible:outline-solid focus-visible:outline-[1.5px] focus-visible:outline-offset-4 focus-visible:outline-ring",
"data-disabled:cursor-not-allowed data-disabled:opacity-50",
className,
)}
{...props}
>
<InkOutline pen={pen} seed={seed} estimate={[20, 20]} shape="ring" pad={6}>
{({ w, h, s }) => (
<g transform={`translate(${w / 2} ${h / 2})`}>
<Stroke d={dotStroke(s + 40, w * 0.22)} draw="checked" duration={260} width={w * 0.17} />
</g>
)}
</InkOutline>
</RadioPrimitive.Root>
);
}
export { RadioGroup, RadioGroupItem };Usage
import { Label } from "@/components/ui/label"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
<RadioGroup defaultValue="blue">
<Label>
<RadioGroupItem value="blue" />
Blue ballpoint
</Label>
<Label>
<RadioGroupItem value="pencil" />
Pencil
</Label>
</RadioGroup>API reference
- RadioGroupItem seed
string | number - Pins the drawing.
Every other prop goes to Base UI's Radio.
Pen settings
The drawing also takes draw, passes, weight and speed, as props or from the nearest InkProvider. What each one does.