NakedRadio
Headless radio button primitives backed by RadioGroup for exclusive selection
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
Full examples are available in example/lib/api/naked_radio.0.dart
.
Basic implementation
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 optionisSelected
→ convenience getter for selection statewidgetStates
→ hover/focus/pressed/disabled information
Access from Context
NakedRadioState.of<T>(context)
/maybeOf<T>(context)
→ obtain the nearest typed stateNakedRadioState.controllerOf(context)
/maybeControllerOf(context)
→ access the underlyingWidgetStatesController
when you need listeners outside the builder
RadioGroup Constructor
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 yourNakedRadio
widgets
NakedRadio Constructor
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,
})
Key notes:
- Provide either
child
(static content) orbuilder
(dynamic styling viaNakedRadioState
) 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
(preferbuilder
pattern)
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