Checkbox
Tristate checkbox — unchecked, checked & indeterminate, with label, disabled & error states
View component →A single, reusable Flutter switch driven by a bool value and a SwitchSize enum. The thumb slides with a 180ms animation, the track fills lavender when on and an inset grey when off, and it ships in two sizes (small and medium) plus an optional label and a disabled look. Built on the FlutterKit house theme with a pill-shaped track and a 48px touch target. No external packages, fully DartPad-ready.
What's Included
Use Cases
PRO TIP
Use a switch only for an instant, binary setting that takes effect immediately — if the change needs a Save button or has more than two states, reach for a checkbox or segmented control instead. Drive the value from your own state and update it inside onChanged so the switch never holds local truth, and keep the tap target at 48px even though the track is shorter, 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> {
bool _wifi = true;
bool _bluetooth = false;
@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: [
AppSwitch(
value: _wifi,
label: 'Wi-Fi',
onChanged: (v) => setState(() => _wifi = v),
),
const SizedBox(height: 12),
AppSwitch(
value: _bluetooth,
label: 'Bluetooth',
onChanged: (v) => setState(() => _bluetooth = v),
),
const SizedBox(height: 12),
const AppSwitch(
value: true,
label: 'Disabled',
enabled: false,
),
],
),
),
),
),
);
}
}
/// FlutterKit Switch — house design system (Linear lavender, light theme).
/// One value-driven switch with an animated thumb and two sizes.
/// rohansurve.in/flutterkit/switch-toggle
enum SwitchSize { small, medium }
class AppSwitch extends StatelessWidget {
const AppSwitch({
super.key,
required this.value,
this.label,
this.onChanged,
this.size = SwitchSize.medium,
this.enabled = true,
});
/// On (true) or off (false).
final bool value;
/// Optional trailing label; tapping it toggles the switch too.
final String? label;
/// Called with the next value on tap.
final ValueChanged<bool>? onChanged;
/// small · medium.
final SwitchSize size;
/// Disabled look — muted track and thumb, taps ignored.
final bool enabled;
// ── House tokens — docs/flutterkit-design.md §1 ──
static const Color _accent = Color(0xFF5E6AD2);
static const Color _surfaceInset = Color(0xFFEEEFF2);
static const Color _surfaceMuted = Color(0xFFF4F5F7);
static const Color _hairlineStrong = Color(0xFFD5D8DD);
static const Color _ink = Color(0xFF1C1E26);
static const Color _inkTertiary = Color(0xFFB4B8BF);
@override
Widget build(BuildContext context) {
final bool medium = size == SwitchSize.medium;
final double trackW = medium ? 44 : 36;
final double trackH = medium ? 26 : 22;
final double thumb = (medium ? 20 : 16).toDouble();
const double pad = 3;
// Resolve track / thumb from value + enabled.
final Color track = !enabled
? _surfaceMuted
: value
? _accent
: _surfaceInset;
final Color thumbColor = enabled ? Colors.white : const Color(0xFFF7F7F8);
final Border? trackBorder =
value ? null : Border.all(color: _hairlineStrong);
final switchWidget = AnimatedContainer(
duration: const Duration(milliseconds: 180),
curve: Curves.easeInOut,
width: trackW,
height: trackH,
padding: const EdgeInsets.all(pad),
decoration: BoxDecoration(
color: track,
borderRadius: BorderRadius.circular(999),
border: trackBorder,
),
child: AnimatedAlign(
duration: const Duration(milliseconds: 180),
curve: Curves.easeInOut,
alignment: value ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
width: thumb,
height: thumb,
decoration: BoxDecoration(
color: thumbColor,
shape: BoxShape.circle,
boxShadow: const [
BoxShadow(
color: Color(0x1F1C1E26),
blurRadius: 3,
offset: Offset(0, 1),
),
],
),
),
),
);
final row = Row(
mainAxisSize: MainAxisSize.min,
children: [
switchWidget,
if (label != null) ...[
const SizedBox(width: 12),
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 && onChanged != null ? () => onChanged!(!value) : null,
borderRadius: BorderRadius.circular(8),
child: Padding(
// 48px touch target around the track.
padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 4),
child: row,
),
),
);
}
}
// ── Usage ──
// AppSwitch(value: true, onChanged: (v) {}) // on
// AppSwitch(value: false, onChanged: (v) {}) // off
// AppSwitch(value: v, label: 'Wi-Fi', onChanged: (n) {}) // labelled
// AppSwitch(value: v, size: SwitchSize.small, onChanged: (n) {})// small
// const AppSwitch(value: true, enabled: false) // disabledTristate checkbox — unchecked, checked & indeterminate, with label, disabled & error states
View component →One 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 →