Chip
One enum-driven chip — basic, input, choice & status kinds, lavender selected state
View component →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.
What's Included
Use Cases
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.
Copy this into your Flutter project. No external packages required.
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) // errorOne enum-driven chip — basic, input, choice & status kinds, lavender selected state
View component →One enum-driven button — 5 types, solid/pill/outline styles, 4 sizes, icon + loading
View component →One enum-driven badge — count, dot, label & status kinds on the house theme
View component →