Repository: ubuntu/yaru.dart

Issue:
Add YaruCheckbox




Fix patch:
diff --git a/example/lib/example_page_items.dart b/example/lib/example_page_items.dart
index 6f625c4da..2696e0f94 100644
--- a/example/lib/example_page_items.dart
+++ b/example/lib/example_page_items.dart
@@ -4,6 +4,7 @@ import 'pages/banner_page.dart';
 import 'pages/carousel_page.dart';
 import 'pages/check_button_page.dart';
 import 'pages/color_disk_page.dart';
+import 'pages/controls_page.dart';
 import 'pages/dialog_page.dart';
 import 'pages/draggable_page.dart';
 import 'pages/expandable_page.dart';
@@ -33,6 +34,12 @@ class PageItem {
 }
 
 final examplePageItems = <PageItem>[
+  PageItem(
+    titleBuilder: (context) => const Text('Controls'),
+    tooltipMessage: 'Controls',
+    pageBuilder: (context) => const ControlsPage(),
+    iconBuilder: (context, selected) => const Icon(YaruIcons.games),
+  ),
   PageItem(
     titleBuilder: (context) => const Text('YaruBanner'),
     tooltipMessage: 'YaruBanner',
diff --git a/example/lib/pages/controls_page.dart b/example/lib/pages/controls_page.dart
new file mode 100644
index 000000000..ecff17e8b
--- /dev/null
+++ b/example/lib/pages/controls_page.dart
@@ -0,0 +1,42 @@
+import 'package:flutter/material.dart';
+import 'package:yaru_widgets/yaru_widgets.dart';
+
+class ControlsPage extends StatefulWidget {
+  const ControlsPage({super.key});
+
+  @override
+  _ControlsPageState createState() => _ControlsPageState();
+}
+
+class _ControlsPageState extends State<ControlsPage> {
+  final List<bool?> _values = [false, null, true];
+
+  @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,
+                )
+              ],
+            ),
+          ),
+          const SizedBox(height: 10),
+        ],
+      ],
+    );
+  }
+}
diff --git a/lib/src/controls/yaru_check_button.dart b/lib/src/controls/yaru_check_button.dart
index 943aba574..ac58229fb 100644
--- a/lib/src/controls/yaru_check_button.dart
+++ b/lib/src/controls/yaru_check_button.dart
@@ -1,5 +1,6 @@
 import 'package:flutter/material.dart';
 
+import 'yaru_checkbox.dart';
 import 'yaru_toggle_button.dart';
 
 /// A desktop style check button with an interactive label.
