SELECTION CONTROLS

Checkbox

A single, reusable Flutter checkbox driven by a CheckState enum. Covers all three states — unchecked, checked (lavender fill), and indeterminate (a dash for partial selection) — plus an optional label, a disabled look, and an error state with a danger-red border. Built on the FlutterKit house theme with a 6px-radius box and a 48px touch target. No external packages, fully DartPad-ready.

checkboxtristateform
Preview8 variants

Unchecked

Checked

Indeterminate

What's Included

  • Three states — unchecked, checked, and indeterminate via a CheckState enum
  • Lavender checked fill with a crisp white check icon
  • Indeterminate dash for "some but not all" parent rows
  • Optional trailing label that toggles the box when tapped
  • Disabled look — muted fill and tertiary ink, taps ignored
  • Error state — danger-red border for required, unchecked fields

Use Cases

  • Terms & conditions — a required checkbox with an error state on submit
  • Multi-select lists — a checked box per row with a "select all" parent
  • Indeterminate parents — partial child selection shows the dash state
  • Settings toggles — labelled checkboxes for opt-in preferences

PRO TIP

Reach for the indeterminate state whenever you have a "select all" parent over a list of children — it reads as "some, but not all" far more clearly than an unchecked or checked box. Drive it from your own state by counting selected children (none → unchecked, all → checked, otherwise → indeterminate) rather than storing a third boolean, so the parent always stays a single source of truth. Keep the tap target at 48px even though the box is only 20px, so it stays comfortable on touch.

Complete Dart Code

Copy this into your Flutter project. No external packages required.

dart
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  CheckState _terms = CheckState.unchecked;
  CheckState _all = CheckState.indeterminate;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        backgroundColor: const Color(0xFFFAFAFB),
        body: Center(
          child: Padding(
            padding: const EdgeInsets.all(24),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                AppCheckbox(
                  state: _all,
                  label: 'Select all',
                  onChanged: (s) => setState(() => _all = s),
                ),
                const SizedBox(height: 12),
                AppCheckbox(
                  state: _terms,
                  label: 'I agree to the terms',
                  onChanged: (s) => setState(() => _terms = s),
                ),
                const SizedBox(height: 12),
                const AppCheckbox(
                  state: CheckState.checked,
                  label: 'Disabled',
                  enabled: false,
                ),
                const SizedBox(height: 12),
                const AppCheckbox(
                  state: CheckState.unchecked,
                  label: 'Required field',
                  error: true,
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

/// FlutterKit Checkbox — house design system (Linear lavender, light theme).
/// One enum-driven checkbox: unchecked, checked, and indeterminate states.
/// rohansurve.in/flutterkit/checkbox

enum CheckState { unchecked, checked, indeterminate }

class AppCheckbox extends StatelessWidget {
  const AppCheckbox({
    super.key,
    required this.state,
    this.label,
    this.onChanged,
    this.enabled = true,
    this.error = false,
  });

  /// unchecked · checked · indeterminate.
  final CheckState state;

  /// Optional trailing label; tapping it toggles the box too.
  final String? label;

  /// Called with the next state on tap (cycles unchecked ⇄ checked).
  final ValueChanged<CheckState>? onChanged;

  /// Disabled look — muted fill, tertiary ink, taps ignored.
  final bool enabled;

  /// Error look — danger-red border on an unchecked box.
  final bool error;

  // ── House tokens — docs/flutterkit-design.md §1 ──
  static const Color _accent = Color(0xFF5E6AD2);
  static const Color _surfaceMuted = Color(0xFFF4F5F7);
  static const Color _hairlineStrong = Color(0xFFD5D8DD);
  static const Color _ink = Color(0xFF1C1E26);
  static const Color _inkTertiary = Color(0xFFB4B8BF);
  static const Color _danger = Color(0xFFD92D20);

  bool get _on => state != CheckState.unchecked;

  void _handleTap() {
    if (!enabled || onChanged == null) return;
    onChanged!(_on ? CheckState.unchecked : CheckState.checked);
  }

  @override
  Widget build(BuildContext context) {
    // Resolve box fill / border / icon color from state + flags.
    late final Color fill;
    late final Color border;

    if (!enabled) {
      fill = _on ? _surfaceMuted : _surfaceMuted;
      border = _hairlineStrong;
    } else if (_on) {
      fill = _accent;
      border = _accent;
    } else {
      fill = Colors.transparent;
      border = error ? _danger : _hairlineStrong;
    }

    final iconColor = enabled ? Colors.white : _inkTertiary;

    final box = Container(
      width: 20,
      height: 20,
      alignment: Alignment.center,
      decoration: BoxDecoration(
        color: fill,
        borderRadius: BorderRadius.circular(6),
        border: Border.all(color: border, width: error && !_on ? 2 : 1.5),
      ),
      child: state == CheckState.checked
          ? Icon(Icons.check, size: 14, color: iconColor)
          : state == CheckState.indeterminate
              ? Icon(Icons.remove, size: 14, color: iconColor)
              : const SizedBox.shrink(),
    );

    final row = Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        box,
        if (label != null) ...[
          const SizedBox(width: 10),
          Text(
            label!,
            style: TextStyle(
              color: enabled ? _ink : _inkTertiary,
              fontSize: 14,
              fontWeight: FontWeight.w400,
              height: 1.4,
            ),
          ),
        ],
      ],
    );

    return Material(
      color: Colors.transparent,
      child: InkWell(
        onTap: enabled ? _handleTap : null,
        borderRadius: BorderRadius.circular(8),
        child: Padding(
          // 48px touch target around the 20px box.
          padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 4),
          child: row,
        ),
      ),
    );
  }
}

// ── Usage ──
// AppCheckbox(state: CheckState.unchecked, onChanged: (s) {})        // unchecked
// AppCheckbox(state: CheckState.checked, onChanged: (s) {})          // checked
// AppCheckbox(state: CheckState.indeterminate, onChanged: (s) {})    // dash
// AppCheckbox(state: s, label: 'I agree', onChanged: (v) {})         // labelled
// AppCheckbox(state: CheckState.checked, enabled: false)             // disabled
// AppCheckbox(state: CheckState.unchecked, error: true)              // error

How to use this widget

  1. 1Copy the complete Dart code above
  2. 2Create a new file — app_checkbox.dart in your Flutter project
  3. 3Use AppCheckbox(state: CheckState.checked, onChanged: (s) {}) — switch the look with the state, label, enabled, and error parameters

Related components

Avatars & Chips

Chip

One enum-driven chip — basic, input, choice & status kinds, lavender selected state

View component →
Buttons & Actions

Button Widget

One enum-driven button — 5 types, solid/pill/outline styles, 4 sizes, icon + loading

View component →
Avatars & Chips

Badge

One enum-driven badge — count, dot, label & status kinds on the house theme

View component →