UnflowUI
Components

Radio

A styled native radio input, plus two higher-level building blocks — a card-style RadioArea and a labeled RadioInput — for building single-choice groups.

Overview

The Radio family gives you three layers, from bare input to fully labeled controls:

  • Radio — a styled native <input type="radio"> with size variants and nothing else. Use it when you're building a fully custom layout around the radio yourself (e.g. inside a design that doesn't match RadioArea or RadioInput).
  • RadioArea — a card-style choice built on top of Radio. The whole card is clickable, and it supports a label, hint, and optional leading/trailing elements (startEl/endEl). Use it for card-based choice groups (plans, payment methods, layout options, …).
  • RadioInput — a form-style labeled radio built on top of Radio, integrated with InputLabel and InputStatus. Use it for standard form radio groups where you want an inline label, hint text, and validation status.

All three are independent public exports and are typically used as a group sharing the same name, with checked/onChange driven from a single piece of state.

Import

import { Radio } from '@unflow.io/ui/components/Radio';
import { RadioArea } from '@unflow.io/ui/components/RadioArea';
import { RadioInput } from '@unflow.io/ui/components/RadioInput';

Radio

The bare styled radio input. All standard HTMLInputElement props (checked, value, name, onChange, onBlur, required, disabled, ...) are forwarded to the underlying <input>.

Props

PropTypeDefaultDescription
size'xs' | 'sm' | 'md' | 'lg''xs'Controls the diameter of the radio
checkedbooleanControlled checked state
valuestringValue submitted/reported when this option is selected
namestringGroups radios together — only one per shared name can be checked
onChangeChangeEventHandler<HTMLInputElement>Change handler
onBlurFocusEventHandler<HTMLInputElement>Blur handler
disabledbooleanDisables the input
requiredbooleanMarks the input as required
classNamestringApplied directly to the <input> element

A console warning fires in development if aria-label is missing, so always pair Radio with a visible label (e.g. via <label>) or pass aria-label explicitly.

Basic group

'use client';
import { useState } from 'react';
import { Radio } from '@unflow.io/ui/components/Radio';

export function Example() {
  const [value, setValue] = useState('option_1');

  return (
    <>
      {['option_1', 'option_2', 'option_3'].map((option) => (
        <label key={option} className="flex items-center gap-2">
          <Radio
            name="basic_group"
            value={option}
            checked={value === option}
            onChange={(e) => setValue(e.target.value)}
          />
          <span>{option}</span>
        </label>
      ))}
    </>
  );
}

RadioArea

Props

RadioArea accepts every Radio prop (minus size's effect on layout, which still applies to the hidden input) plus:

PropTypeDefaultDescription
labelstring | undefinedRequired. Card label. Pass undefined explicitly if you render fully custom children instead
hintstringHelper text shown under the label
variant'outlined' | 'solid''outlined'Visual style of the card
fullWidthbooleanMakes the card span the width of its container
startElReactNodeElement rendered before the label content (e.g. an icon)
endElReactNodeElement rendered after the label content
childrenReactNodeFully custom content — when provided, it replaces the built-in label/hint block
disabledbooleanDisables interaction and dims the card

Customization (slots & slotProps)

RadioArea has no overridable slots, only slotProps.

Slot PropPropsDescription
labelRootBaseHTMLProps<HTMLLabelElement>The outer <label> element that wraps the whole card
contentRootBaseHTMLProps<HTMLDivElement>Wrapper around the label/hint text block
labelBaseHTMLProps<HTMLParagraphElement>The label <p> element
hintBaseHTMLProps<HTMLParagraphElement>The hint <p> element

Card group

'use client';
import { useState } from 'react';
import { RadioArea } from '@unflow.io/ui/components/RadioArea';

export function Example() {
  const [value, setValue] = useState('option_1');

  return (
    <>
      {['option_1', 'option_2', 'option_3'].map((option, idx) => (
        <RadioArea
          key={option}
          name="plan"
          label={`Option ${idx + 1}`}
          hint="This is a hint text to help user."
          value={option}
          fullWidth
          checked={value === option}
          onChange={(e) => setValue(e.target.value)}
        />
      ))}
    </>
  );
}

Solid variant with start/end elements

<RadioArea
  name="plan"
  variant="solid"
  label="Pro plan"
  hint="Best for growing teams."
  value="pro"
  fullWidth
  startEl={<CloudFog size={18} />}
  checked={value === 'pro'}
  onChange={(e) => setValue(e.target.value)}
/>

RadioInput

Props

RadioInput accepts every Radio prop plus:

PropTypeDefaultDescription
labelstringVisible label rendered via InputLabel
hintstringHelper text. Shown next to the radio when inline, otherwise below it
inlinebooleanLays the label and radio out on one line instead of stacking
placement'start' | 'end''start'Whether the radio sits before or after the label content
status'valid' | 'invalid' | 'error'Validation status, reflected in styling and (when not inline) an InputStatus indicator
statusMessagestringMessage shown by the InputStatus indicator
fullWidthbooleanMakes the control span the width of its container
disabledbooleanDisables the input and dims the label
requiredbooleanMarks the input as required (shows the required symbol on the label)

A console warning fires in development if none of aria-label, label, or hint is provided.

Customization (slots & slotProps)

RadioInput has no overridable slots, only slotProps.

Slot PropPropsDescription
inputContainerBaseHTMLProps<HTMLDivElement>Wrapper around the radio, hint, and status indicator
inputLabelOmit<IInputLabel, 'label' | 'hint' | 'required' | 'fullWidth' | 'children' | 'inline' | 'status' | 'statusMessage'>The outer InputLabel component
hintBaseHTMLProps<HTMLParagraphElement>The hint <p> element (non-inline mode)
statusOmit<IInputStatus, 'status' | 'message'>The InputStatus indicator (non-inline mode)

Form group

'use client';
import { useState } from 'react';
import { RadioInput } from '@unflow.io/ui/components/RadioInput';

export function Example() {
  const [value, setValue] = useState('option_1');

  return (
    <>
      {['option_1', 'option_2', 'option_3'].map((option, idx) => (
        <RadioInput
          key={option}
          name="radio_group"
          label={`Option ${idx + 1}`}
          hint="This is a hint text to help user."
          value={option}
          inline
          checked={value === option}
          onChange={(e) => setValue(e.target.value)}
        />
      ))}
    </>
  );
}

With validation status

<RadioInput
  name="radio_group"
  label="Option with an error"
  status="error"
  statusMessage="This option is currently unavailable."
  value="option_1"
  checked={checked}
  onChange={(e) => setChecked(e.target.checked)}
/>

Accessibility

  • Radio warns in development if rendered without an aria-label — always pair it with a visible label (RadioArea/RadioInput handle this for you) or pass aria-label explicitly.
  • RadioArea wraps the hidden radio input in a <label>, so clicking anywhere on the card toggles the option; disabled cards use inert to remove them from the accessibility tree and tab order.
  • RadioInput renders its label through InputLabel, keeping the visible label programmatically associated with the input, and surfaces validation state through InputStatus.
  • As with any native radio group, give every option in the group the same name so screen readers and keyboard arrow navigation treat them as a single group.