@@ -50,7 +51,7 @@ class YaruCheckButton extends StatelessWidget {
       leading: SizedBox.square(
         dimension: kMinInteractiveDimension - 8,
         child: Center(
-          child: Checkbox(
+          child: YaruCheckbox(
             value: value,
             onChanged: onChanged,
             tristate: tristate,
diff --git a/lib/src/controls/yaru_checkbox.dart b/lib/src/controls/yaru_checkbox.dart
new file mode 100644
index 000000000..f8b72b13e
--- /dev/null
+++ b/lib/src/controls/yaru_checkbox.dart
@@ -0,0 +1,457 @@
+import 'package:flutter/material.dart';
+
+const _kActivableAreaPadding = EdgeInsets.all(5);
+const _kCheckboxSize = Size.square(22);
+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 {
+  const YaruCheckbox({
+    super.key,
+    required this.value,
+    this.tristate = false,
+    required this.onChanged,
+    this.focusNode,
+    this.autofocus = false,
+  }) : assert(tristate || value != null);
+
+  final bool? value;
+
+  final bool tristate;
+
+  final ValueChanged<bool?>? onChanged;
+
+  final FocusNode? focusNode;
+
+  final bool autofocus;
+
+  @override
+  State<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,
+    );
+  }
+
+  @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();
+      });
+    }
+  }
+
+  @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) {
+      return;
+    }
+    switch (widget.value) {
+      case false:
+        widget.onChanged!(true);
+        break;
+      case true:
+        widget.onChanged!(widget.tristate ? null : false);
+        break;
+      case null:
+        widget.onChanged!(false);
+        break;
+    }
+  }
+
+  @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,
+        ),
+      ),
+    );
+  }
+}
+
+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;
+
+  @override
+  void paint(Canvas canvas, Size canvasSize) {
+    final t = position.value;
+    final drawingOrigin = Offset(
+      _kCheckboxActiveResizeFactor / 2 * sizePosition.value,
+      _kCheckboxActiveResizeFactor / 2 * sizePosition.value,
+    );
+    final drawingSize = Size(
+      canvasSize.width - _kCheckboxActiveResizeFactor * sizePosition.value,
+      canvasSize.height - _kCheckboxActiveResizeFactor * sizePosition.value,
+    );
+
+    _drawStateIndicator(canvas, canvasSize);
+    _drawBox(
+      canvas,
+      drawingSize,
+      drawingOrigin,
+      oldValue == false || value == 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) {
+        _drawCheckMark(canvas, drawingSize, drawingOrigin, t);
+      } else if (oldValue == null || value == null) {
+        _drawDash(canvas, drawingSize, drawingOrigin, t);
+      }
+    }
+    // Two cases: null to true, true to null
+    else {
+      if (t <= 0.5) {
+        final tShrink = 1 - t * 2;
+        if (oldValue == true) {
+          _drawCheckMark(canvas, drawingSize, drawingOrigin, tShrink);
+        } else {
+          _drawDash(canvas, drawingSize, drawingOrigin, tShrink);
+        }
+      } else {
+        final tExpand = (t - 0.5) * 2.0;
+        if (value == true) {
+          _drawCheckMark(canvas, drawingSize, drawingOrigin, tExpand);
+        } else {
+          _drawDash(canvas, drawingSize, drawingOrigin, tExpand);
+        }
+      }
+    }
+  }
+
+  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(
+        Rect.fromLTWH(origin.dx, origin.dy, size.width, size.height),
+        _kCheckboxBorderRadius,
+      ),
+      Paint()
+        ..color = interactive
+            ? Color.lerp(uncheckedColor, checkedColor, t)!
+            : Color.lerp(uncheckedDisabledColor, checkedDisabledColor, t)!
+        ..style = PaintingStyle.fill,
+    );
+  }
+
+  void _drawCheckMark(Canvas canvas, Size size, Offset origin, double t) {
+    final path = Path();
+
+    final start = Offset(size.width * 0.1818, size.height * 0.4545);
+    final mid = Offset(size.width * 0.4091, size.height * 0.6818);
+    final end = Offset(size.width * 0.8128, size.height * 0.2781);
+
+    if (t < 0.5) {
+      final strokeT = t * 2.0;
+      final drawMid = Offset.lerp(start, mid, strokeT)!;
+
+      path.moveTo(origin.dx + start.dx, origin.dy + start.dy);
+      path.lineTo(origin.dx + drawMid.dx, origin.dy + drawMid.dy);
+      path.lineTo(origin.dx + start.dx, origin.dy + start.dy);
+    } else {
+      final strokeT = (t - 0.5) * 2.0;
+      final drawEnd = Offset.lerp(mid, end, strokeT)!;
+
+      path.moveTo(origin.dx + start.dx, origin.dy + start.dy);
+      path.lineTo(origin.dx + mid.dx, origin.dy + mid.dy);
+      path.lineTo(origin.dx + drawEnd.dx, origin.dy + drawEnd.dy);
+    }
+
+    canvas.drawPath(
+      path,
+      _getCheckmarkPaint(),
+    );
+  }
+
+  void _drawDash(Canvas canvas, Size size, Offset origin, double t) {
+    const dashMarginFactor = (1 - _kDashSizeFactor) / 2;
+
+    final start = Offset(size.width * dashMarginFactor, size.height * 0.5);
+    final mid = Offset(size.width * 0.5, size.height * 0.5);
+    final end = Offset(size.width * (1 - dashMarginFactor), size.height * 0.5);
+
+    final drawStart = Offset.lerp(start, mid, 1.0 - t)!;
+    final drawEnd = Offset.lerp(mid, end, t)!;
+
+    canvas.drawLine(
+      origin + drawStart,
+      origin + drawEnd,
+      _getCheckmarkPaint(),
+    );
+  }
+
+  Paint _getCheckmarkPaint() {
+    return Paint()
+      ..color = interactive ? checkmarkColor : checkmarkDisabledColor
+      ..style = PaintingStyle.stroke
+      ..strokeWidth = _kCheckboxDashStroke;
+  }
+
+  @override
+  bool shouldRepaint(covariant _YaruCheckboxPainter oldDelegate) => true;
+}
diff --git a/lib/yaru_widgets.dart b/lib/yaru_widgets.dart
index d11170506..fe5d12929 100644
--- a/lib/yaru_widgets.dart
+++ b/lib/yaru_widgets.dart
@@ -5,6 +5,7 @@ export 'src/constants.dart';
 // Controls
 export 'src/controls/yaru_back_button.dart';
 export 'src/controls/yaru_check_button.dart';
