Skip to content
Ballpoint
Contents

Radio Group

Drawn rings; choosing one inks a dot in the middle with a tight spiral.

Built on Base UI Radio

Installation

pnpm dlx shadcn@latest add @ballpoint/radio-group
Or copy the source
ui/radio-group.tsx
"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

tsx
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 seedstring | 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.