Headless radio button component. RadioGroup coordinates keyboard navigation and exclusive selection. NakedRadio provides state for custom styling.
- 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
Information
Full examples are available in packages/example/lib/api/naked_radio.0.dart.
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),
],
],
),
);
},
);
}
}NakedRadioState<T> (delivered to the builder) exposes:
value→ radio value for this optionisSelected→ convenience getter for selection statewidgetStates→ hover/focus/pressed/disabled information
NakedRadioState.of<T>(context)/maybeOf<T>(context)→ obtain the nearest typed stateNakedRadioState.controllerOf(context)/maybeControllerOf(context)→ access the underlyingWidgetStatesControllerwhen you need listeners outside the builder
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 activateschild→ subtree that contains yourNakedRadiowidgets
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) orbuilder(dynamic styling viaNakedRadioState) builder→ValueWidgetBuilder<NakedRadioState<T>>?toggleablelets the selected radio clear the group when activated again- Pass
focusNodeto integrate with external focus management - interaction callbacks:
onPressChange/onHoverChange/onFocusChange(preferbuilderpattern) semanticLabel→ override the accessible name for the radioexcludeSemantics→ hide the radio and its descendants from the semantic tree
- 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
FocusTraversalGroupto control traversal order