+export 'src/controls/yaru_checkbox.dart';
 export 'src/controls/yaru_close_button.dart';
 export 'src/controls/yaru_color_disk.dart';
 export 'src/controls/yaru_icon_button.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 e144625b9..6e097bf0c 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 7dedede76..17364b8b2 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 e349d7674..6a7c3294a 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 c7d974a3f..e46c96fe2 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 f723485e8..60f1574b7 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 9393820ff..059913ba0 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 444fc6819..adba4cc34 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 193b2ad54..a804d7560 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 9393820ff..a99d39e75 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 444fc6819..3a27960cd 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 a5c39abca..1dc3b7694 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 385f2bcdb..353f7038f 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 c519b8cd7..46a82dc83 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 ae601553c..fd07e86b6 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 6527c257d..ac3f73702 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 8456265e8..f3bcaa56c 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 c072e11ba..05c83f93d 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 042b41202..82570827d 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 8456265e8..8e4ee83a1 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 c072e11ba..43e479516 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 1ce592105..fee2b6dfc 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 cea1cc1b6..b5466a596 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 148bd37fa..e87920148 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 a4f1f6a5e..73127c565 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 a3d25c2d1..31bfdc269 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 5d7b328d2..d3e29c696 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 18cb29369..6245a4de7 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 bc591c7a0..ce0c6f57e 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 18cb29369..e10775dc0 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 bc591c7a0..5f0e018ca 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
