SELECTION CONTROLS

Switch Toggle

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.

switchtoggleform
Preview8 variants

Off

On

What's Included

  • Animated thumb that slides between off and on over 180ms
  • Lavender on-track and an inset-grey off-track, both pill-shaped
  • Two sizes — small and medium via a SwitchSize enum
  • Optional trailing label that toggles the switch when tapped
  • Disabled look — muted track and thumb, taps ignored
  • One AppSwitch widget driven by a bool value and onChanged callback

Use Cases

  • Settings rows — labelled switches for opt-in preferences
  • Inline toggles — a compact small switch beside a list item
  • Feature flags — enable / disable a capability in a form
  • Dark-mode / notification toggles in a profile screen

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.

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> {
  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)                  // disabled

How to use this widget

  1. 1Copy the complete Dart code above
  2. 2Create a new file — app_switch.dart in your Flutter project
  3. 3Use AppSwitch(value: true, onChanged: (v) {}) — switch the look with the value, label, size, and enabled parameters

Related components

Selection Controls

Checkbox

Tristate checkbox — unchecked, checked & indeterminate, with label, disabled & error states

View component →
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 →