fluttercnfluttercn
Components

Toggle

A smooth switch component for binary on/off interactions

Installation

fluttercn add toggle

Usage

Basic

Use Toggle anywhere you need a quick on/off switch. The component is fully controlled, so keep the current value in state and pass an onChanged callback.

Basic toggle preview
class BasicToggleExample extends StatefulWidget {
  const BasicToggleExample({super.key});
 
  @override
  State<BasicToggleExample> createState() => _BasicToggleExampleState();
}
 
class _BasicToggleExampleState extends State<BasicToggleExample> {
  bool _enabled = false;
 
  @override
  Widget build(BuildContext context) {
    return Toggle(
      value: _enabled,
      onChanged: (value) => setState(() => _enabled = value),
    );
  }
}

With Label

Add a label to describe what the toggle controls. The label automatically adapts its typography, spacing, and disabled styles.

Toggle with label preview
Toggle(
  value: notifications,
  label: 'Enable notifications',
  onChanged: (value) => setState(() => notifications = value),
);

With Description

Provide extra context using description. It renders under the primary label with the proper font size for the current toggle size.

Toggle with description preview
Toggle(
  value: darkMode,
  label: 'Dark mode',
  description: 'Switch to dark theme for low-light environments',
  onChanged: (value) => setState(() => darkMode = value),
);

Sizes

Pick from three sizes to match the density of the surrounding UI. Track/Thumb dimensions, spacing, and typography all scale together.

Toggle sizes preview
Wrap(
  spacing: 16,
  runSpacing: 16,
  children: ToggleSize.values
      .map(
        (size) => Toggle(
          value: values[size] ?? false,
          size: size,
          label: '${size.name.toUpperCase()} toggle',
          onChanged: (value) => setState(() => values[size] = value),
        ),
      )
      .toList(),
);

Disabled State

Set disabled to true to show non-interactive toggles. The cursor, track, thumb, and label all adopt disabled styles automatically.

Toggle disabled state preview
Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: const [
    Toggle(
      value: true,
      label: 'Disabled on',
      disabled: true,
      onChanged: null,
    ),
    SizedBox(height: 16),
    Toggle(
      value: false,
      label: 'Disabled off',
      disabled: true,
      onChanged: null,
    ),
  ],
);

Error State

Use error and errorText to surface validation issues, like confirming terms of service or other mandatory options.

Toggle error state preview
Toggle(
  value: acceptTerms,
  label: 'I accept the terms and conditions',
  error: showError,
  errorText: showError ? 'You must accept the terms to continue' : null,
  onChanged: (value) => setState(() => acceptTerms = value),
);

Custom Active Color

Override the success-colored track by passing activeColor. The component interpolates between the inactive surface and your color so transitions stay smooth.

Toggle(
  value: autoUpdate,
  label: 'Auto-update apps',
  activeColor: Colors.blueAccent,
  onChanged: (value) => setState(() => autoUpdate = value),
);

Toggles animate both the track fill and thumb translation. When moving from off → on, the color change is delayed until the thumb crosses the midpoint, mirroring modern OS behavior.

Because Toggle is controlled, keep the source of truth in state. Not updating value inside onChanged will make the component appear unresponsive.

API Reference

Toggle

PropertyTypeDefaultDescription
valueboolrequiredCurrent on/off value.
onChangedValueChanged<bool>?required*Called when the user toggles the switch.
labelString?nullOptional label rendered to the right of the switch.
descriptionString?nullSecondary text shown below the label.
sizeToggleSizeToggleSize.mdControls track, thumb, gap, and font sizes.
disabledboolfalseDisables hover, press, and tap interactions.
errorboolfalseForces the track into the error color.
errorTextString?nullOptional helper text displayed under the toggle.
activeColorColor?AppTheme.successCustom track color when value is true.

*Optional in the API to support disabled/onChanged-less previews, but required for interactivity.

ToggleSize

  • sm – Compact 36×20 track with 14px thumb, small typography
  • md – Default 44×24 track with 18px thumb
  • lg – Spacious 52×28 track with 22px thumb and larger text

Features

  • Smooth animations – Independent thumb translation and track color easing
  • Labels & descriptions – Built-in typography and spacing for adjacent text
  • Error + helper text – Inline validation messaging support
  • Custom colors – Override the success accent without rewriting styles
  • Accessible states – Disabled cursor, hover, and pressed feedback baked in
  • Responsive sizing – Three presets to match any layout density

Examples

Notification Preferences

Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    Toggle(
      value: emailUpdates,
      label: 'Email updates',
      description: 'Product launches, tips, and inspiration',
      onChanged: (value) => setState(() => emailUpdates = value),
    ),
    const SizedBox(height: 16),
    Toggle(
      value: pushAlerts,
      label: 'Push alerts',
      description: 'Remind me about important account events',
      onChanged: (value) => setState(() => pushAlerts = value),
    ),
  ],
);

Terms Confirmation

class TermsExampleState extends State<TermsExample> {
  bool _accepted = false;
  bool _submitted = false;
 
  void _handleSubmit() {
    setState(() => _submitted = true);
    if (_accepted) {
      // Continue flow...
    }
  }
 
  @override
  Widget build(BuildContext context) {
    final showError = _submitted && !_accepted;
 
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Toggle(
          value: _accepted,
          label: 'I agree to the updated Terms of Service',
          error: showError,
          errorText: showError
              ? 'You must accept the terms before continuing'
              : null,
          onChanged: (value) {
            setState(() {
              _accepted = value;
              if (_submitted && value) _submitted = false;
            });
          },
        ),
        const SizedBox(height: 24),
        Button(onPressed: _handleSubmit, child: const Text('Continue')),
      ],
    );
  }
}

Best Practices

  • Use toggles only for instantaneous, reversible options
  • Pair every toggle with clear labeling; add description when the action needs context
  • Group related toggles in columns or lists with consistent spacing
  • Prefer forms or confirmation dialogs when the action is destructive or delayed
  • Disable toggles while async work is running to prevent conflicting updates
  • Keep the source of truth in state and debounce network syncs if needed