Repository: ubuntu/yaru.dart

Issue:
Add YaruRadio




Fix patch:
diff --git a/example/lib/pages/controls_page.dart b/example/lib/pages/controls_page.dart
index ecff17e8b..7c3916460 100644
--- a/example/lib/pages/controls_page.dart
+++ b/example/lib/pages/controls_page.dart
@@ -9,30 +9,49 @@ class ControlsPage extends StatefulWidget {
 }
 
 class _ControlsPageState extends State<ControlsPage> {
-  final List<bool?> _values = [false, null, true];
+  final List<bool?> _checkboxValues = [false, null, true];
+  int? _radioValue = 1;
 
   @override
   Widget build(BuildContext context) {
     return ListView(
       padding: const EdgeInsets.all(kYaruPagePadding),
       children: [
-        for (var i = 0; i < _values.length; ++i) ...[
-          Center(
-            child: Row(
-              children: [
-                YaruCheckbox(
-                  value: _values[i],
-                  onChanged: (v) => setState(() => _values[i] = v),
-                  tristate: true,
-                ),
-                const SizedBox(width: 10),
-                YaruCheckbox(
-                  value: _values[i],
-                  onChanged: null,
-                  tristate: true,
-                )
-              ],
-            ),
+        for (var i = 0; i < _checkboxValues.length; ++i) ...[
+          Row(
+            children: [
+              YaruCheckbox(
+                value: _checkboxValues[i],
+                onChanged: (v) => setState(() => _checkboxValues[i] = v),
+                tristate: true,
+              ),
+              const SizedBox(width: 10),
+              YaruCheckbox(
+                value: _checkboxValues[i],
+                onChanged: null,
+                tristate: true,
+              )
+            ],
+          ),
+          const SizedBox(height: 10),
+        ],
+        for (var i = 0; i < 3; ++i) ...[
+          Row(
+            children: [
+              YaruRadio<int>(
+                value: i,
+                groupValue: _radioValue,
+                onChanged: (v) => setState(() => _radioValue = v),
+                toggleable: true,
+              ),
+              const SizedBox(width: 10),
+              YaruRadio<int>(
+                value: i,
+                groupValue: _radioValue,
+                onChanged: null,
+                toggleable: true,
+              ),
+            ],
           ),
           const SizedBox(height: 10),
         ],
diff --git a/example/lib/pages/radio_button_page.dart b/example/lib/pages/radio_button_page.dart
index 3d973f2e3..d420464af 100644
--- a/example/lib/pages/radio_button_page.dart
+++ b/example/lib/pages/radio_button_page.dart
@@ -9,7 +9,7 @@ class RadioButtonPage extends StatefulWidget {
 }
 
 class _RadioButtonPageState extends State<RadioButtonPage> {
-  int _value = 0;
+  int? _value = 0;
 
   @override
   Widget build(BuildContext context) {
@@ -20,7 +20,8 @@ class _RadioButtonPageState extends State<RadioButtonPage> {
           YaruRadioButton<int>(
             value: i,
             groupValue: _value,
-            onChanged: (v) => setState(() => _value = v!),
+            onChanged: (v) => setState(() => _value = v),
+            toggleable: true,
             title: const Text('YaruRadioButton'),
           ),
           const SizedBox(height: 10),
diff --git a/lib/src/constants.dart b/lib/src/constants.dart
index 34a62dc0f..76e0afa33 100644
--- a/lib/src/constants.dart
+++ b/lib/src/constants.dart
@@ -13,3 +13,7 @@ const kYaruButtonRadius = 6.0;
 /// The default breakpoint width [YaruMasterDetailPage] uses for switching
 /// between portrait and landscape modes.
 const kYaruMasterDetailBreakpoint = 620.0;
+
+// Used by tooglable widgets to resize the canvas on active state
+// Need to be an even number
+const kCheckRadioActiveResizeFactor = 2;
diff --git a/lib/src/controls/yaru_checkbox.dart b/lib/src/controls/yaru_checkbox.dart
index f8b72b13e..914690418 100644
--- a/lib/src/controls/yaru_checkbox.dart
+++ b/lib/src/controls/yaru_checkbox.dart
@@ -1,184 +1,38 @@
 import 'package:flutter/material.dart';
 
-const _kActivableAreaPadding = EdgeInsets.all(5);
-const _kCheckboxSize = Size.square(22);
+import '../constants.dart';
+import 'yaru_togglable.dart';
+
 const _kCheckboxBorderRadius = Radius.circular(4);
 const _kCheckboxDashStroke = 2.0;
 const _kDashSizeFactor = 0.52;
-const _kCheckboxActiveResizeFactor = 2; // Need to be an even number
-const _kCheckboxAnimationDuration = Duration(milliseconds: 150);
-const _kCheckboxSizeAnimationDuration = Duration(milliseconds: 100);
-const _kIndicatorAnimationDuration = Duration(milliseconds: 200);
-const _kIndicatorRadius = 20.0;
 
-class YaruCheckbox extends StatefulWidget {
+class YaruCheckbox extends YaruTogglable<bool?> {
   const YaruCheckbox({
     super.key,
-    required this.value,
-    this.tristate = false,
-    required this.onChanged,
-    this.focusNode,
-    this.autofocus = false,
+    required super.value,
+    super.tristate,
+    super.onChanged,
+    super.focusNode,
+    super.autofocus,
   }) : assert(tristate || value != null);
 
-  final bool? value;
-
-  final bool tristate;
-
-  final ValueChanged<bool?>? onChanged;
-
-  final FocusNode? focusNode;
-
-  final bool autofocus;
+  @override
+  bool? get checked => value;
 
   @override
-  State<YaruCheckbox> createState() {
+  YaruTogglableState<YaruCheckbox> createState() {
     return _YaruCheckboxState();
   }
 }
 
-class _YaruCheckboxState extends State<YaruCheckbox>
-    with TickerProviderStateMixin {
-  bool _hover = false;
-  bool _focus = false;
-  bool _active = false;
-  bool? _oldValue;
-
-  bool get _interactive => widget.onChanged != null;
-
-  late CurvedAnimation _position;
-  late AnimationController _positionController;
-
-  late CurvedAnimation _sizePosition;
-  late AnimationController _sizeController;
-
-  late CurvedAnimation _indicatorPosition;
-  late AnimationController _indicatorController;
-
-  late final Map<Type, Action<Intent>> _actionMap = <Type, Action<Intent>>{
-    ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: _handleTap),
-  };
-
-  @override
-  void initState() {
-    super.initState();
-
-    _oldValue = widget.value;
-
-    _positionController = AnimationController(
-      duration: _kCheckboxAnimationDuration,
-      value: widget.value == false ? 0.0 : 1.0,
-      vsync: this,
-    );
-    _position = CurvedAnimation(
-      parent: _positionController,
-      curve: Curves.easeInQuad,
-      reverseCurve: Curves.easeOutQuad,
-    );
-
-    _sizeController = AnimationController(
-      duration: _kCheckboxSizeAnimationDuration,
-      vsync: this,
-    );
-    _sizePosition = CurvedAnimation(
-      parent: _sizeController,
-      curve: Curves.easeIn,
-      reverseCurve: Curves.easeOut,
-    );
-
-    _indicatorController = AnimationController(
-      duration: _kIndicatorAnimationDuration,
-      vsync: this,
-    );
-    _indicatorPosition = CurvedAnimation(
-      parent: _indicatorController,
-      curve: Curves.fastOutSlowIn,
-    );
-  }
-
+class _YaruCheckboxState extends YaruTogglableState<YaruCheckbox> {
   @override
-  void didUpdateWidget(covariant YaruCheckbox oldWidget) {
-    super.didUpdateWidget(oldWidget);
-
-    if (oldWidget.value != widget.value) {
-      _oldValue = oldWidget.value;
-
-      if (widget.tristate) {
-        if (widget.value == null) {
-          _positionController.value = 0.0;
-        }
-        if (widget.value ?? true) {
-          _positionController.forward();
-        } else {
-          _positionController.reverse();
-        }
-      } else {
-        if (widget.value ?? false) {
-          _positionController.forward();
-        } else {
-          _positionController.reverse();
-        }
-      }
-
-      _sizeController.forward().then((_) {
-        _sizeController.reverse();
-      });
-    }
-  }
+  bool get interactive => widget.onChanged != null;
 
   @override
-  void dispose() {
-    _positionController.dispose();
-    _indicatorController.dispose();
-    _sizeController.dispose();
-
-    super.dispose();
-  }
-
-  void _handleFocusChange(bool focus) {
-    if (focus == _focus) {
-      return;
-    }
-
-    setState(() => _focus = focus);
-
-    if (_focus) {
-      _indicatorController.forward();
-    } else {
-      _indicatorController.reverse();
-    }
-  }
-
-  void _handleHoverChange(bool hover) {
-    if (hover == _hover) {
-      return;
-    }
-
-    setState(() => _hover = hover);
-
-    if (_hover) {
-      _indicatorController.forward();
-    } else {
-      _indicatorController.reverse();
-    }
-  }
-
-  void _handleActiveChange(bool active) {
-    if (active == _active) {
-      return;
-    }
-
-    setState(() => _active = active);
-
-    if (_active) {
-      _sizeController.forward();
-    } else {
-      _sizeController.reverse();
-    }
-  }
-
-  void _handleTap([Intent? _]) {
-    if (!_interactive) {
+  void handleTap([Intent? _]) {
+    if (!interactive) {
       return;
     }
     switch (widget.value) {
@@ -196,157 +50,37 @@ class _YaruCheckboxState extends State<YaruCheckbox>
 
   @override
   Widget build(BuildContext context) {
-    final colorScheme = Theme.of(context).colorScheme;
-
-    // Normal colors
-    final uncheckedColor = colorScheme.onSurface.withOpacity(.3);
-    final checkedColor = colorScheme.primary;
-    final checkmarkColor = colorScheme.onPrimary;
-
-    // Disabled colors
-    final uncheckedDisabledColor = colorScheme.onSurface.withOpacity(.1);
-    final checkedDisabledColor = colorScheme.onSurface.withOpacity(.2);
-    final checkmarkDisabledColor = colorScheme.onSurface.withOpacity(.5);
-
-    // Indicator colors
-    final hoverIndicatorColor = colorScheme.onSurface.withOpacity(.05);
-    final focusIndicatorColor = colorScheme.onSurface.withOpacity(.1);
-
-    return _buildSemantics(
-      child: _buildEventDetectors(
-        child: Padding(
-          padding: _kActivableAreaPadding,
-          child: AnimatedBuilder(
-            animation: Listenable.merge([
-              _indicatorController,
-              _sizeController,
-            ]),
-            builder: (context, child) => CustomPaint(
-              size: _kCheckboxSize,
-              painter: _YaruCheckboxPainter(
-                interactive: _interactive,
-                hover: _hover,
-                focus: _focus,
-                active: _active,
-                value: widget.value,
-                oldValue: _oldValue,
-                position: _position,
-                sizePosition: _sizePosition,
-                indicatorPosition: _indicatorPosition,
-                uncheckedColor: uncheckedColor,
-                checkedColor: checkedColor,
-                checkmarkColor: checkmarkColor,
-                uncheckedDisabledColor: uncheckedDisabledColor,
-                checkedDisabledColor: checkedDisabledColor,
-                checkmarkDisabledColor: checkmarkDisabledColor,
-                hoverIndicatorColor: hoverIndicatorColor,
-                focusIndicatorColor: focusIndicatorColor,
-              ),
-            ),
-          ),
-        ),
-      ),
-    );
-  }
-
-  Widget _buildSemantics({required Widget child}) {
-    return Semantics(
-      checked: widget.value ?? false,
-      enabled: _interactive,
-      child: child,
-    );
-  }
-
-  Widget _buildEventDetectors({required Widget child}) {
-    return FocusableActionDetector(
-      actions: _actionMap,
-      enabled: _interactive,
-      focusNode: widget.focusNode,
-      autofocus: widget.autofocus,
-      onShowFocusHighlight: _handleFocusChange,
-      onShowHoverHighlight: _handleHoverChange,
-      mouseCursor:
-          _interactive ? SystemMouseCursors.click : SystemMouseCursors.basic,
-      child: GestureDetector(
-        excludeFromSemantics: !_interactive,
-        onTapDown: (_) => _handleActiveChange(_interactive),
-        onTap: _handleTap,
-        onTapUp: (_) => _handleActiveChange(false),
-        onTapCancel: () => _handleActiveChange(false),
-        child: AbsorbPointer(
-          child: child,
-        ),
-      ),
-    );
+    return buildToggleable(_YaruCheckboxPainter());
   }
 }
 
-class _YaruCheckboxPainter extends CustomPainter {
-  _YaruCheckboxPainter({
-    required this.interactive,
-    required this.hover,
-    required this.focus,
-    required this.active,
-    required this.value,
-    required this.oldValue,
-    required this.position,
-    required this.sizePosition,
-    required this.indicatorPosition,
-    required this.uncheckedColor,
-    required this.checkedColor,
-    required this.checkmarkColor,
-    required this.uncheckedDisabledColor,
-    required this.checkedDisabledColor,
-    required this.checkmarkDisabledColor,
-    required this.hoverIndicatorColor,
-    required this.focusIndicatorColor,
-  });
-
-  final bool interactive;
-  final bool hover;
-  final bool focus;
-  final bool active;
-  final bool? value;
-  final bool? oldValue;
-
-  final CurvedAnimation position;
-  final CurvedAnimation sizePosition;
-  final CurvedAnimation indicatorPosition;
-
-  final Color uncheckedColor;
-  final Color checkedColor;
-  final Color checkmarkColor;
-  final Color uncheckedDisabledColor;
-  final Color checkedDisabledColor;
-  final Color checkmarkDisabledColor;
-  final Color hoverIndicatorColor;
-  final Color focusIndicatorColor;
-
+class _YaruCheckboxPainter extends YaruTogglablePainter {
   @override
-  void paint(Canvas canvas, Size canvasSize) {
+  void paint(Canvas canvas, Size size) {
+    final canvasSize = size;
     final t = position.value;
     final drawingOrigin = Offset(
-      _kCheckboxActiveResizeFactor / 2 * sizePosition.value,
-      _kCheckboxActiveResizeFactor / 2 * sizePosition.value,
+      kCheckRadioActiveResizeFactor / 2 * sizePosition.value,
+      kCheckRadioActiveResizeFactor / 2 * sizePosition.value,
     );
     final drawingSize = Size(
-      canvasSize.width - _kCheckboxActiveResizeFactor * sizePosition.value,
-      canvasSize.height - _kCheckboxActiveResizeFactor * sizePosition.value,
+      canvasSize.width - kCheckRadioActiveResizeFactor * sizePosition.value,
+      canvasSize.height - kCheckRadioActiveResizeFactor * sizePosition.value,
     );
 
-    _drawStateIndicator(canvas, canvasSize);
+    drawStateIndicator(canvas, canvasSize);
     _drawBox(
       canvas,
       drawingSize,
       drawingOrigin,
-      oldValue == false || value == false ? t : 1,
+      oldChecked == false || checked == false ? t : 1,
     );
 
     // Four cases: false to null, false to true, null to false, true to false
-    if (oldValue == false || value == false) {
-      if (oldValue == true || value == true) {
+    if (oldChecked == false || checked == false) {
+      if (oldChecked == true || checked == true) {
         _drawCheckMark(canvas, drawingSize, drawingOrigin, t);
-      } else if (oldValue == null || value == null) {
+      } else if (oldChecked == null || checked == null) {
         _drawDash(canvas, drawingSize, drawingOrigin, t);
       }
     }
@@ -354,14 +88,14 @@ class _YaruCheckboxPainter extends CustomPainter {
     else {
       if (t <= 0.5) {
         final tShrink = 1 - t * 2;
-        if (oldValue == true) {
+        if (oldChecked == true) {
           _drawCheckMark(canvas, drawingSize, drawingOrigin, tShrink);
         } else {
           _drawDash(canvas, drawingSize, drawingOrigin, tShrink);
         }
       } else {
         final tExpand = (t - 0.5) * 2.0;
-        if (value == true) {
+        if (checked == true) {
           _drawCheckMark(canvas, drawingSize, drawingOrigin, tExpand);
         } else {
           _drawDash(canvas, drawingSize, drawingOrigin, tExpand);
@@ -370,21 +104,6 @@ class _YaruCheckboxPainter extends CustomPainter {
     }
   }
 
-  void _drawStateIndicator(Canvas canvas, Size canvasSize) {
-    if (interactive) {
-      final color = focus ? focusIndicatorColor : hoverIndicatorColor;
-
-      canvas.drawCircle(
-        Offset(canvasSize.width / 2, canvasSize.height / 2),
-        _kIndicatorRadius,
-        Paint()
-          ..color =
-              Color.lerp(Colors.transparent, color, indicatorPosition.value)!
-          ..style = PaintingStyle.fill,
-      );
-    }
-  }
-
   void _drawBox(Canvas canvas, Size size, Offset origin, double t) {
     canvas.drawRRect(
       RRect.fromRectAndRadius(
@@ -451,7 +170,4 @@ class _YaruCheckboxPainter extends CustomPainter {
       ..style = PaintingStyle.stroke
       ..strokeWidth = _kCheckboxDashStroke;
   }
-
-  @override
-  bool shouldRepaint(covariant _YaruCheckboxPainter oldDelegate) => true;
 }
diff --git a/lib/src/controls/yaru_radio.dart b/lib/src/controls/yaru_radio.dart
new file mode 100644
index 000000000..348ccb8ae
--- /dev/null
+++ b/lib/src/controls/yaru_radio.dart
@@ -0,0 +1,102 @@
+import 'package:flutter/material.dart';
+
+import '../constants.dart';
+import 'yaru_togglable.dart';
+
+const _kDotSizeFactor = 0.4;
+
+class YaruRadio<T> extends YaruTogglable<T> {
+  const YaruRadio({
+    super.key,
+    required super.value,
+    required this.groupValue,
+    this.toggleable = false,
+    required super.onChanged,
+    super.focusNode,
+    super.autofocus,
+  }) : assert(toggleable || value != null);
+
+  final bool toggleable;
+
+  @override
+  bool get checked => value == groupValue;
+
+  final T? groupValue;
+
+  @override
+  YaruTogglableState<YaruRadio<T>> createState() {
+    return _YaruRadioState<T>();
+  }
+}
+
+class _YaruRadioState<T> extends YaruTogglableState<YaruRadio<T>> {
+  @override
+  void handleTap([Intent? _]) {
+    if (!interactive) {
+      return;
+    }
+
+    if (widget.groupValue != widget.value || !widget.toggleable) {
+      widget.onChanged!(widget.value);
+    } else if (widget.toggleable) {
+      widget.onChanged!(null);
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return buildToggleable(_YaruRadioPainter());
+  }
+}
+
+class _YaruRadioPainter extends YaruTogglablePainter {
+  @override
+  void paint(Canvas canvas, Size size) {
+    final canvasSize = size;
+    final t = position.value;
+    final drawingOrigin = Offset(
+      kCheckRadioActiveResizeFactor / 2 * sizePosition.value,
+      kCheckRadioActiveResizeFactor / 2 * sizePosition.value,
+    );
+    final drawingSize = Size(
+      canvasSize.width - kCheckRadioActiveResizeFactor * sizePosition.value,
+      canvasSize.height - kCheckRadioActiveResizeFactor * sizePosition.value,
+    );
+
+    drawStateIndicator(canvas, canvasSize);
+    _drawBox(canvas, drawingSize, drawingOrigin, t);
+    _drawDot(canvas, drawingSize, drawingOrigin, t);
+  }
+
+  void _drawBox(Canvas canvas, Size size, Offset origin, double t) {
+    canvas.drawOval(
+      Rect.fromLTWH(
+        origin.dx,
+        origin.dy,
+        size.width,
+        size.height,
+      ),
+      Paint()
+        ..color = interactive
+            ? Color.lerp(uncheckedColor, checkedColor, t)!
+            : Color.lerp(uncheckedDisabledColor, checkedDisabledColor, t)!
+        ..style = PaintingStyle.fill,
+    );
+  }
+
+  void _drawDot(Canvas canvas, Size size, Offset origin, double t) {
+    final center = (Offset.zero & size).center + origin;
+    final dotSize = size * _kDotSizeFactor;
+
+    canvas.drawOval(
+      Rect.fromCenter(
+        center: center,
+        width: dotSize.width * t,
+        height: dotSize.height * t,
+      ),
+      Paint()
+        ..color = interactive ? checkmarkColor : checkmarkDisabledColor
+        ..style = PaintingStyle.fill,
+    );
+  }
+}
diff --git a/lib/src/controls/yaru_radio_button.dart b/lib/src/controls/yaru_radio_button.dart
index ca7e25aa6..30d7a602f 100644
--- a/lib/src/controls/yaru_radio_button.dart
+++ b/lib/src/controls/yaru_radio_button.dart
@@ -1,5 +1,6 @@
 import 'package:flutter/material.dart';
 
+import 'yaru_radio.dart';
 import 'yaru_toggle_button.dart';
 
 /// A desktop style radio button with an interactive label.
@@ -54,7 +55,7 @@ class YaruRadioButton<T> extends StatelessWidget {
       leading: SizedBox.square(
         dimension: kMinInteractiveDimension - 8,
         child: Center(
-          child: Radio<T>(
+          child: YaruRadio<T>(
             value: value,
             groupValue: groupValue,
             onChanged: onChanged,
diff --git a/lib/src/controls/yaru_togglable.dart b/lib/src/controls/yaru_togglable.dart
new file mode 100644
index 000000000..b74b22d4b
--- /dev/null
+++ b/lib/src/controls/yaru_togglable.dart
@@ -0,0 +1,336 @@
+import 'package:flutter/material.dart';
+
+const _kActivableAreaPadding = EdgeInsets.all(6);
+const _kTogglableSize = Size.square(20);
+const _kTogglableAnimationDuration = Duration(milliseconds: 150);
+const _kTogglableSizeAnimationDuration = Duration(milliseconds: 100);
+const _kIndicatorAnimationDuration = Duration(milliseconds: 200);
+const _kIndicatorRadius = 20.0;
+
+abstract class YaruTogglable<T> extends StatefulWidget {
+  const YaruTogglable({
+    super.key,
+    required this.value,
+    this.tristate = false,
+    this.onChanged,
+    this.focusNode,
+    this.autofocus = false,
+  });
+
+  final T value;
+
+  final bool tristate;
+
+  bool? get checked;
+
+  final ValueChanged<T?>? onChanged;
+
+  bool get interactive => onChanged != null;
+
+  final FocusNode? focusNode;
+
+  final bool autofocus;
+}
+
+abstract class YaruTogglableState<S extends YaruTogglable> extends State<S>
+    with TickerProviderStateMixin {
+  bool hover = false;
+  bool focus = false;
+  bool active = false;
+  bool? oldChecked;
+
+  bool get interactive => widget.interactive;
+
+  late CurvedAnimation _position;
+  late AnimationController _positionController;
+
+  late CurvedAnimation _sizePosition;
+  late AnimationController _sizeController;
+
+  late CurvedAnimation _indicatorPosition;
+  late AnimationController _indicatorController;
+
+  late final Map<Type, Action<Intent>> _actionMap = <Type, Action<Intent>>{
+    ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: handleTap),
+  };
+
+  @override
+  void initState() {
+    super.initState();
+
+    oldChecked = widget.checked;
+
+    _positionController = AnimationController(
+      duration: _kTogglableAnimationDuration,
+      value: widget.checked == false ? 0.0 : 1.0,
+      vsync: this,
+    );
+    _position = CurvedAnimation(
+      parent: _positionController,
+      curve: Curves.easeInQuad,
+      reverseCurve: Curves.easeOutQuad,
+    );
+
+    _sizeController = AnimationController(
+      duration: _kTogglableSizeAnimationDuration,
+      vsync: this,
+    );
+    _sizePosition = CurvedAnimation(
+      parent: _sizeController,
+      curve: Curves.easeIn,
+      reverseCurve: Curves.easeOut,
+    );
+
+    _indicatorController = AnimationController(
+      duration: _kIndicatorAnimationDuration,
+      vsync: this,
+    );
+    _indicatorPosition = CurvedAnimation(
+      parent: _indicatorController,
+      curve: Curves.fastOutSlowIn,
+    );
+  }
+
+  @override
+  void didUpdateWidget(covariant S oldWidget) {
+    super.didUpdateWidget(oldWidget);
+
+    if (oldWidget.checked != widget.checked) {
+      oldChecked = oldWidget.checked;
+
+      if (widget.tristate) {
+        if (widget.checked == null) {
+          _positionController.value = 0.0;
+        }
+        if (widget.checked ?? true) {
+          _positionController.forward();
+        } else {
+          _positionController.reverse();
+        }
+      } else {
+        if (widget.checked ?? false) {
+          _positionController.forward();
+        } else {
+          _positionController.reverse();
+        }
+      }
+
+      _sizeController.forward().then((_) {
+        _sizeController.reverse();
+      });
+    }
+  }
+
+  @override
+  void dispose() {
+    _positionController.dispose();
+    _indicatorController.dispose();
+    _sizeController.dispose();
+
+    super.dispose();
+  }
+
+  void _handleFocusChange(bool value) {
+    if (focus == value) {
+      return;
+    }
+
+    setState(() => focus = value);
+
+    if (focus) {
+      _indicatorController.forward();
+    } else {
+      _indicatorController.reverse();
+    }
+  }
+
+  void _handleHoverChange(bool value) {
+    if (hover == value) {
+      return;
+    }
+
+    setState(() => hover = value);
+
+    if (hover) {
+      _indicatorController.forward();
+    } else {
+      _indicatorController.reverse();
+    }
+  }
+
+  void _handleActiveChange(bool value) {
+    if (active == value) {
+      return;
+    }
+
+    setState(() => active = value);
+
+    if (active) {
+      _sizeController.forward();
+    } else {
+      _sizeController.reverse();
+    }
+  }
+
+  void handleTap([Intent? _]);
+
+  Widget _buildSemantics({required Widget child}) {
+    return Semantics(
+      checked: widget.checked ?? false,
+      enabled: interactive,
+      child: child,
+    );
+  }
+
+  Widget _buildEventDetectors({required Widget child}) {
+    return FocusableActionDetector(
+      actions: _actionMap,
+      enabled: interactive,
+      focusNode: widget.focusNode,
+      autofocus: widget.autofocus,
+      onShowFocusHighlight: _handleFocusChange,
+      onShowHoverHighlight: _handleHoverChange,
+      mouseCursor:
+          interactive ? SystemMouseCursors.click : SystemMouseCursors.basic,
+      child: GestureDetector(
+        excludeFromSemantics: !interactive,
+        onTapDown: (_) => _handleActiveChange(interactive),
+        onTap: handleTap,
+        onTapUp: (_) => _handleActiveChange(false),
+        onTapCancel: () => _handleActiveChange(false),
+        child: AbsorbPointer(
+          child: child,
+        ),
+      ),
+    );
+  }
+
+  Widget buildToggleable(YaruTogglablePainter painter) {
+    final colorScheme = Theme.of(context).colorScheme;
+
+    // Normal colors
+    final uncheckedColor = colorScheme.onSurface.withOpacity(.3);
+    final checkedColor = colorScheme.primary;
+    final checkmarkColor = colorScheme.onPrimary;
+
+    // Disabled colors
+    final uncheckedDisabledColor = colorScheme.onSurface.withOpacity(.1);
+    final checkedDisabledColor = colorScheme.onSurface.withOpacity(.2);
+    final checkmarkDisabledColor = colorScheme.onSurface.withOpacity(.5);
+
+    // Indicator colors
+    final hoverIndicatorColor = colorScheme.onSurface.withOpacity(.05);
+    final focusIndicatorColor = colorScheme.onSurface.withOpacity(.1);
+
+    return _buildSemantics(
+      child: _buildEventDetectors(
+        child: Padding(
+          padding: _kActivableAreaPadding,
+          child: CustomPaint(
+            size: _kTogglableSize,
+            painter: painter
+              ..interactive = interactive
+              ..hover = hover
+              ..focus = focus
+              ..active = active
+              ..checked = widget.checked
+              ..oldChecked = oldChecked
+              ..position = _position
+              ..sizePosition = _sizePosition
+              ..indicatorPosition = _indicatorPosition
+              ..uncheckedColor = uncheckedColor
+              ..checkedColor = checkedColor
+              ..checkmarkColor = checkmarkColor
+              ..uncheckedDisabledColor = uncheckedDisabledColor
+              ..checkedDisabledColor = checkedDisabledColor
+              ..checkmarkDisabledColor = checkmarkDisabledColor
+              ..hoverIndicatorColor = hoverIndicatorColor
+              ..focusIndicatorColor = focusIndicatorColor,
+          ),
+        ),
+      ),
+    );
+  }
+}
+
+abstract class YaruTogglablePainter extends ChangeNotifier
+    implements CustomPainter {
+  late bool interactive;
+  late bool hover;
+  late bool focus;
+  late bool active;
+  late bool? checked;
+  late bool? oldChecked;
+
+  late Color uncheckedColor;
+  late Color checkedColor;
+  late Color checkmarkColor;
+  late Color uncheckedDisabledColor;
+  late Color checkedDisabledColor;
+  late Color checkmarkDisabledColor;
+  late Color hoverIndicatorColor;
+  late Color focusIndicatorColor;
+
+  Animation<double> get position => _position!;
+  Animation<double>? _position;
+  set position(Animation<double> value) {
+    if (value == _position) {
+      return;
+    }
+    _position?.removeListener(notifyListeners);
+    value.addListener(notifyListeners);
+    _position = value;
+    notifyListeners();
+  }
+
+  Animation<double> get sizePosition => _sizePosition!;
+  Animation<double>? _sizePosition;
+  set sizePosition(Animation<double> value) {
+    if (value == _sizePosition) {
+      return;
+    }
+    _sizePosition?.removeListener(notifyListeners);
+    value.addListener(notifyListeners);
+    _sizePosition = value;
+    notifyListeners();
+  }
+
+  Animation<double> get indicatorPosition => _indicatorPosition!;
+  Animation<double>? _indicatorPosition;
+  set indicatorPosition(Animation<double> value) {
+    if (value == _indicatorPosition) {
+      return;
+    }
+    _indicatorPosition?.removeListener(notifyListeners);
+    value.addListener(notifyListeners);
+    _indicatorPosition = value;
+    notifyListeners();
+  }
+
+  void drawStateIndicator(Canvas canvas, Size canvasSize) {
+    if (interactive) {
+      final color = focus ? focusIndicatorColor : hoverIndicatorColor;
+
+      canvas.drawCircle(
+        Offset(canvasSize.width / 2, canvasSize.height / 2),
+        _kIndicatorRadius,
+        Paint()
+          ..color =
+              Color.lerp(Colors.transparent, color, indicatorPosition.value)!
+          ..style = PaintingStyle.fill,
+      );
+    }
+  }
+
+  @override
+  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
+
+  @override
+  bool? hitTest(Offset position) => null;
+
+  @override
+  SemanticsBuilderCallback? get semanticsBuilder => null;
+
+  @override
+  bool shouldRebuildSemantics(covariant CustomPainter oldDelegate) => false;
+}
diff --git a/lib/yaru_widgets.dart b/lib/yaru_widgets.dart
index fe5d12929..14027d1b3 100644
--- a/lib/yaru_widgets.dart
+++ b/lib/yaru_widgets.dart
@@ -12,6 +12,7 @@ export 'src/controls/yaru_icon_button.dart';
 export 'src/controls/yaru_option_button.dart';
 export 'src/controls/yaru_popup_menu_button.dart';
 export 'src/controls/yaru_progress_indicator.dart';
+export 'src/controls/yaru_radio.dart';
 export 'src/controls/yaru_radio_button.dart';
 export 'src/controls/yaru_switch_button.dart';
 export 'src/controls/yaru_title_bar.dart';


Test patch:
diff --git a/test/controls/goldens/yaru_check_button-checked-dark.png b/test/controls/goldens/yaru_check_button-checked-dark.png
index 6e097bf0c..02bcc38b4 100644
Binary files a/test/controls/goldens/yaru_check_button-checked-dark.png and b/test/controls/goldens/yaru_check_button-checked-dark.png differ
diff --git a/test/controls/goldens/yaru_check_button-checked-disabled-dark.png b/test/controls/goldens/yaru_check_button-checked-disabled-dark.png
index 17364b8b2..a14821a60 100644
Binary files a/test/controls/goldens/yaru_check_button-checked-disabled-dark.png and b/test/controls/goldens/yaru_check_button-checked-disabled-dark.png differ
diff --git a/test/controls/goldens/yaru_check_button-checked-disabled-light.png b/test/controls/goldens/yaru_check_button-checked-disabled-light.png
index 6a7c3294a..54b2c197f 100644
Binary files a/test/controls/goldens/yaru_check_button-checked-disabled-light.png and b/test/controls/goldens/yaru_check_button-checked-disabled-light.png differ
diff --git a/test/controls/goldens/yaru_check_button-checked-focused-dark.png b/test/controls/goldens/yaru_check_button-checked-focused-dark.png
index e46c96fe2..6c6078259 100644
Binary files a/test/controls/goldens/yaru_check_button-checked-focused-dark.png and b/test/controls/goldens/yaru_check_button-checked-focused-dark.png differ
diff --git a/test/controls/goldens/yaru_check_button-checked-focused-light.png b/test/controls/goldens/yaru_check_button-checked-focused-light.png
index 60f1574b7..67621fc66 100644
Binary files a/test/controls/goldens/yaru_check_button-checked-focused-light.png and b/test/controls/goldens/yaru_check_button-checked-focused-light.png differ
diff --git a/test/controls/goldens/yaru_check_button-checked-hovered-dark.png b/test/controls/goldens/yaru_check_button-checked-hovered-dark.png
index 059913ba0..a99d39e75 100644
Binary files a/test/controls/goldens/yaru_check_button-checked-hovered-dark.png and b/test/controls/goldens/yaru_check_button-checked-hovered-dark.png differ
diff --git a/test/controls/goldens/yaru_check_button-checked-hovered-light.png b/test/controls/goldens/yaru_check_button-checked-hovered-light.png
index adba4cc34..3a27960cd 100644
Binary files a/test/controls/goldens/yaru_check_button-checked-hovered-light.png and b/test/controls/goldens/yaru_check_button-checked-hovered-light.png differ
diff --git a/test/controls/goldens/yaru_check_button-checked-light.png b/test/controls/goldens/yaru_check_button-checked-light.png
index a804d7560..d3a3ff88c 100644
Binary files a/test/controls/goldens/yaru_check_button-checked-light.png and b/test/controls/goldens/yaru_check_button-checked-light.png differ
diff --git a/test/controls/goldens/yaru_check_button-checked-pressed-dark.png b/test/controls/goldens/yaru_check_button-checked-pressed-dark.png
index a99d39e75..3d8387d39 100644
Binary files a/test/controls/goldens/yaru_check_button-checked-pressed-dark.png and b/test/controls/goldens/yaru_check_button-checked-pressed-dark.png differ
diff --git a/test/controls/goldens/yaru_check_button-checked-pressed-light.png b/test/controls/goldens/yaru_check_button-checked-pressed-light.png
index 3a27960cd..f7a0d9d66 100644
Binary files a/test/controls/goldens/yaru_check_button-checked-pressed-light.png and b/test/controls/goldens/yaru_check_button-checked-pressed-light.png differ
diff --git a/test/controls/goldens/yaru_check_button-tristate-dark.png b/test/controls/goldens/yaru_check_button-tristate-dark.png
index 1dc3b7694..36793642f 100644
Binary files a/test/controls/goldens/yaru_check_button-tristate-dark.png and b/test/controls/goldens/yaru_check_button-tristate-dark.png differ
diff --git a/test/controls/goldens/yaru_check_button-tristate-disabled-dark.png b/test/controls/goldens/yaru_check_button-tristate-disabled-dark.png
index 353f7038f..ac919fe76 100644
Binary files a/test/controls/goldens/yaru_check_button-tristate-disabled-dark.png and b/test/controls/goldens/yaru_check_button-tristate-disabled-dark.png differ
diff --git a/test/controls/goldens/yaru_check_button-tristate-disabled-light.png b/test/controls/goldens/yaru_check_button-tristate-disabled-light.png
index 46a82dc83..6b9d6a11d 100644
Binary files a/test/controls/goldens/yaru_check_button-tristate-disabled-light.png and b/test/controls/goldens/yaru_check_button-tristate-disabled-light.png differ
diff --git a/test/controls/goldens/yaru_check_button-tristate-focused-dark.png b/test/controls/goldens/yaru_check_button-tristate-focused-dark.png
index fd07e86b6..5391ee3cf 100644
Binary files a/test/controls/goldens/yaru_check_button-tristate-focused-dark.png and b/test/controls/goldens/yaru_check_button-tristate-focused-dark.png differ
diff --git a/test/controls/goldens/yaru_check_button-tristate-focused-light.png b/test/controls/goldens/yaru_check_button-tristate-focused-light.png
index ac3f73702..d435a667a 100644
Binary files a/test/controls/goldens/yaru_check_button-tristate-focused-light.png and b/test/controls/goldens/yaru_check_button-tristate-focused-light.png differ
diff --git a/test/controls/goldens/yaru_check_button-tristate-hovered-dark.png b/test/controls/goldens/yaru_check_button-tristate-hovered-dark.png
index f3bcaa56c..8e4ee83a1 100644
Binary files a/test/controls/goldens/yaru_check_button-tristate-hovered-dark.png and b/test/controls/goldens/yaru_check_button-tristate-hovered-dark.png differ
diff --git a/test/controls/goldens/yaru_check_button-tristate-hovered-light.png b/test/controls/goldens/yaru_check_button-tristate-hovered-light.png
index 05c83f93d..43e479516 100644
Binary files a/test/controls/goldens/yaru_check_button-tristate-hovered-light.png and b/test/controls/goldens/yaru_check_button-tristate-hovered-light.png differ
diff --git a/test/controls/goldens/yaru_check_button-tristate-light.png b/test/controls/goldens/yaru_check_button-tristate-light.png
index 82570827d..056190b40 100644
Binary files a/test/controls/goldens/yaru_check_button-tristate-light.png and b/test/controls/goldens/yaru_check_button-tristate-light.png differ
diff --git a/test/controls/goldens/yaru_check_button-tristate-pressed-dark.png b/test/controls/goldens/yaru_check_button-tristate-pressed-dark.png
index 8e4ee83a1..882d31e82 100644
Binary files a/test/controls/goldens/yaru_check_button-tristate-pressed-dark.png and b/test/controls/goldens/yaru_check_button-tristate-pressed-dark.png differ
diff --git a/test/controls/goldens/yaru_check_button-tristate-pressed-light.png b/test/controls/goldens/yaru_check_button-tristate-pressed-light.png
index 43e479516..f82c26027 100644
Binary files a/test/controls/goldens/yaru_check_button-tristate-pressed-light.png and b/test/controls/goldens/yaru_check_button-tristate-pressed-light.png differ
diff --git a/test/controls/goldens/yaru_check_button-unchecked-dark.png b/test/controls/goldens/yaru_check_button-unchecked-dark.png
index fee2b6dfc..fc1a82d71 100644
Binary files a/test/controls/goldens/yaru_check_button-unchecked-dark.png and b/test/controls/goldens/yaru_check_button-unchecked-dark.png differ
diff --git a/test/controls/goldens/yaru_check_button-unchecked-light.png b/test/controls/goldens/yaru_check_button-unchecked-light.png
index b5466a596..4f4cd3942 100644
Binary files a/test/controls/goldens/yaru_check_button-unchecked-light.png and b/test/controls/goldens/yaru_check_button-unchecked-light.png differ
diff --git a/test/controls/goldens/yaru_check_button-unckecked-disabled-dark.png b/test/controls/goldens/yaru_check_button-unckecked-disabled-dark.png
index e87920148..3fe281eb7 100644
Binary files a/test/controls/goldens/yaru_check_button-unckecked-disabled-dark.png and b/test/controls/goldens/yaru_check_button-unckecked-disabled-dark.png differ
diff --git a/test/controls/goldens/yaru_check_button-unckecked-disabled-light.png b/test/controls/goldens/yaru_check_button-unckecked-disabled-light.png
index 73127c565..55b7c7516 100644
Binary files a/test/controls/goldens/yaru_check_button-unckecked-disabled-light.png and b/test/controls/goldens/yaru_check_button-unckecked-disabled-light.png differ
diff --git a/test/controls/goldens/yaru_check_button-unckecked-focused-dark.png b/test/controls/goldens/yaru_check_button-unckecked-focused-dark.png
index 31bfdc269..ba3f1602f 100644
Binary files a/test/controls/goldens/yaru_check_button-unckecked-focused-dark.png and b/test/controls/goldens/yaru_check_button-unckecked-focused-dark.png differ
diff --git a/test/controls/goldens/yaru_check_button-unckecked-focused-light.png b/test/controls/goldens/yaru_check_button-unckecked-focused-light.png
index d3e29c696..7fa8157b7 100644
Binary files a/test/controls/goldens/yaru_check_button-unckecked-focused-light.png and b/test/controls/goldens/yaru_check_button-unckecked-focused-light.png differ
diff --git a/test/controls/goldens/yaru_check_button-unckecked-hovered-dark.png b/test/controls/goldens/yaru_check_button-unckecked-hovered-dark.png
index 6245a4de7..e10775dc0 100644
Binary files a/test/controls/goldens/yaru_check_button-unckecked-hovered-dark.png and b/test/controls/goldens/yaru_check_button-unckecked-hovered-dark.png differ
diff --git a/test/controls/goldens/yaru_check_button-unckecked-hovered-light.png b/test/controls/goldens/yaru_check_button-unckecked-hovered-light.png
index ce0c6f57e..5f0e018ca 100644
Binary files a/test/controls/goldens/yaru_check_button-unckecked-hovered-light.png and b/test/controls/goldens/yaru_check_button-unckecked-hovered-light.png differ
diff --git a/test/controls/goldens/yaru_check_button-unckecked-pressed-dark.png b/test/controls/goldens/yaru_check_button-unckecked-pressed-dark.png
index e10775dc0..7d1937077 100644
Binary files a/test/controls/goldens/yaru_check_button-unckecked-pressed-dark.png and b/test/controls/goldens/yaru_check_button-unckecked-pressed-dark.png differ
diff --git a/test/controls/goldens/yaru_check_button-unckecked-pressed-light.png b/test/controls/goldens/yaru_check_button-unckecked-pressed-light.png
index 5f0e018ca..6d2e72289 100644
Binary files a/test/controls/goldens/yaru_check_button-unckecked-pressed-light.png and b/test/controls/goldens/yaru_check_button-unckecked-pressed-light.png differ
diff --git a/test/controls/goldens/yaru_checkbox-checked-dark.png b/test/controls/goldens/yaru_checkbox-checked-dark.png
index 09394aacd..c452aed94 100644
Binary files a/test/controls/goldens/yaru_checkbox-checked-dark.png and b/test/controls/goldens/yaru_checkbox-checked-dark.png differ
diff --git a/test/controls/goldens/yaru_checkbox-checked-disabled-dark.png b/test/controls/goldens/yaru_checkbox-checked-disabled-dark.png
index 7a2fdef10..ab36679c9 100644
Binary files a/test/controls/goldens/yaru_checkbox-checked-disabled-dark.png and b/test/controls/goldens/yaru_checkbox-checked-disabled-dark.png differ
diff --git a/test/controls/goldens/yaru_checkbox-checked-disabled-light.png b/test/controls/goldens/yaru_checkbox-checked-disabled-light.png
index e058bbf0e..184c74742 100644
Binary files a/test/controls/goldens/yaru_checkbox-checked-disabled-light.png and b/test/controls/goldens/yaru_checkbox-checked-disabled-light.png differ
diff --git a/test/controls/goldens/yaru_checkbox-checked-focused-dark.png b/test/controls/goldens/yaru_checkbox-checked-focused-dark.png
index 9b98f390f..e9b65fc46 100644
Binary files a/test/controls/goldens/yaru_checkbox-checked-focused-dark.png and b/test/controls/goldens/yaru_checkbox-checked-focused-dark.png differ
diff --git a/test/controls/goldens/yaru_checkbox-checked-focused-light.png b/test/controls/goldens/yaru_checkbox-checked-focused-light.png
index 587dab35e..679ee4db7 100644
Binary files a/test/controls/goldens/yaru_checkbox-checked-focused-light.png and b/test/controls/goldens/yaru_checkbox-checked-focused-light.png differ
diff --git a/test/controls/goldens/yaru_checkbox-checked-hovered-dark.png b/test/controls/goldens/yaru_checkbox-checked-hovered-dark.png
index 7925d29c6..862d813bd 100644
Binary files a/test/controls/goldens/yaru_checkbox-checked-hovered-dark.png and b/test/controls/goldens/yaru_checkbox-checked-hovered-dark.png differ
diff --git a/test/controls/goldens/yaru_checkbox-checked-hovered-light.png b/test/controls/goldens/yaru_checkbox-checked-hovered-light.png
index 0bb2921b3..e21fe7f72 100644
Binary files a/test/controls/goldens/yaru_checkbox-checked-hovered-light.png and b/test/controls/goldens/yaru_checkbox-checked-hovered-light.png differ
diff --git a/test/controls/goldens/yaru_checkbox-checked-light.png b/test/controls/goldens/yaru_checkbox-checked-light.png
index fa2aeb211..51d0a683b 100644
Binary files a/test/controls/goldens/yaru_checkbox-checked-light.png and b/test/controls/goldens/yaru_checkbox-checked-light.png differ
diff --git a/test/controls/goldens/yaru_checkbox-checked-pressed-dark.png b/test/controls/goldens/yaru_checkbox-checked-pressed-dark.png
index 862d813bd..1c19ab9ae 100644
Binary files a/test/controls/goldens/yaru_checkbox-checked-pressed-dark.png and b/test/controls/goldens/yaru_checkbox-checked-pressed-dark.png differ
diff --git a/test/controls/goldens/yaru_checkbox-checked-pressed-light.png b/test/controls/goldens/yaru_checkbox-checked-pressed-light.png
index e21fe7f72..2ae3e658e 100644
Binary files a/test/controls/goldens/yaru_checkbox-checked-pressed-light.png and b/test/controls/goldens/yaru_checkbox-checked-pressed-light.png differ
diff --git a/test/controls/goldens/yaru_checkbox-tristate-dark.png b/test/controls/goldens/yaru_checkbox-tristate-dark.png
index 9cce4c3c7..218c6374b 100644
Binary files a/test/controls/goldens/yaru_checkbox-tristate-dark.png and b/test/controls/goldens/yaru_checkbox-tristate-dark.png differ
diff --git a/test/controls/goldens/yaru_checkbox-tristate-disabled-dark.png b/test/controls/goldens/yaru_checkbox-tristate-disabled-dark.png
index a2f86f04e..141cc02f2 100644
Binary files a/test/controls/goldens/yaru_checkbox-tristate-disabled-dark.png and b/test/controls/goldens/yaru_checkbox-tristate-disabled-dark.png differ
diff --git a/test/controls/goldens/yaru_checkbox-tristate-disabled-light.png b/test/controls/goldens/yaru_checkbox-tristate-disabled-light.png
index 5dbd2c7ba..462d48687 100644
Binary files a/test/controls/goldens/yaru_checkbox-tristate-disabled-light.png and b/test/controls/goldens/yaru_checkbox-tristate-disabled-light.png differ
diff --git a/test/controls/goldens/yaru_checkbox-tristate-focused-dark.png b/test/controls/goldens/yaru_checkbox-tristate-focused-dark.png
index a1ac79ac6..94e5d3f66 100644
Binary files a/test/controls/goldens/yaru_checkbox-tristate-focused-dark.png and b/test/controls/goldens/yaru_checkbox-tristate-focused-dark.png differ
diff --git a/test/controls/goldens/yaru_checkbox-tristate-focused-light.png b/test/controls/goldens/yaru_checkbox-tristate-focused-light.png
index 14a0c0369..b95ab45c2 100644
Binary files a/test/controls/goldens/yaru_checkbox-tristate-focused-light.png and b/test/controls/goldens/yaru_checkbox-tristate-focused-light.png differ
diff --git a/test/controls/goldens/yaru_checkbox-tristate-hovered-dark.png b/test/controls/goldens/yaru_checkbox-tristate-hovered-dark.png
index 55f3542b7..551359e9f 100644
Binary files a/test/controls/goldens/yaru_checkbox-tristate-hovered-dark.png and b/test/controls/goldens/yaru_checkbox-tristate-hovered-dark.png differ
diff --git a/test/controls/goldens/yaru_checkbox-tristate-hovered-light.png b/test/controls/goldens/yaru_checkbox-tristate-hovered-light.png
index ee7038abf..f2fa58294 100644
Binary files a/test/controls/goldens/yaru_checkbox-tristate-hovered-light.png and b/test/controls/goldens/yaru_checkbox-tristate-hovered-light.png differ
diff --git a/test/controls/goldens/yaru_checkbox-tristate-light.png b/test/controls/goldens/yaru_checkbox-tristate-light.png
index 2570517f1..a9d3b53f9 100644
Binary files a/test/controls/goldens/yaru_checkbox-tristate-light.png and b/test/controls/goldens/yaru_checkbox-tristate-light.png differ
diff --git a/test/controls/goldens/yaru_checkbox-tristate-pressed-dark.png b/test/controls/goldens/yaru_checkbox-tristate-pressed-dark.png
index 551359e9f..cb7368794 100644
Binary files a/test/controls/goldens/yaru_checkbox-tristate-pressed-dark.png and b/test/controls/goldens/yaru_checkbox-tristate-pressed-dark.png differ
diff --git a/test/controls/goldens/yaru_checkbox-tristate-pressed-light.png b/test/controls/goldens/yaru_checkbox-tristate-pressed-light.png
index f2fa58294..6a2dee7ca 100644
Binary files a/test/controls/goldens/yaru_checkbox-tristate-pressed-light.png and b/test/controls/goldens/yaru_checkbox-tristate-pressed-light.png differ
diff --git a/test/controls/goldens/yaru_checkbox-unchecked-dark.png b/test/controls/goldens/yaru_checkbox-unchecked-dark.png
index d235d0605..6235f05d3 100644
Binary files a/test/controls/goldens/yaru_checkbox-unchecked-dark.png and b/test/controls/goldens/yaru_checkbox-unchecked-dark.png differ
diff --git a/test/controls/goldens/yaru_checkbox-unchecked-light.png b/test/controls/goldens/yaru_checkbox-unchecked-light.png
index 430b93b35..4f554df47 100644
Binary files a/test/controls/goldens/yaru_checkbox-unchecked-light.png and b/test/controls/goldens/yaru_checkbox-unchecked-light.png differ
diff --git a/test/controls/goldens/yaru_checkbox-unckecked-disabled-dark.png b/test/controls/goldens/yaru_checkbox-unckecked-disabled-dark.png
index b99b4ecc7..3bbf1d178 100644
Binary files a/test/controls/goldens/yaru_checkbox-unckecked-disabled-dark.png and b/test/controls/goldens/yaru_checkbox-unckecked-disabled-dark.png differ
diff --git a/test/controls/goldens/yaru_checkbox-unckecked-disabled-light.png b/test/controls/goldens/yaru_checkbox-unckecked-disabled-light.png
index d57be38ca..152b9362b 100644
Binary files a/test/controls/goldens/yaru_checkbox-unckecked-disabled-light.png and b/test/controls/goldens/yaru_checkbox-unckecked-disabled-light.png differ
diff --git a/test/controls/goldens/yaru_checkbox-unckecked-focused-dark.png b/test/controls/goldens/yaru_checkbox-unckecked-focused-dark.png
index d087fcbb7..3da7943b1 100644
Binary files a/test/controls/goldens/yaru_checkbox-unckecked-focused-dark.png and b/test/controls/goldens/yaru_checkbox-unckecked-focused-dark.png differ
diff --git a/test/controls/goldens/yaru_checkbox-unckecked-focused-light.png b/test/controls/goldens/yaru_checkbox-unckecked-focused-light.png
index 8f0e1248e..1a513a790 100644
Binary files a/test/controls/goldens/yaru_checkbox-unckecked-focused-light.png and b/test/controls/goldens/yaru_checkbox-unckecked-focused-light.png differ
diff --git a/test/controls/goldens/yaru_checkbox-unckecked-hovered-dark.png b/test/controls/goldens/yaru_checkbox-unckecked-hovered-dark.png
index 2e2ba2237..59151511d 100644
Binary files a/test/controls/goldens/yaru_checkbox-unckecked-hovered-dark.png and b/test/controls/goldens/yaru_checkbox-unckecked-hovered-dark.png differ
diff --git a/test/controls/goldens/yaru_checkbox-unckecked-hovered-light.png b/test/controls/goldens/yaru_checkbox-unckecked-hovered-light.png
index 9f7cf9022..d62bc8b65 100644
Binary files a/test/controls/goldens/yaru_checkbox-unckecked-hovered-light.png and b/test/controls/goldens/yaru_checkbox-unckecked-hovered-light.png differ
diff --git a/test/controls/goldens/yaru_checkbox-unckecked-pressed-dark.png b/test/controls/goldens/yaru_checkbox-unckecked-pressed-dark.png
index 59151511d..8d5c7ca74 100644
Binary files a/test/controls/goldens/yaru_checkbox-unckecked-pressed-dark.png and b/test/controls/goldens/yaru_checkbox-unckecked-pressed-dark.png differ
diff --git a/test/controls/goldens/yaru_checkbox-unckecked-pressed-light.png b/test/controls/goldens/yaru_checkbox-unckecked-pressed-light.png
index d62bc8b65..63ba98487 100644
Binary files a/test/controls/goldens/yaru_checkbox-unckecked-pressed-light.png and b/test/controls/goldens/yaru_checkbox-unckecked-pressed-light.png differ
diff --git a/test/controls/goldens/yaru_popup_menu_item-checked-dark.png b/test/controls/goldens/yaru_popup_menu_item-checked-dark.png
index 01af72da2..912466449 100644
Binary files a/test/controls/goldens/yaru_popup_menu_item-checked-dark.png and b/test/controls/goldens/yaru_popup_menu_item-checked-dark.png differ
diff --git a/test/controls/goldens/yaru_popup_menu_item-checked-disabled-dark.png b/test/controls/goldens/yaru_popup_menu_item-checked-disabled-dark.png
index 5d2002eb3..91e088e96 100644
Binary files a/test/controls/goldens/yaru_popup_menu_item-checked-disabled-dark.png and b/test/controls/goldens/yaru_popup_menu_item-checked-disabled-dark.png differ
diff --git a/test/controls/goldens/yaru_popup_menu_item-checked-disabled-light.png b/test/controls/goldens/yaru_popup_menu_item-checked-disabled-light.png
index fd7ed0ab8..13d99b607 100644
Binary files a/test/controls/goldens/yaru_popup_menu_item-checked-disabled-light.png and b/test/controls/goldens/yaru_popup_menu_item-checked-disabled-light.png differ
diff --git a/test/controls/goldens/yaru_popup_menu_item-checked-hovered-dark.png b/test/controls/goldens/yaru_popup_menu_item-checked-hovered-dark.png
index f91098fd8..23e0e5b0a 100644
Binary files a/test/controls/goldens/yaru_popup_menu_item-checked-hovered-dark.png and b/test/controls/goldens/yaru_popup_menu_item-checked-hovered-dark.png differ
diff --git a/test/controls/goldens/yaru_popup_menu_item-checked-hovered-light.png b/test/controls/goldens/yaru_popup_menu_item-checked-hovered-light.png
index 32c7aa6a7..3367bdcfe 100644
Binary files a/test/controls/goldens/yaru_popup_menu_item-checked-hovered-light.png and b/test/controls/goldens/yaru_popup_menu_item-checked-hovered-light.png differ
diff --git a/test/controls/goldens/yaru_popup_menu_item-checked-light.png b/test/controls/goldens/yaru_popup_menu_item-checked-light.png
index 8850eb9d7..9d76c437c 100644
Binary files a/test/controls/goldens/yaru_popup_menu_item-checked-light.png and b/test/controls/goldens/yaru_popup_menu_item-checked-light.png differ
diff --git a/test/controls/goldens/yaru_popup_menu_item-checked-pressed-dark.png b/test/controls/goldens/yaru_popup_menu_item-checked-pressed-dark.png
index c26cbf852..569039bb9 100644
Binary files a/test/controls/goldens/yaru_popup_menu_item-checked-pressed-dark.png and b/test/controls/goldens/yaru_popup_menu_item-checked-pressed-dark.png differ
diff --git a/test/controls/goldens/yaru_popup_menu_item-checked-pressed-light.png b/test/controls/goldens/yaru_popup_menu_item-checked-pressed-light.png
index 9cb678bc6..0d227939a 100644
Binary files a/test/controls/goldens/yaru_popup_menu_item-checked-pressed-light.png and b/test/controls/goldens/yaru_popup_menu_item-checked-pressed-light.png differ
diff --git a/test/controls/goldens/yaru_popup_menu_item-unchecked-dark.png b/test/controls/goldens/yaru_popup_menu_item-unchecked-dark.png
index 1d2c8fd68..d7a4baafb 100644
Binary files a/test/controls/goldens/yaru_popup_menu_item-unchecked-dark.png and b/test/controls/goldens/yaru_popup_menu_item-unchecked-dark.png differ
diff --git a/test/controls/goldens/yaru_popup_menu_item-unchecked-disabled-dark.png b/test/controls/goldens/yaru_popup_menu_item-unchecked-disabled-dark.png
index ea18d6762..605753355 100644
Binary files a/test/controls/goldens/yaru_popup_menu_item-unchecked-disabled-dark.png and b/test/controls/goldens/yaru_popup_menu_item-unchecked-disabled-dark.png differ
diff --git a/test/controls/goldens/yaru_popup_menu_item-unchecked-disabled-light.png b/test/controls/goldens/yaru_popup_menu_item-unchecked-disabled-light.png
index af91a7104..fec6d2225 100644
Binary files a/test/controls/goldens/yaru_popup_menu_item-unchecked-disabled-light.png and b/test/controls/goldens/yaru_popup_menu_item-unchecked-disabled-light.png differ
diff --git a/test/controls/goldens/yaru_popup_menu_item-unchecked-hovered-dark.png b/test/controls/goldens/yaru_popup_menu_item-unchecked-hovered-dark.png
index a1addba45..700781ceb 100644
Binary files a/test/controls/goldens/yaru_popup_menu_item-unchecked-hovered-dark.png and b/test/controls/goldens/yaru_popup_menu_item-unchecked-hovered-dark.png differ
diff --git a/test/controls/goldens/yaru_popup_menu_item-unchecked-hovered-light.png b/test/controls/goldens/yaru_popup_menu_item-unchecked-hovered-light.png
index 60e69544c..4820c5d06 100644
Binary files a/test/controls/goldens/yaru_popup_menu_item-unchecked-hovered-light.png and b/test/controls/goldens/yaru_popup_menu_item-unchecked-hovered-light.png differ
diff --git a/test/controls/goldens/yaru_popup_menu_item-unchecked-light.png b/test/controls/goldens/yaru_popup_menu_item-unchecked-light.png
index ceee0e4e7..097600fcf 100644
Binary files a/test/controls/goldens/yaru_popup_menu_item-unchecked-light.png and b/test/controls/goldens/yaru_popup_menu_item-unchecked-light.png differ
diff --git a/test/controls/goldens/yaru_popup_menu_item-unchecked-pressed-dark.png b/test/controls/goldens/yaru_popup_menu_item-unchecked-pressed-dark.png
index 681fe52a6..bdfd91bb0 100644
Binary files a/test/controls/goldens/yaru_popup_menu_item-unchecked-pressed-dark.png and b/test/controls/goldens/yaru_popup_menu_item-unchecked-pressed-dark.png differ
diff --git a/test/controls/goldens/yaru_popup_menu_item-unchecked-pressed-light.png b/test/controls/goldens/yaru_popup_menu_item-unchecked-pressed-light.png
index ba4a7a2fe..939accc77 100644
Binary files a/test/controls/goldens/yaru_popup_menu_item-unchecked-pressed-light.png and b/test/controls/goldens/yaru_popup_menu_item-unchecked-pressed-light.png differ
diff --git a/test/controls/goldens/yaru_radio-checked-dark.png b/test/controls/goldens/yaru_radio-checked-dark.png
new file mode 100644
index 000000000..496deeb09
Binary files /dev/null and b/test/controls/goldens/yaru_radio-checked-dark.png differ
diff --git a/test/controls/goldens/yaru_radio-checked-disabled-dark.png b/test/controls/goldens/yaru_radio-checked-disabled-dark.png
new file mode 100644
index 000000000..3cdbb1830
Binary files /dev/null and b/test/controls/goldens/yaru_radio-checked-disabled-dark.png differ
diff --git a/test/controls/goldens/yaru_radio-checked-disabled-light.png b/test/controls/goldens/yaru_radio-checked-disabled-light.png
new file mode 100644
index 000000000..7bba11fe5
Binary files /dev/null and b/test/controls/goldens/yaru_radio-checked-disabled-light.png differ
diff --git a/test/controls/goldens/yaru_radio-checked-focused-dark.png b/test/controls/goldens/yaru_radio-checked-focused-dark.png
new file mode 100644
index 000000000..fa6474017
Binary files /dev/null and b/test/controls/goldens/yaru_radio-checked-focused-dark.png differ
diff --git a/test/controls/goldens/yaru_radio-checked-focused-light.png b/test/controls/goldens/yaru_radio-checked-focused-light.png
new file mode 100644
index 000000000..8c6910f92
Binary files /dev/null and b/test/controls/goldens/yaru_radio-checked-focused-light.png differ
diff --git a/test/controls/goldens/yaru_radio-checked-hovered-dark.png b/test/controls/goldens/yaru_radio-checked-hovered-dark.png
new file mode 100644
index 000000000..f88551e59
Binary files /dev/null and b/test/controls/goldens/yaru_radio-checked-hovered-dark.png differ
diff --git a/test/controls/goldens/yaru_radio-checked-hovered-light.png b/test/controls/goldens/yaru_radio-checked-hovered-light.png
new file mode 100644
index 000000000..b2ca03a2e
Binary files /dev/null and b/test/controls/goldens/yaru_radio-checked-hovered-light.png differ
diff --git a/test/controls/goldens/yaru_radio-checked-light.png b/test/controls/goldens/yaru_radio-checked-light.png
new file mode 100644
index 000000000..69674f3ca
Binary files /dev/null and b/test/controls/goldens/yaru_radio-checked-light.png differ
diff --git a/test/controls/goldens/yaru_radio-checked-pressed-dark.png b/test/controls/goldens/yaru_radio-checked-pressed-dark.png
new file mode 100644
index 000000000..c4fa449b3
Binary files /dev/null and b/test/controls/goldens/yaru_radio-checked-pressed-dark.png differ
diff --git a/test/controls/goldens/yaru_radio-checked-pressed-light.png b/test/controls/goldens/yaru_radio-checked-pressed-light.png
new file mode 100644
index 000000000..c34651ec4
Binary files /dev/null and b/test/controls/goldens/yaru_radio-checked-pressed-light.png differ
diff --git a/test/controls/goldens/yaru_radio-unchecked-dark.png b/test/controls/goldens/yaru_radio-unchecked-dark.png
new file mode 100644
index 000000000..9cb646382
Binary files /dev/null and b/test/controls/goldens/yaru_radio-unchecked-dark.png differ
diff --git a/test/controls/goldens/yaru_radio-unchecked-light.png b/test/controls/goldens/yaru_radio-unchecked-light.png
new file mode 100644
index 000000000..8c8b146ea
Binary files /dev/null and b/test/controls/goldens/yaru_radio-unchecked-light.png differ
diff --git a/test/controls/goldens/yaru_radio-unckecked-disabled-dark.png b/test/controls/goldens/yaru_radio-unckecked-disabled-dark.png
new file mode 100644
index 000000000..9994ff23c
Binary files /dev/null and b/test/controls/goldens/yaru_radio-unckecked-disabled-dark.png differ
diff --git a/test/controls/goldens/yaru_radio-unckecked-disabled-light.png b/test/controls/goldens/yaru_radio-unckecked-disabled-light.png
new file mode 100644
index 000000000..04d666ceb
Binary files /dev/null and b/test/controls/goldens/yaru_radio-unckecked-disabled-light.png differ
diff --git a/test/controls/goldens/yaru_radio-unckecked-focused-dark.png b/test/controls/goldens/yaru_radio-unckecked-focused-dark.png
new file mode 100644
index 000000000..d7cad8aac
Binary files /dev/null and b/test/controls/goldens/yaru_radio-unckecked-focused-dark.png differ
diff --git a/test/controls/goldens/yaru_radio-unckecked-focused-light.png b/test/controls/goldens/yaru_radio-unckecked-focused-light.png
new file mode 100644
index 000000000..992483afe
Binary files /dev/null and b/test/controls/goldens/yaru_radio-unckecked-focused-light.png differ
diff --git a/test/controls/goldens/yaru_radio-unckecked-hovered-dark.png b/test/controls/goldens/yaru_radio-unckecked-hovered-dark.png
new file mode 100644
index 000000000..421712153
Binary files /dev/null and b/test/controls/goldens/yaru_radio-unckecked-hovered-dark.png differ
diff --git a/test/controls/goldens/yaru_radio-unckecked-hovered-light.png b/test/controls/goldens/yaru_radio-unckecked-hovered-light.png
new file mode 100644
index 000000000..bae85627a
Binary files /dev/null and b/test/controls/goldens/yaru_radio-unckecked-hovered-light.png differ
diff --git a/test/controls/goldens/yaru_radio-unckecked-pressed-dark.png b/test/controls/goldens/yaru_radio-unckecked-pressed-dark.png
new file mode 100644
index 000000000..9b41e3d2e
Binary files /dev/null and b/test/controls/goldens/yaru_radio-unckecked-pressed-dark.png differ
diff --git a/test/controls/goldens/yaru_radio-unckecked-pressed-light.png b/test/controls/goldens/yaru_radio-unckecked-pressed-light.png
new file mode 100644
index 000000000..dbb2d5963
Binary files /dev/null and b/test/controls/goldens/yaru_radio-unckecked-pressed-light.png differ
diff --git a/test/controls/goldens/yaru_radio_button-checked-dark.png b/test/controls/goldens/yaru_radio_button-checked-dark.png
index 1dcf97d2b..a2aeb4b09 100644
Binary files a/test/controls/goldens/yaru_radio_button-checked-dark.png and b/test/controls/goldens/yaru_radio_button-checked-dark.png differ
diff --git a/test/controls/goldens/yaru_radio_button-checked-disabled-dark.png b/test/controls/goldens/yaru_radio_button-checked-disabled-dark.png
index caf29b532..326f3365e 100644
Binary files a/test/controls/goldens/yaru_radio_button-checked-disabled-dark.png and b/test/controls/goldens/yaru_radio_button-checked-disabled-dark.png differ
diff --git a/test/controls/goldens/yaru_radio_button-checked-disabled-light.png b/test/controls/goldens/yaru_radio_button-checked-disabled-light.png
index a823e91fb..da9609686 100644
Binary files a/test/controls/goldens/yaru_radio_button-checked-disabled-light.png and b/test/controls/goldens/yaru_radio_button-checked-disabled-light.png differ
diff --git a/test/controls/goldens/yaru_radio_button-checked-focused-dark.png b/test/controls/goldens/yaru_radio_button-checked-focused-dark.png
index 5343a56c2..6b8b92ba4 100644
Binary files a/test/controls/goldens/yaru_radio_button-checked-focused-dark.png and b/test/controls/goldens/yaru_radio_button-checked-focused-dark.png differ
diff --git a/test/controls/goldens/yaru_radio_button-checked-focused-light.png b/test/controls/goldens/yaru_radio_button-checked-focused-light.png
index 2aa15c17b..4982ad3bf 100644
Binary files a/test/controls/goldens/yaru_radio_button-checked-focused-light.png and b/test/controls/goldens/yaru_radio_button-checked-focused-light.png differ
diff --git a/test/controls/goldens/yaru_radio_button-checked-hovered-dark.png b/test/controls/goldens/yaru_radio_button-checked-hovered-dark.png
index 4953a41b2..12ea5211c 100644
Binary files a/test/controls/goldens/yaru_radio_button-checked-hovered-dark.png and b/test/controls/goldens/yaru_radio_button-checked-hovered-dark.png differ
diff --git a/test/controls/goldens/yaru_radio_button-checked-hovered-light.png b/test/controls/goldens/yaru_radio_button-checked-hovered-light.png
index 9e2a7fe1d..db67db786 100644
Binary files a/test/controls/goldens/yaru_radio_button-checked-hovered-light.png and b/test/controls/goldens/yaru_radio_button-checked-hovered-light.png differ
diff --git a/test/controls/goldens/yaru_radio_button-checked-light.png b/test/controls/goldens/yaru_radio_button-checked-light.png
index 98a966e8e..63ea44865 100644
Binary files a/test/controls/goldens/yaru_radio_button-checked-light.png and b/test/controls/goldens/yaru_radio_button-checked-light.png differ
diff --git a/test/controls/goldens/yaru_radio_button-checked-pressed-dark.png b/test/controls/goldens/yaru_radio_button-checked-pressed-dark.png
index 4953a41b2..2a4b3e0ff 100644
Binary files a/test/controls/goldens/yaru_radio_button-checked-pressed-dark.png and b/test/controls/goldens/yaru_radio_button-checked-pressed-dark.png differ
diff --git a/test/controls/goldens/yaru_radio_button-checked-pressed-light.png b/test/controls/goldens/yaru_radio_button-checked-pressed-light.png
index 9e2a7fe1d..6b5e0427e 100644
Binary files a/test/controls/goldens/yaru_radio_button-checked-pressed-light.png and b/test/controls/goldens/yaru_radio_button-checked-pressed-light.png differ
diff --git a/test/controls/goldens/yaru_radio_button-unchecked-dark.png b/test/controls/goldens/yaru_radio_button-unchecked-dark.png
index 2c26ebd65..d4506629d 100644
Binary files a/test/controls/goldens/yaru_radio_button-unchecked-dark.png and b/test/controls/goldens/yaru_radio_button-unchecked-dark.png differ
diff --git a/test/controls/goldens/yaru_radio_button-unchecked-light.png b/test/controls/goldens/yaru_radio_button-unchecked-light.png
index c388ba638..a539a9d08 100644
Binary files a/test/controls/goldens/yaru_radio_button-unchecked-light.png and b/test/controls/goldens/yaru_radio_button-unchecked-light.png differ
diff --git a/test/controls/goldens/yaru_radio_button-unckecked-disabled-dark.png b/test/controls/goldens/yaru_radio_button-unckecked-disabled-dark.png
index 7cc146bfe..be9597727 100644
Binary files a/test/controls/goldens/yaru_radio_button-unckecked-disabled-dark.png and b/test/controls/goldens/yaru_radio_button-unckecked-disabled-dark.png differ
diff --git a/test/controls/goldens/yaru_radio_button-unckecked-disabled-light.png b/test/controls/goldens/yaru_radio_button-unckecked-disabled-light.png
index 3059a9e29..0e29e2bca 100644
Binary files a/test/controls/goldens/yaru_radio_button-unckecked-disabled-light.png and b/test/controls/goldens/yaru_radio_button-unckecked-disabled-light.png differ
diff --git a/test/controls/goldens/yaru_radio_button-unckecked-focused-dark.png b/test/controls/goldens/yaru_radio_button-unckecked-focused-dark.png
index 20d9c21d4..60a67ae36 100644
Binary files a/test/controls/goldens/yaru_radio_button-unckecked-focused-dark.png and b/test/controls/goldens/yaru_radio_button-unckecked-focused-dark.png differ
diff --git a/test/controls/goldens/yaru_radio_button-unckecked-focused-light.png b/test/controls/goldens/yaru_radio_button-unckecked-focused-light.png
index ba73eac14..16736a513 100644
Binary files a/test/controls/goldens/yaru_radio_button-unckecked-focused-light.png and b/test/controls/goldens/yaru_radio_button-unckecked-focused-light.png differ
diff --git a/test/controls/goldens/yaru_radio_button-unckecked-hovered-dark.png b/test/controls/goldens/yaru_radio_button-unckecked-hovered-dark.png
index 6cf6ee6b1..d03f92c25 100644
Binary files a/test/controls/goldens/yaru_radio_button-unckecked-hovered-dark.png and b/test/controls/goldens/yaru_radio_button-unckecked-hovered-dark.png differ
diff --git a/test/controls/goldens/yaru_radio_button-unckecked-hovered-light.png b/test/controls/goldens/yaru_radio_button-unckecked-hovered-light.png
index 3e7faaa0e..b6d3342e9 100644
Binary files a/test/controls/goldens/yaru_radio_button-unckecked-hovered-light.png and b/test/controls/goldens/yaru_radio_button-unckecked-hovered-light.png differ
diff --git a/test/controls/goldens/yaru_radio_button-unckecked-pressed-dark.png b/test/controls/goldens/yaru_radio_button-unckecked-pressed-dark.png
index 6cf6ee6b1..bd46cba8a 100644
Binary files a/test/controls/goldens/yaru_radio_button-unckecked-pressed-dark.png and b/test/controls/goldens/yaru_radio_button-unckecked-pressed-dark.png differ
diff --git a/test/controls/goldens/yaru_radio_button-unckecked-pressed-light.png b/test/controls/goldens/yaru_radio_button-unckecked-pressed-light.png
index 3e7faaa0e..00d5359c8 100644
Binary files a/test/controls/goldens/yaru_radio_button-unckecked-pressed-light.png and b/test/controls/goldens/yaru_radio_button-unckecked-pressed-light.png differ
diff --git a/test/controls/yaru_radio_button_test.dart b/test/controls/yaru_radio_button_test.dart
index 592249a48..b2b2ea812 100644
--- a/test/controls/yaru_radio_button_test.dart
+++ b/test/controls/yaru_radio_button_test.dart
@@ -26,14 +26,14 @@ void main() {
         .pumpWidget(builder(title: const Text('title'), subtitle: null));
     expect(find.text('title'), findsOneWidget);
     expect(find.text('subtitle'), findsNothing);
-    expect(find.byType(Radio<int>), findsOneWidget);
+    expect(find.byType(YaruRadio<int>), findsOneWidget);
 
     await tester.pumpWidget(
       builder(title: const Text('title'), subtitle: const Text('subtitle')),
     );
     expect(find.text('title'), findsOneWidget);
     expect(find.text('subtitle'), findsOneWidget);
-    expect(find.byType(Radio<int>), findsOneWidget);
+    expect(find.byType(YaruRadio<int>), findsOneWidget);
   });
 
   testWidgets('the labels react to taps', (tester) async {
@@ -168,10 +168,10 @@ void main() {
       await tester.pumpAndSettle();
 
       if (variant.hasState(MaterialState.pressed)) {
-        await tester.down(find.byType(Radio<bool>));
+        await tester.down(find.byType(YaruRadio<bool>));
         await tester.pumpAndSettle();
       } else if (variant.hasState(MaterialState.hovered)) {
-        await tester.hover(find.byType(Radio<bool>));
+        await tester.hover(find.byType(YaruRadio<bool>));
         await tester.pumpAndSettle();
       }
 
diff --git a/test/controls/yaru_radio_test.dart b/test/controls/yaru_radio_test.dart
new file mode 100644
index 000000000..6f5630eb0
--- /dev/null
+++ b/test/controls/yaru_radio_test.dart
@@ -0,0 +1,166 @@
+import 'package:flutter/gestures.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:yaru_widgets/yaru_widgets.dart';
+
+import '../yaru_golden_tester.dart';
+
+void main() {
+  testWidgets('radio react to taps', (tester) async {
+    int? changedValue;
+    Widget builder({
+      required int value,
+      required int groupValue,
+      required bool toggleable,
+    }) {
+      return MaterialApp(
+        home: Scaffold(
+          body: YaruRadio<int>(
+            value: value,
+            groupValue: groupValue,
+            onChanged: (v) => changedValue = v,
+            toggleable: toggleable,
+          ),
+        ),
+      );
+    }
+
+    final radioFinder = find.byType(YaruRadio<int>);
+
+    await tester
+        .pumpWidget(builder(value: 1, groupValue: 1, toggleable: false));
+    await tester.tap(radioFinder);
+    expect(changedValue, equals(1));
+
+    await tester
+        .pumpWidget(builder(value: 2, groupValue: 3, toggleable: false));
+    await tester.tap(radioFinder);
+    expect(changedValue, equals(2));
+
+    await tester.pumpWidget(builder(value: 1, groupValue: 1, toggleable: true));
+    await tester.tap(radioFinder);
+    expect(changedValue, equals(null));
+
+    await tester.pumpWidget(builder(value: 2, groupValue: 3, toggleable: true));
+    await tester.tap(radioFinder);
+    expect(changedValue, equals(2));
+  });
+
+  testWidgets('mouse cursor changes depending on the state', (tester) async {
+    await tester.pumpWidget(
+      MaterialApp(
+        home: Scaffold(
+          body: Column(
+            children: [
+              YaruRadio<int>(
+                value: 0,
+                groupValue: 0,
+                onChanged: (_) {},
+              ),
+              const YaruRadio<int>(
+                value: 0,
+                groupValue: 0,
+                onChanged: null,
+              ),
+            ],
+          ),
+        ),
+      ),
+    );
+
+    final gesture =
+        await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
+    await gesture.addPointer(location: Offset.zero);
+    addTearDown(gesture.removePointer);
+
+    await gesture.moveTo(
+      tester.getCenter(
+        find.byWidgetPredicate(
+          (widget) => widget is YaruRadio<int> && widget.onChanged != null,
+        ),
+      ),
+    );
+    await tester.pump();
+    expect(
+      RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
+      SystemMouseCursors.click,
+    );
+
+    await gesture.moveTo(
+      tester.getCenter(
+        find.byWidgetPredicate(
+          (widget) => widget is YaruRadio<int> && widget.onChanged == null,
+        ),
+      ),
+    );
+    await tester.pump();
+    expect(
+      RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
+      SystemMouseCursors.basic,
+    );
+  });
+
+  testWidgets(
+    'golden images',
+    (tester) async {
+      final variant = goldenVariant.currentValue!;
+
+      // ensure traditional focus highlight
+      FocusManager.instance.highlightStrategy =
+          FocusHighlightStrategy.alwaysTraditional;
+
+      await tester.pumpScaffold(
+        YaruRadio<bool>(
+          autofocus: variant.hasState(MaterialState.focused),
+          value: variant.hasState(MaterialState.selected),
+          groupValue: true,
+          onChanged: variant.hasState(MaterialState.disabled) ? null : (_) {},
+        ),
+        themeMode: variant.themeMode,
+        size: const Size(224, 56),
+      );
+      await tester.pumpAndSettle();
+
+      if (variant.hasState(MaterialState.pressed)) {
+        await tester.down(find.byType(YaruRadio<bool>));
+        await tester.pumpAndSettle();
+      } else if (variant.hasState(MaterialState.hovered)) {
+        await tester.hover(find.byType(YaruRadio<bool>));
+        await tester.pumpAndSettle();
+      }
+
+      await expectLater(
+        find.byType(YaruRadio<bool>),
+        matchesGoldenFile('goldens/yaru_radio-${variant.label}.png'),
+      );
+    },
+    variant: goldenVariant,
+    tags: 'golden',
+  );
+}
+
+final goldenVariant = ValueVariant({
+  ...goldenThemeVariants('unchecked', <MaterialState>{}),
+  ...goldenThemeVariants('unckecked-disabled', {MaterialState.disabled}),
+  ...goldenThemeVariants('unckecked-focused', {MaterialState.focused}),
+  ...goldenThemeVariants('unckecked-hovered', {MaterialState.hovered}),
+  ...goldenThemeVariants('unckecked-pressed', {MaterialState.pressed}),
+  ...goldenThemeVariants('checked', {MaterialState.selected}),
+  ...goldenThemeVariants('checked-disabled', {
+    MaterialState.selected,
+    MaterialState.disabled,
+  }),
+  ...goldenThemeVariants('checked-focused', {
+    MaterialState.selected,
+    MaterialState.focused,
+  }),
+  ...goldenThemeVariants('checked-hovered', {
+    MaterialState.selected,
+    MaterialState.hovered,
+  }),
+  ...goldenThemeVariants('checked-pressed', {
+    MaterialState.selected,
+    MaterialState.pressed,
+  }),
+});