---
title: NakedRadio
description: Headless radio button primitives backed by RadioGroup for exclusive selection
keywords: [flutter, radio, radiogroup, headless, accessibility, widgetstate]
---

Headless radio button component. RadioGroup coordinates keyboard navigation and exclusive selection. NakedRadio provides state for custom styling.

## When to use this

- **Single selection**: Choose one option from multiple choices
- **Settings**: Select one preference from a group (theme, language, etc.)
- **Forms**: Required single-choice fields
- **Filters**: Select one filter option from a category

<Info>
  Full examples are available in [`packages/example/lib/api/naked_radio.0.dart`](https://github.com/btwld/naked_ui/blob/main/packages/example/lib/api/naked_radio.0.dart).
</Info>

## Basic implementation

```dart
import 'package:flutter/material.dart';
import 'package:naked_ui/naked_ui.dart';

enum RadioOption { banana, apple }

class RadioExample extends StatefulWidget {
  const RadioExample({super.key});

  @override
  State<RadioExample> createState() => _RadioExampleState();
}

class _RadioExampleState extends State<RadioExample> {
  RadioOption _selected = RadioOption.banana;

  @override
  Widget build(BuildContext context) {
    return RadioGroup<RadioOption>(
      groupValue: _selected,
      onChanged: (value) => setState(() => _selected = value!),
      child: const Row(
        mainAxisSize: MainAxisSize.min,
        spacing: 12,
        children: [
          _RadioChip(value: RadioOption.banana, label: 'Banana'),
          _RadioChip(value: RadioOption.apple, label: 'Apple'),
        ],
      ),
    );
  }
}

class _RadioChip extends StatelessWidget {
  const _RadioChip({required this.value, required this.label});

  final RadioOption value;
  final String label;

  @override
  Widget build(BuildContext context) {
    return NakedRadio<RadioOption>(
      value: value,
      builder: (context, state, _) {
        final bool isFocused = state.isFocused;

        final backgroundColor = state.when(
          selected: Colors.blue.shade600,
          hovered: Colors.grey.shade200,
          orElse: Colors.white,
        );

        return AnimatedContainer(
          duration: const Duration(milliseconds: 150),
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
          decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(20),
            color: backgroundColor,
            border: Border.all(
              color: state.when(
                focused: Colors.blue,
                orElse: Colors.grey.shade300,
              ),
              width: isFocused ? 2 : 1,
            ),
          ),
          child: Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              Text(
                label,
                style: TextStyle(
                  color: state.isSelected ? Colors.white : Colors.grey.shade800,
                  fontWeight: FontWeight.w600,
                ),
              ),
              if (state.isSelected) ...[
                const SizedBox(width: 6),
                const Icon(Icons.check, size: 16, color: Colors.white),
              ],
            ],
          ),
        );
      },
    );
  }
}
```

## Typed Radio State

`NakedRadioState<T>` (delivered to the builder) exposes:

- `value` → radio value for this option
- `isSelected` → convenience getter for selection state
- `widgetStates` → hover/focus/pressed/disabled information

### Access from Context

- `NakedRadioState.of<T>(context)` / `maybeOf<T>(context)` → obtain the nearest typed state
- `NakedRadioState.controllerOf(context)` / `maybeControllerOf(context)` → access the underlying `WidgetStatesController` when you need listeners outside the builder

## RadioGroup Constructor

```dart
const RadioGroup({
  Key? key,
  required this.groupValue,
  required this.onChanged,
  required this.child,
})
```

- `groupValue` → the selected value (nullable to allow “no selection”)
- `onChanged` → receives new value when a radio activates
- `child` → subtree that contains your `NakedRadio` widgets

## NakedRadio Constructor

```dart
const NakedRadio<T>({
  Key? key,
  required this.value,
  this.child,
  this.enabled = true,
  this.mouseCursor,
  this.focusNode,
  this.autofocus = false,
  this.toggleable = false,
  this.onFocusChange,
  this.onHoverChange,
  this.onPressChange,
  this.builder,
  this.groupRegistry,
  this.semanticLabel,
  this.excludeSemantics = false,
})
```

Key notes:

- Provide either `child` (static content) or `builder` (dynamic styling via `NakedRadioState`)
- `builder` → `ValueWidgetBuilder<NakedRadioState<T>>?`
- `toggleable` lets the selected radio clear the group when activated again
- Pass `focusNode` to integrate with external focus management
- interaction callbacks: `onPressChange`/`onHoverChange`/`onFocusChange` (prefer `builder` pattern)
- `semanticLabel` → override the accessible name for the radio
- `excludeSemantics` → hide the radio and its descendants from the semantic tree

## Accessibility Tips

- Ensure each radio exposes a label (e.g. text within the builder)
- Default keyboard support: arrow keys move between radios, Space selects
- Keep your focus/hover styles visible using `state.isFocused` / `state.isHovered`
- For more advanced layouts, wrap radio groups in `FocusTraversalGroup` to control traversal order
