# RadioGroup

Part of the Dovetail design system (https://graham-goebel.github.io/Dovetail/), in the Forms family. Files: [RadioGroup.jsx](https://graham-goebel.github.io/Dovetail/system/components/forms/RadioGroup.jsx), [RadioGroup.d.ts](https://graham-goebel.github.io/Dovetail/system/components/forms/RadioGroup.d.ts), [RadioGroup.md](https://graham-goebel.github.io/Dovetail/system/components/forms/RadioGroup.md).

Live page: https://graham-goebel.github.io/Dovetail/components/RadioGroup.html

## Guidelines

A labelled set of radios where exactly one option wins. Always use the group rather than loose \`Radio\` elements: it generates the shared \`name\`, applies \`role="radiogroup"\`, and gives the set an accessible name.

### Always preselect

Ship a \`defaultValue\`. An empty radio group forces a decision before the user has read the options and cannot be returned to its original state once touched. If no option is a safe default, the question is a \`Select\` with a placeholder, not a radio group.

### Control at the end

\`labelPosition="start"\` puts each label on the left and each radio at the right edge of the row, so the options line up with the group's question instead of indenting. It suits narrow panels and settings lists, where the right edge is where the eye looks for the control.

\`\`\`jsx
<RadioGroup label="Sort by" labelPosition="start" defaultValue="due" options={[
  { value: "due", label: "Due date" },
  { value: "amount", label: "Amount" },
]} />
\`\`\`

### When to use a Select instead

Radios show every option at once, which is their advantage and their cost. Past about five options they crowd the form, so switch to \`Select\`. Below three, consider whether the choice is really a \`Switch\`.

\`\`\`jsx
<RadioGroup
  label="Deployment target"
  defaultValue="staging"
  options={[
    { value: "staging", label: "Staging", hint: "Rebuilt on every merge" },
    { value: "canary", label: "Canary", hint: "Five percent of traffic" },
    { value: "production", label: "Production" },
  ]}
/>
\`\`\`

## Props

```ts
import * as React from "react";

export interface RadioOption {
  value: string;
  label: React.ReactNode;
  hint?: React.ReactNode;
  disabled?: boolean;
}

/** A labelled set of radios. Exactly one option is selected. */
export interface RadioGroupProps extends Omit<React.HTMLAttributes<HTMLElement>, "onChange" | "defaultValue"> {
  /** The question the set answers. Required. */
  label: React.ReactNode;
  hint?: React.ReactNode;
  error?: React.ReactNode;
  required?: boolean;
  options: RadioOption[];
  value?: string;
  defaultValue?: string;
  onChange?: (value: string) => void;
  disabled?: boolean;
  /** @default "vertical" */
  orientation?: "vertical" | "horizontal";
  /** Passed to every option. "start" puts each label on the left and each control at the right edge of the row, so labels line up with the group's question instead of indenting. @default "end" */
  labelPosition?: "start" | "end";
  /** Shared input name. Generated when omitted. */
  name?: string;
}

export declare function RadioGroup(props: RadioGroupProps): React.JSX.Element;
```

## Tokens it reads

| Token | Tier | Declared as |
| --- | --- | --- |
| `--dt-space-inline-lg` | semantic | `var(--dt-dim-6)` |
| `--dt-space-stack-sm` | semantic | `var(--dt-dim-3)` |

## Source

```jsx
import React from "react";
import { Field } from "./Field.jsx";
import { Radio } from "./Radio.jsx";

export function RadioGroup({ label, hint, error, required = false, options = [], value, defaultValue, onChange, disabled = false, orientation = "vertical", labelPosition = "end", name, style, ...rest }) {
  const ids = React.useId();
  const groupName = name || ids;
  const [internal, setInternal] = React.useState(defaultValue);
  const selected = value !== undefined ? value : internal;

  const pick = (optValue) => {
    if (value === undefined) setInternal(optValue);
    onChange && onChange(optValue);
  };

  return (
    <Field label={label} hint={hint} error={error} required={required} labelId={`${ids}-label`} messageId={`${ids}-message`} style={style} {...rest}>
      <div
        role="radiogroup"
        aria-labelledby={label ? `${ids}-label` : undefined}
        aria-describedby={error || hint ? `${ids}-message` : undefined}
        style={{ display: "flex", flexDirection: orientation === "horizontal" ? "row" : "column", flexWrap: "wrap", gap: orientation === "horizontal" ? "var(--dt-space-inline-lg)" : "var(--dt-space-stack-sm)" }}
      >
        {options.map(opt => (
          <Radio
            key={opt.value}
            name={groupName}
            value={opt.value}
            label={opt.label}
            hint={opt.hint}
            checked={selected === opt.value}
            disabled={disabled || opt.disabled}
            labelPosition={labelPosition}
            onChange={() => pick(opt.value)}
          />
        ))}
      </div>
    </Field>
  );
}
```
