image

05 Feb 2026

9K

35K

Custom Checkbox and Radio Button Widgets in Flutter

Flutter provides excellent default widgets for common UI elements, including Checkbox and Radio buttons. While functional, their customization options are often limited to basic properties like color and shape. For applications requiring a unique brand identity, complex interactive states, or highly specific visual designs, creating custom checkbox and radio button widgets becomes essential. This article will guide you through the process of building your own highly customizable versions of these fundamental input elements.

Why Customize?

  • Brand Consistency: Ensure your UI elements perfectly match your application's design language.
  • Enhanced User Experience: Implement unique visual feedback or animations that default widgets don't offer.
  • Complex States: Handle intricate UI states (e.g., indeterminate states, disabled with specific styling) beyond standard capabilities.
  • Code Reusability: Create a single, flexible component that can be used throughout your app with different configurations.

Flutter's Default Widgets: A Quick Look

Before diving into custom implementations, it's useful to recall how Flutter's default Checkbox and Radio widgets work. They are typically used within a StatefulWidget to manage their value and groupValue (for radio buttons) respectively.


import 'package:flutter/material.dart';

class DefaultWidgetsExample extends StatefulWidget {
  @override
  _DefaultWidgetsExampleState createState() => _DefaultWidgetsExampleState();
}

class _DefaultWidgetsExampleState extends State {
  bool _isChecked = false;
  String? _selectedRadio = 'optionA';

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // Default Checkbox
        Row(
          children: [
            Checkbox(
              value: _isChecked,
              onChanged: (bool? newValue) {
                setState(() {
                  _isChecked = newValue ?? false;
                });
              },
            ),
            Text('Default Checkbox'),
          ],
        ),
        // Default Radio Buttons
        Row(
          children: [
            Radio(
              value: 'optionA',
              groupValue: _selectedRadio,
              onChanged: (String? value) {
                setState(() {
                  _selectedRadio = value;
                });
              },
            ),
            Text('Option A'),
          ],
        ),
        Row(
          children: [
            Radio(
              value: 'optionB',
              groupValue: _selectedRadio,
              onChanged: (String? value) {
                setState(() {
                  _selectedRadio = value;
                });
              },
            ),
            Text('Option B'),
          ],
        ),
      ],
    );
  }
}

Core Concepts for Customization

Building custom interactive widgets in Flutter primarily involves these components:

  • StatefulWidget: Essential for widgets whose appearance or behavior can change over time based on user interaction or data.
  • GestureDetector: Detects user gestures like taps, double taps, long presses, etc., allowing you to trigger actions.
  • AnimatedContainer: Creates containers that smoothly animate their properties (color, size, border-radius) over a specified duration. Perfect for visual feedback.
  • Container & Icon: Fundamental building blocks for drawing the visual appearance.
  • Callbacks (Function): To communicate changes back to the parent widget, mimicking the onChanged property of default widgets.

Creating a Custom Checkbox Widget

Let's create a custom checkbox that changes its background color and displays an icon when checked.


import 'package:flutter/material.dart';

class CustomCheckbox extends StatefulWidget {
  final bool value;
  final ValueChanged onChanged;
  final Color activeColor;
  final Color inactiveColor;
  final Color iconColor;
  final double size;
  final double borderRadius;

  const CustomCheckbox({
    Key? key,
    required this.value,
    required this.onChanged,
    this.activeColor = Colors.blue,
    this.inactiveColor = Colors.grey,
    this.iconColor = Colors.white,
    this.size = 24.0,
    this.borderRadius = 4.0,
  }) : super(key: key);

  @override
  _CustomCheckboxState createState() => _CustomCheckboxState();
}

class _CustomCheckboxState extends State {
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        widget.onChanged(!widget.value);
      },
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 200),
        curve: Curves.easeIn,
        width: widget.size,
        height: widget.size,
        decoration: BoxDecoration(
          color: widget.value ? widget.activeColor : widget.inactiveColor,
          borderRadius: BorderRadius.circular(widget.borderRadius),
          border: Border.all(
            color: widget.value ? widget.activeColor : widget.inactiveColor.withOpacity(0.6),
            width: 2,
          ),
        ),
        child: widget.value
            ? Icon(
                Icons.check,
                size: widget.size * 0.7, // Adjust icon size relative to checkbox
                color: widget.iconColor,
              )
            : null,
      ),
    );
  }
}

To use this custom checkbox:


import 'package:flutter/material.dart';
// Assuming CustomCheckbox widget is defined in custom_checkbox.dart

class CustomCheckboxExample extends StatefulWidget {
  @override
  _CustomCheckboxExampleState createState() => _CustomCheckboxExampleState();
}

