|Design System

Core Component

Radio

Radio group for mutually exclusive selection with spring-animated selection dot and context-based state sharing.

Preview

Claude 3.5 Sonnet

Best for complex reasoning

GPT-4o

Fast multimodal model

Gemini Pro

Google's flagship model

Source

Full component implementation using the design system tokens.

tsx
"use client";

import { createContext, useContext, useState } from "react";

type RadioContextType = { value: string; onChange: (value: string) => void; name: string };
const RadioContext = createContext<RadioContextType | null>(null);

export function DSRadioGroup({
  value: controlledValue, defaultValue = "", onChange, name, children,
}: {
  value?: string; defaultValue?: string; onChange?: (value: string) => void;
  name: string; children: React.ReactNode;
}) {
  const [internalValue, setInternalValue] = useState(defaultValue);
  const isControlled = controlledValue !== undefined;
  const currentValue = isControlled ? controlledValue : internalValue;

  const handleChange = (val: string) => {
    if (!isControlled) setInternalValue(val);
    onChange?.(val);
  };

  return (
    <RadioContext.Provider value={{ value: currentValue, onChange: handleChange, name }}>
      <div role="radiogroup" className="flex flex-col gap-3">{children}</div>
    </RadioContext.Provider>
  );
}

export function DSRadioItem({
  value, label, description, disabled = false,
}: {
  value: string; label: string; description?: string; disabled?: boolean;
}) {
  const ctx = useContext(RadioContext);
  if (!ctx) throw new Error("DSRadioItem must be used within DSRadioGroup");
  const selected = ctx.value === value;
  const select = () => { if (!disabled) ctx.onChange(value); };

  return (
    <div className="flex items-start gap-3">
      <button role="radio" aria-checked={selected} onClick={select} disabled={disabled}
        className={`relative inline-flex items-center justify-center w-5 h-5 shrink-0 rounded-full border-2 transition-all duration-150 active:scale-[0.95] mt-0.5 ${selected ? "border-accent" : "border-overlay/15 hover:border-overlay/25"} ${disabled ? "opacity-50 cursor-not-allowed active:scale-100" : ""}`}
        style={{ transitionTimingFunction: "var(--ease-out)" }}>
        <span className="w-2.5 h-2.5 rounded-full bg-accent" style={{
          opacity: selected ? 1 : 0,
          transform: selected ? "scale(1)" : "scale(0)",
          transition: "opacity 150ms var(--ease-out), transform 200ms var(--ease-out)",
        }} />
      </button>
      {(label || description) && (
        <div className={disabled ? "opacity-50" : "cursor-pointer"} onClick={disabled ? undefined : select}>
          {label && <p className="text-sm font-medium text-foreground">{label}</p>}
          {description && <p className="text-xs text-tertiary mt-0.5">{description}</p>}
        </div>
      )}
    </div>
  );
}

Props

All available props with types and defaults.

PropTypeDescription
name*stringGroup name for form association (RadioGroup)
label*stringLabel text (RadioItem)
valuestringControlled selected value (RadioGroup)
defaultValuestringInitial value (RadioGroup, uncontrolled)
onChange(value: string) => voidCalled when selection changes (RadioGroup)
descriptionstringHelper text (RadioItem)
disabledbooleanDisables the radio item

Variants

Default Group

Basic radio group with three options.

Claude 3.5 Sonnet

GPT-4o

Gemini Pro

tsx
<DSRadioGroup name="model" defaultValue="claude">
  <DSRadioItem value="claude" label="Claude 3.5 Sonnet" />
  <DSRadioItem value="gpt4" label="GPT-4o" />
  <DSRadioItem value="gemini" label="Gemini Pro" />
</DSRadioGroup>

With Descriptions

Radio items with helper text for context.

Precise

Lower temperature, more focused

Balanced

Default settings

Creative

Higher temperature, more varied

tsx
<DSRadioGroup name="mode" defaultValue="balanced">
  <DSRadioItem value="precise" label="Precise" description="Lower temperature, more focused" />
  <DSRadioItem value="balanced" label="Balanced" description="Default settings" />
  <DSRadioItem value="creative" label="Creative" description="Higher temperature, more varied" />
</DSRadioGroup>

With Disabled Item

Some options can be individually disabled.

Free

Pro

Enterprise

tsx
<DSRadioGroup name="tier">
  <DSRadioItem value="free" label="Free" />
  <DSRadioItem value="pro" label="Pro" />
  <DSRadioItem value="enterprise" label="Enterprise" disabled />
</DSRadioGroup>

Prompt Guide

Prompt Guide — Radio

Use for

  • Model selection (mutually exclusive)
  • Response mode (precise / balanced / creative)
  • Export format selection
  • Tier or plan selection

Don't use for

  • Multi-select — use Checkbox group
  • Binary toggle — use Toggle or Checkbox
  • More than 7 options — use Select dropdown

AI Context

Radio uses React context to share group state. The selection dot animates from scale(0) to scale(1) with 200ms ease-out for a spring-like feel. Use for 3-7 mutually exclusive options. For more, switch to Select.