new file mode 100644
index 000000000..09394aacd
Binary files /dev/null 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
new file mode 100644
index 000000000..7a2fdef10
Binary files /dev/null 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
new file mode 100644
index 000000000..e058bbf0e
Binary files /dev/null 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
new file mode 100644
index 000000000..9b98f390f
Binary files /dev/null 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
new file mode 100644
index 000000000..587dab35e
Binary files /dev/null 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
new file mode 100644
index 000000000..7925d29c6
Binary files /dev/null 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
new file mode 100644
index 000000000..0bb2921b3
Binary files /dev/null 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
new file mode 100644
index 000000000..fa2aeb211
Binary files /dev/null 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
new file mode 100644
index 000000000..862d813bd
Binary files /dev/null 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
new file mode 100644
index 000000000..e21fe7f72
Binary files /dev/null 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
new file mode 100644
index 000000000..9cce4c3c7
Binary files /dev/null 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
new file mode 100644
index 000000000..a2f86f04e
Binary files /dev/null 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
new file mode 100644
index 000000000..5dbd2c7ba
Binary files /dev/null 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
new file mode 100644
index 000000000..a1ac79ac6
Binary files /dev/null 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
new file mode 100644
index 000000000..14a0c0369
Binary files /dev/null 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
new file mode 100644
index 000000000..55f3542b7
Binary files /dev/null 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
new file mode 100644
index 000000000..ee7038abf
Binary files /dev/null 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
new file mode 100644
index 000000000..2570517f1
Binary files /dev/null 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
new file mode 100644
index 000000000..551359e9f
Binary files /dev/null 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
new file mode 100644
index 000000000..f2fa58294
Binary files /dev/null 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
new file mode 100644
index 000000000..d235d0605
Binary files /dev/null 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
new file mode 100644
index 000000000..430b93b35
Binary files /dev/null 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
new file mode 100644
index 000000000..b99b4ecc7
Binary files /dev/null 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
new file mode 100644
index 000000000..d57be38ca
Binary files /dev/null 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
new file mode 100644
index 000000000..d087fcbb7
Binary files /dev/null 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
new file mode 100644
index 000000000..8f0e1248e
Binary files /dev/null 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
new file mode 100644
index 000000000..2e2ba2237
Binary files /dev/null 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
new file mode 100644
index 000000000..9f7cf9022
Binary files /dev/null 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
new file mode 100644
index 000000000..59151511d
Binary files /dev/null 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
new file mode 100644
index 000000000..d62bc8b65
Binary files /dev/null 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 a7e6c6903..01af72da2 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 4207430c0..5d2002eb3 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 042cbda0e..fd7ed0ab8 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 f7f5bc946..f91098fd8 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 317050574..32c7aa6a7 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 47769908c..8850eb9d7 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 e000ae6f7..c26cbf852 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 c92a4ddd3..9cb678bc6 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 3f4b7f09c..1d2c8fd68 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 655cae243..ea18d6762 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 2977a91ff..af91a7104 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 eb73f00e2..a1addba45 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 4c06412b3..60e69544c 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 6d79bece1..ceee0e4e7 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 1da30bcd4..681fe52a6 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 ee1b64342..ba4a7a2fe 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/yaru_check_button_test.dart b/test/controls/yaru_check_button_test.dart
index d92713853..36df346aa 100644
--- a/test/controls/yaru_check_button_test.dart
+++ b/test/controls/yaru_check_button_test.dart
@@ -25,14 +25,14 @@ void main() {
         .pumpWidget(builder(title: const Text('title'), subtitle: null));
     expect(find.text('title'), findsOneWidget);
     expect(find.text('subtitle'), findsNothing);
-    expect(find.byType(Checkbox), findsOneWidget);
+    expect(find.byType(YaruCheckbox), 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(Checkbox), findsOneWidget);
+    expect(find.byType(YaruCheckbox), findsOneWidget);
   });
 
   testWidgets('the labels react to taps', (tester) async {
@@ -172,10 +172,10 @@ void main() {
       await tester.pumpAndSettle();
 
       if (variant.hasState(MaterialState.pressed)) {
-        await tester.down(find.byType(Checkbox));
+        await tester.down(find.byType(YaruCheckbox));
         await tester.pumpAndSettle();
       } else if (variant.hasState(MaterialState.hovered)) {
-        await tester.hover(find.byType(Checkbox));
+        await tester.hover(find.byType(YaruCheckbox));
         await tester.pumpAndSettle();
       }
 
diff --git a/test/controls/yaru_checkbox_test.dart b/test/controls/yaru_checkbox_test.dart
new file mode 100644
index 000000000..eedf6331a
--- /dev/null
+++ b/test/controls/yaru_checkbox_test.dart
@@ -0,0 +1,168 @@
+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('the checkbox react to taps', (tester) async {
+    bool? changedValue;
+    Widget builder({required bool? initialValue, required bool tristate}) {
+      return MaterialApp(
+        home: Scaffold(
+          body: YaruCheckbox(
+            value: initialValue,
+            tristate: tristate,
+            onChanged: (v) => changedValue = v,
+          ),
+        ),
+      );
+    }
+
+    final checkboxFinder = find.byType(YaruCheckbox);
+
+    await tester.pumpWidget(builder(initialValue: false, tristate: false));
+    await tester.tap(checkboxFinder);
+    expect(changedValue, isTrue);
+
+    await tester.pumpWidget(builder(initialValue: true, tristate: false));
+    await tester.tap(checkboxFinder);
+    expect(changedValue, isFalse);
+
+    await tester.pumpWidget(builder(initialValue: false, tristate: true));
+    await tester.tap(checkboxFinder);
+    expect(changedValue, isTrue);
+
+    await tester.pumpWidget(builder(initialValue: true, tristate: true));
+    await tester.tap(checkboxFinder);
+    expect(changedValue, null);
+
+    await tester.pumpWidget(builder(initialValue: null, tristate: true));
+    await tester.tap(checkboxFinder);
+    expect(changedValue, isFalse);
+  });
+
+  testWidgets('mouse cursor changes depending on the state', (tester) async {
+    await tester.pumpWidget(
+      MaterialApp(
+        home: Scaffold(
+          body: Column(
+            children: [
+              YaruCheckbox(
+                value: false,
+                onChanged: (_) {},
+              ),
+              const YaruCheckbox(
+                value: false,
+                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 YaruCheckbox && widget.onChanged != null,
+        ),
+      ),
+    );
+    await tester.pump();
+    expect(
+      RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
+      SystemMouseCursors.click,
+    );
+
+    await gesture.moveTo(
+      tester.getCenter(
+        find.byWidgetPredicate(
+          (widget) => widget is YaruCheckbox && 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;
+
+      final tristate = variant.label.startsWith('tristate');
+
+      await tester.pumpScaffold(
+        YaruCheckbox(
+          autofocus: variant.hasState(MaterialState.focused),
+          tristate: tristate,
+          value: tristate ? null : variant.hasState(MaterialState.selected),
+          onChanged: variant.hasState(MaterialState.disabled) ? null : (_) {},
+        ),
+        themeMode: variant.themeMode,
+        size: const Size(40, 40),
+      );
+      await tester.pumpAndSettle();
+
+      if (variant.hasState(MaterialState.pressed)) {
+        await tester.down(find.byType(YaruCheckbox));
+        await tester.pumpAndSettle();
+      } else if (variant.hasState(MaterialState.hovered)) {
+        await tester.hover(find.byType(YaruCheckbox));
+        await tester.pumpAndSettle();
+      }
+
+      await expectLater(
+        find.byType(YaruCheckbox),
+        matchesGoldenFile('goldens/yaru_checkbox-${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,
+  }),
+  ...goldenThemeVariants('tristate', {MaterialState.selected}),
+  ...goldenThemeVariants('tristate-disabled', {MaterialState.disabled}),
+  ...goldenThemeVariants('tristate-focused', {MaterialState.focused}),
+  ...goldenThemeVariants('tristate-hovered', {MaterialState.hovered}),
+  ...goldenThemeVariants('tristate-pressed', {MaterialState.pressed}),
+});