ashlarui

Switch

An on and off switch.

Switch, as it looks in an app Switch, as it looks in an app, dark
import 'package:flutter/widgets.dart';

import 'ui/switch.dart';

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

  @override
  State<SwitchDemo> createState() => _SwitchDemoState();
}

class _SwitchDemoState extends State<SwitchDemo> {
  bool airplane = false;

  @override
  Widget build(BuildContext context) => Row(
    mainAxisSize: MainAxisSize.min,
    children: [
      Switch(value: airplane, onChanged: (v) => setState(() => airplane = v), semanticLabel: 'Airplane mode'),
      const SizedBox(width: 8),
      const Text('Airplane mode'),
    ],
  );
}

Try its options

Each option its constructor takes, to change and see; the code below it is what you chose, to copy.

Switch with controls, as it looks in an app Switch with controls, as it looks in an app, dark

Add it

In your Flutter project:

$ ashlar add switch

It writes theme.dart, palette.dart, group.dart, pressable.dart and switch.dart into lib/ui, keeping any you already have. It needs pressable, which come with it.

Examples

Other ways to use it, each running live, with its code.

Small

For a dense row.

Small, as it looks in an app Small, as it looks in an app, dark
// Small: For a dense row.
import 'package:flutter/widgets.dart';

import 'ui/field.dart';
import 'ui/switch.dart';

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

  @override
  State<SwitchSmallExample> createState() => _SwitchSmallExampleState();
}

class _SwitchSmallExampleState extends State<SwitchSmallExample> {
  var on = true;

  @override
  Widget build(BuildContext context) => SizedBox(
    width: 220,
    child: Field(
      axis: Axis.horizontal,
      label: const Text('Compact rows'),
      child: Switch(size: SwitchSize.sm, value: on, onChanged: (v) => setState(() => on = v)),
    ),
  );
}

In a card

The whole card flips it.

In a card, as it looks in an app In a card, as it looks in an app, dark
// In a card: The whole card flips it.
import 'package:flutter/widgets.dart';

import 'ui/field.dart';
import 'ui/switch.dart';

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

  @override
  State<SwitchCardExample> createState() => _SwitchCardExampleState();
}

class _SwitchCardExampleState extends State<SwitchCardExample> {
  var on = false;

  @override
  Widget build(BuildContext context) => SizedBox(
    width: 360,
    child: Field(
      card: true,
      selected: on,
      onPressed: () => setState(() => on = !on),
      label: const Text('Share across devices'),
      description: const Text('Focus is shared, so others see you are busy.'),
      responsive: true,
      child: Switch(value: on, onChanged: (v) => setState(() => on = v)),
    ),
  );
}

API

Read from the source above: each parameter with its type, its default, and what it is for.

SwitchSizeenum

An on and off switch, for a setting that takes effect at once. Give it a semanticLabel, or put it in a row with a Text.

ValueWhat it is for
sm24 by 14.
md32 by 18.

Switchclass

Parameter Type Default What it is for
valuerequired bool Whether it is on.
onChangedrequired ValueChanged<bool>? Null disables the switch.
semanticLabel String? What a screen reader calls it when its visible text does not say, as for an icon alone.
size SwitchSize SwitchSize.md Medium, or small to sit in a dense row.
focusNode FocusNode? Its focus node, to move focus to it or follow it from outside; one is made when not given.
autofocus bool false Takes the keyboard's focus when it first appears.

Source

What lands in your app. It is yours from then on: change it as you would your own code, and ashlar add will not write over it.

lib/ui/switch.dart 105 lines
// Ashlar UI: Switch. Copied into your app by `ashlar add switch`; it is yours to change.

import 'package:flutter/widgets.dart';

import 'pressable.dart';
import 'theme.dart';

/// An on and off switch, for a setting that takes effect at once. Give it a
/// [semanticLabel], or put it in a row with a Text.
///
/// ```dart
/// Switch(value: notify, onChanged: (v) => setState(() => notify = v), semanticLabel: 'Notifications')
/// ```
enum SwitchSize {
  /// 24 by 14.
  sm,

  /// 32 by 18.
  md,
}

class Switch extends StatelessWidget {
  const Switch({
    super.key,
    required this.value,
    required this.onChanged,
    this.semanticLabel,
    this.size = SwitchSize.md,
    this.focusNode,
    this.autofocus = false,
  });

  /// Whether it is on.
  final bool value;

  /// Null disables the switch.
  final ValueChanged<bool>? onChanged;

  /// What a screen reader calls it when its visible text does not say, as for an icon
  /// alone.
  final String? semanticLabel;

  /// Medium, or small to sit in a dense row.
  final SwitchSize size;

  /// Its focus node, to move focus to it or follow it from outside; one is made when not
  /// given.
  final FocusNode? focusNode;

  /// Takes the keyboard's focus when it first appears.
  final bool autofocus;

  // Its own size, at the start, even where its parent stretches what it holds: a
  // stacked Field, a column that fills its width.
  @override
  Widget build(BuildContext context) => Align(
    alignment: AlignmentDirectional.centerStart,
    widthFactor: 1,
    heightFactor: 1,
    child: _control(context),
  );

  Widget _control(BuildContext context) {
    final t = AshlarTheme.of(context);
    final c = t.colors;
    return Pressable(
      onPressed: onChanged == null ? null : () => onChanged!(!value),
      toggled: value,
      semanticLabel: semanticLabel,
      focusNode: focusNode,
      autofocus: autofocus,
      builder: (context, state) => Opacity(
        opacity: state.enabled ? 1 : .5,
        child: AnimatedContainer(
          duration: t.duration,
          width: size == SwitchSize.sm ? 24 : 32,
          height: size == SwitchSize.sm ? 14 : 18,
          padding: const EdgeInsets.all(1),
          decoration: BoxDecoration(
            color: value ? c.primary : c.input,
            borderRadius: BorderRadius.circular(9),
            boxShadow: [if (state.focused) ...t.focusRing, ...t.shadowXs],
          ),
          child: AnimatedAlign(
            duration: t.duration,
            curve: Curves.easeOutCubic,
            alignment: value
                ? AlignmentDirectional.centerEnd
                : AlignmentDirectional.centerStart,
            child: Container(
              width: size == SwitchSize.sm ? 12 : 16,
              height: size == SwitchSize.sm ? 12 : 16,
              decoration: BoxDecoration(
                color: t.brightness == Brightness.dark && !value
                    ? c.foreground
                    : c.background,
                shape: BoxShape.circle,
              ),
            ),
          ),
        ),
      ),
    );
  }
}