ashlarui

Toggle

A button that stays pressed: bold, italic, a filter.

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

import 'ui/toggle.dart';

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

  @override
  State<ToggleDemo> createState() => _ToggleDemoState();
}

class _ToggleDemoState extends State<ToggleDemo> {
  bool bold = true, starred = false;

  @override
  Widget build(BuildContext context) => Row(
    mainAxisSize: MainAxisSize.min,
    children: [
      Toggle(
        pressed: bold,
        onChanged: (v) => setState(() => bold = v),
        semanticLabel: 'Bold',
        child: const AshlarIcon(AshlarIconsRoundedStroke.bold),
      ),
      const SizedBox(width: 8),
      Toggle(
        pressed: starred,
        onChanged: (v) => setState(() => starred = v),
        variant: ToggleVariant.outline,
        child: const Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            AshlarIcon(AshlarIconsRoundedStroke.star),
            SizedBox(width: 6),
            Text('Starred'),
          ],
        ),
      ),
    ],
  );
}

Try its options

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

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

Add it

In your Flutter project:

$ ashlar add toggle

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

Examples

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

Outlined

A border all the time.

Outlined, as it looks in an app Outlined, as it looks in an app, dark
// Outlined: A border all the time.
import 'package:flutter/widgets.dart';

import 'ui/toggle.dart';

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

  @override
  State<ToggleOutlineExample> createState() => _ToggleOutlineExampleState();
}

class _ToggleOutlineExampleState extends State<ToggleOutlineExample> {
  var on = false;

  @override
  Widget build(BuildContext context) => Toggle(
    variant: ToggleVariant.outline,
    pressed: on,
    onChanged: (v) => setState(() => on = v),
    child: const Text('Show hidden files'),
  );
}

Sizes

Small, medium and large.

Sizes, as it looks in an app Sizes, as it looks in an app, dark
// Sizes: Small, medium and large.
import 'package:ashlar_icons/ashlar_icons.dart';
import 'package:flutter/widgets.dart';

import 'ui/toggle.dart';

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

  @override
  State<ToggleSizesExample> createState() => _ToggleSizesExampleState();
}

class _ToggleSizesExampleState extends State<ToggleSizesExample> {
  final on = <ToggleSize>{ToggleSize.md};

  @override
  Widget build(BuildContext context) => Row(
    mainAxisSize: MainAxisSize.min,
    children: [
      for (final size in ToggleSize.values) ...[
        Toggle(
          size: size,
          pressed: on.contains(size),
          semanticLabel: 'Bold',
          onChanged: (v) => setState(() => v ? on.add(size) : on.remove(size)),
          child: const AshlarIcon(AshlarIconsRoundedStroke.bold),
        ),
        const SizedBox(width: 8),
      ],
    ],
  );
}

Disabled

Null onChanged.

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

import 'ui/toggle.dart';

class ToggleDisabledExample extends StatelessWidget {
  const ToggleDisabledExample({super.key});

  @override
  Widget build(BuildContext context) => const Toggle(pressed: true, onChanged: null, child: Text('Locked'));
}

API

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

ToggleVariantenum

ValueWhat it is for
plainNo border until it is on.
outlineA border all the time.

ToggleSizeenum

ValueWhat it is for
sm
md
lg

Toggleclass

A button that stays pressed: bold, italic, a filter. Give an icon-only toggle a semanticLabel.

Parameter Type Default What it is for
pressedrequired bool Whether it is on.
onChangedrequired ValueChanged<bool>? Null disables it.
childrequired Widget Its label or icon.
variant ToggleVariant ToggleVariant.plain Plain, or with a border all the time.
size ToggleSize ToggleSize.md Small, medium or large.
semanticLabel String? What a screen reader calls it, for a toggle that shows an icon alone.

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/toggle.dart 114 lines
// Ashlar UI: Toggle. Copied into your app by `ashlar add toggle`; it is yours to change.

import 'package:flutter/widgets.dart';

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

enum ToggleVariant {
  /// No border until it is on.
  plain,

  /// A border all the time.
  outline,
}

enum ToggleSize { sm, md, lg }

/// A button that stays pressed: bold, italic, a filter. Give an icon-only toggle a
/// [semanticLabel].
///
/// ```dart
/// Toggle(pressed: bold, onChanged: (v) => setState(() => bold = v), semanticLabel: 'Bold', child: AshlarIcon(AshlarIconsRoundedStroke.bold))
/// ```
class Toggle extends StatelessWidget {
  const Toggle({
    super.key,
    required this.pressed,
    required this.onChanged,
    required this.child,
    this.variant = ToggleVariant.plain,
    this.size = ToggleSize.md,
    this.semanticLabel,
  });

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

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

  /// Its label or icon.
  final Widget child;

  /// Plain, or with a border all the time.
  final ToggleVariant variant;

  /// Small, medium or large.
  final ToggleSize size;

  /// What a screen reader calls it, for a toggle that shows an icon alone.
  final String? semanticLabel;

  @override
  Widget build(BuildContext context) {
    final t = AshlarTheme.of(context);
    final c = t.colors;
    final height = t.control(switch (size) {
      ToggleSize.sm => 32.0,
      ToggleSize.md => 36.0,
      ToggleSize.lg => 40.0,
    });
    return Pressable(
      onPressed: onChanged == null ? null : () => onChanged!(!pressed),
      toggled: pressed,
      semanticLabel: semanticLabel,
      builder: (context, state) {
        final fg = pressed
            ? c.accentForeground
            : (state.hovered ? c.mutedForeground : c.foreground);
        return Opacity(
          opacity: state.enabled ? 1 : .5,
          child: AnimatedContainer(
            duration: t.duration,
            // Taller with larger text rather than cutting it off, and never stretched.
            height: grownHeight(context, height),
            constraints: BoxConstraints(minWidth: height),
            padding: EdgeInsets.symmetric(
              horizontal: size == ToggleSize.sm ? 6 : 8,
            ),
            decoration: BoxDecoration(
              color: pressed
                  ? c.accent
                  : (state.hovered ? c.muted : c.muted.withValues(alpha: 0)),
              borderRadius: joinedRadius(context, t.radiusMd),
              border: variant == ToggleVariant.outline
                  ? joinedBorder(context, state.focused ? c.ring : c.input)
                  : null,
              boxShadow: [
                if (state.focused) ...t.focusRing,
                if (variant == ToggleVariant.outline) ...t.shadowXs,
              ],
            ),
            // Centred, and as wide as its label unless a group stretches it.
            child: Align(
              widthFactor: 1,
              child: DefaultTextStyle.merge(
                style: t.text.sm.copyWith(
                  color: fg,
                  fontWeight: FontWeight.w500,
                ),
                maxLines: 1,
                child: IconTheme.merge(
                  data: IconThemeData(color: fg, size: 16),
                  child: child,
                ),
              ),
            ),
          ),
        );
      },
    );
  }
}