class _CustomCheckboxExampleState extends State {
  bool _myCustomCheck = false;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          CustomCheckbox(
            value: _myCustomCheck,
            onChanged: (newValue) {
              setState(() {
                _myCustomCheck = newValue;
              });
            },
            activeColor: Colors.teal,
            inactiveColor: Colors.red.shade100,
            iconColor: Colors.black,
            size: 30,
            borderRadius: 8,
          ),
          SizedBox(width: 10),
          Text('My Custom Checkbox'),
        ],
      ),
    );
  }
}

Creating a Custom Radio Button Widget

Custom radio buttons work similarly to checkboxes but require managing a groupValue to ensure only one radio button in a group is selected at a time. We'll create a generic CustomRadio<T> widget to handle any data type for its value.


import 'package:flutter/material.dart';

class CustomRadio extends StatefulWidget {
  final T value;
  final T groupValue;
  final ValueChanged onChanged;
  final Color activeColor;
  final Color inactiveColor;
  final double size;
  final double innerCircleRadiusFactor; // How big the inner circle is relative to the outer size

  const CustomRadio({
    Key? key,
    required this.value,
    required this.groupValue,
    required this.onChanged,
    this.activeColor = Colors.blue,
    this.inactiveColor = Colors.grey,
    this.size = 24.0,
    this.innerCircleRadiusFactor = 0.5, // Default inner circle is half the size
  }) : super(key: key);

  @override
  _CustomRadioState createState() => _CustomRadioState();
}

class _CustomRadioState extends State> {
  bool get _isSelected => widget.value == widget.groupValue;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        if (!_isSelected) {
          widget.onChanged(widget.value);
        }
      },
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 200),
        curve: Curves.easeIn,
        width: widget.size,
        height: widget.size,
        decoration: BoxDecoration(
          shape: BoxShape.circle,
          color: Colors.transparent, // Outer circle is transparent or has a border
          border: Border.all(
            color: _isSelected ? widget.activeColor : widget.inactiveColor,
            width: 2,
          ),
        ),
        child: Center(
          child: AnimatedContainer(
            duration: const Duration(milliseconds: 200),
            curve: Curves.easeIn,
            width: _isSelected ? widget.size * widget.innerCircleRadiusFactor : 0,
            height: _isSelected ? widget.size * widget.innerCircleRadiusFactor : 0,
            decoration: BoxDecoration(
              shape: BoxShape.circle,
              color: _isSelected ? widget.activeColor : Colors.transparent,
            ),
          ),
        ),
      ),
    );
  }
}

To use this custom radio button:


import 'package:flutter/material.dart';
// Assuming CustomRadio widget is defined in custom_radio.dart

enum SingingCharacter { lafayette, jefferson }

class CustomRadioExample extends StatefulWidget {
  @override
  _CustomRadioExampleState createState() => _CustomRadioExampleState();
}

class _CustomRadioExampleState extends State {
  SingingCharacter? _character = SingingCharacter.lafayette;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              CustomRadio(
                value: SingingCharacter.lafayette,
                groupValue: _character,
                onChanged: (SingingCharacter? newValue) {
                  setState(() {
                    _character = newValue;
                  });
                },
                activeColor: Colors.purple,
                inactiveColor: Colors.deepPurple.shade100,
                size: 28,
                innerCircleRadiusFactor: 0.4,
              ),
              SizedBox(width: 10),
              Text('Lafayette'),
            ],
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              CustomRadio(
                value: SingingCharacter.jefferson,
                groupValue: _character,
                onChanged: (SingingCharacter? newValue) {
                  setState(() {
                    _character = newValue;
                  });
                },
                activeColor: Colors.purple,
                inactiveColor: Colors.deepPurple.shade100,
                size: 28,
                innerCircleRadiusFactor: 0.4,
              ),
              SizedBox(width: 10),
              Text('Jefferson'),
            ],
          ),
        ],
      ),
    );
  }
}

Conclusion

Creating custom checkbox and radio button widgets in Flutter empowers you to design highly personalized and brand-aligned user interfaces. By leveraging fundamental Flutter concepts like StatefulWidget, GestureDetector, and AnimatedContainer, you can build interactive components that not only look unique but also provide richer user feedback. This approach enhances the overall aesthetic and user experience of your application, making it stand out from standard implementations.

Related Articles

May 14, 2026

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter Countdown timers are essential in many applic

May 11, 2026

Unleashing Dynamic UIs: Flutter's Animation Prowess

Unleashing Dynamic UIs: Flutter's Animation Prowess for Slide & Scale Effects Flutter's declarative UI framework, combined with its powerful animation capabilit

May 11, 2026

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy A well-designed Product Detail Page (PDP) is