Fliin
On this page

Components / actions

Toolbar

Gathers the commands that act on the same content in one bar, floating or not, such as text formatting: the arrow keys move from one button to the next.

16 of 16 variants pass
Toolbar, plain state
Platform
Brightness
Text scale
Direction

Import

In a Flutter app where fliin init has run:

fliin add toolbarAlso installs: core, separator

Examples

Plain

A toolbar lines up toggles and actions, split by a separator; semanticLabel names it for the screen reader.

Toolbar, plain state
import 'package:your_app/ui/fliin/components/button/fl_button.dart';
import 'package:your_app/ui/fliin/components/separator/fl_separator.dart';
import 'package:your_app/ui/fliin/components/toggle_button/fl_toggle_button.dart';
import 'package:your_app/ui/fliin/components/toolbar/fl_toolbar.dart';
import 'package:your_app/ui/fliin/core/fliin.dart';
import 'package:your_app/ui/fliin/icons/fl_icons.dart';
import 'package:flutter/widgets.dart';

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

  @override
  State<ToolbarPlainExample> createState() => _ToolbarPlainExampleState();
}

class _ToolbarPlainExampleState extends State<ToolbarPlainExample> {
  var _favorite = true;
  var _bookmark = false;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: FlToolbar(
        semanticLabel: 'Actions',
        children: [
          FlToggleButton.icon(
            label: 'Favorite',
            selected: _favorite,
            variant: FlToggleButtonVariant.ghost,
            icon: FlIcon(_favorite ? FlIcons.heartFill : FlIcons.heart),
            onChanged: (on) => setState(() => _favorite = on),
          ),
          FlToggleButton.icon(
            label: 'Bookmark',
            selected: _bookmark,
            variant: FlToggleButtonVariant.ghost,
            icon: FlIcon(FlIcons.bookmark),
            onChanged: (on) => setState(() => _bookmark = on),
          ),
          const FlSeparator(),
          FlButton.icon(label: 'Edit', variant: FlButtonVariant.ghost, icon: FlIcon(FlIcons.pencil), onPressed: () {}),
          FlButton.icon(
            label: 'Share',
            variant: FlButtonVariant.ghost,
            icon: FlIcon(FlIcons.arrowUpFromSquare),
            onPressed: () {},
          ),
        ],
      ),
    );
  }
}

Attached

The attached variant draws a surface behind the tools, to float them over content.

Toolbar, attached state
import 'package:your_app/ui/fliin/components/button/fl_button.dart';
import 'package:your_app/ui/fliin/components/separator/fl_separator.dart';
import 'package:your_app/ui/fliin/components/toggle_button/fl_toggle_button.dart';
import 'package:your_app/ui/fliin/components/toolbar/fl_toolbar.dart';
import 'package:your_app/ui/fliin/core/fliin.dart';
import 'package:your_app/ui/fliin/icons/fl_icons.dart';
import 'package:flutter/widgets.dart';

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

  @override
  State<ToolbarAttachedExample> createState() => _ToolbarAttachedExampleState();
}

class _ToolbarAttachedExampleState extends State<ToolbarAttachedExample> {
  var _favorite = true;
  var _bookmark = false;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: FlToolbar(
        variant: FlToolbarVariant.attached,
        semanticLabel: 'Actions',
        children: [
          FlToggleButton.icon(
            label: 'Favorite',
            selected: _favorite,
            variant: FlToggleButtonVariant.ghost,
            icon: FlIcon(_favorite ? FlIcons.heartFill : FlIcons.heart),
            onChanged: (on) => setState(() => _favorite = on),
          ),
          FlToggleButton.icon(
            label: 'Bookmark',
            selected: _bookmark,
            variant: FlToggleButtonVariant.ghost,
            icon: FlIcon(FlIcons.bookmark),
            onChanged: (on) => setState(() => _bookmark = on),
          ),
          const FlSeparator(),
          FlButton.icon(label: 'Edit', variant: FlButtonVariant.ghost, icon: FlIcon(FlIcons.pencil), onPressed: () {}),
          FlButton.icon(
            label: 'Share',
            variant: FlButtonVariant.ghost,
            icon: FlIcon(FlIcons.arrowUpFromSquare),
            onPressed: () {},
          ),
        ],
      ),
    );
  }
}

Vertical

axis: Axis.vertical stacks the tools, for a side rail.

Toolbar, vertical state
import 'package:your_app/ui/fliin/components/button/fl_button.dart';
import 'package:your_app/ui/fliin/components/separator/fl_separator.dart';
import 'package:your_app/ui/fliin/components/toggle_button/fl_toggle_button.dart';
import 'package:your_app/ui/fliin/components/toolbar/fl_toolbar.dart';
import 'package:your_app/ui/fliin/core/fliin.dart';
import 'package:your_app/ui/fliin/icons/fl_icons.dart';
import 'package:flutter/widgets.dart';

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

  @override
  State<ToolbarVerticalExample> createState() => _ToolbarVerticalExampleState();
}

class _ToolbarVerticalExampleState extends State<ToolbarVerticalExample> {
  var _favorite = true;
  var _bookmark = false;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: FlToolbar(
        axis: Axis.vertical,
        children: [
          FlToggleButton.icon(
            label: 'Favorite',
            selected: _favorite,
            variant: FlToggleButtonVariant.ghost,
            icon: FlIcon(_favorite ? FlIcons.heartFill : FlIcons.heart),
            onChanged: (on) => setState(() => _favorite = on),
          ),
          FlToggleButton.icon(
            label: 'Bookmark',
            selected: _bookmark,
            variant: FlToggleButtonVariant.ghost,
            icon: FlIcon(FlIcons.bookmark),
            onChanged: (on) => setState(() => _bookmark = on),
          ),
          const FlSeparator(),
          FlButton.icon(label: 'Edit', variant: FlButtonVariant.ghost, icon: FlIcon(FlIcons.pencil), onPressed: () {}),
          FlButton.icon(
            label: 'Share',
            variant: FlButtonVariant.ghost,
            icon: FlIcon(FlIcons.arrowUpFromSquare),
            onPressed: () {},
          ),
        ],
      ),
    );
  }
}

With button group

Button groups in a toolbar: a group of toggles whose selected items are on, a separator, then the actions. The arrow keys cross every button of the bar.

Toolbar, with-button-group state
import 'package:your_app/ui/fliin/components/button_group/fl_button_group.dart';
import 'package:your_app/ui/fliin/components/separator/fl_separator.dart';
import 'package:your_app/ui/fliin/components/toolbar/fl_toolbar.dart';
import 'package:your_app/ui/fliin/core/fliin.dart';
import 'package:your_app/ui/fliin/icons/fl_icons.dart';
import 'package:flutter/widgets.dart';

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

  @override
  State<ToolbarWithButtonGroupExample> createState() => _ToolbarWithButtonGroupExampleState();
}

class _ToolbarWithButtonGroupExampleState extends State<ToolbarWithButtonGroupExample> {
  final _on = {'Favorite'};

  @override
  Widget build(BuildContext context) {
    FlButtonGroupItem toggle(String label, FlIconData icon) => FlButtonGroupItem.icon(
      label: label,
      icon: FlIcon(icon),
      selected: _on.contains(label),
      onPressed: () => setState(() => _on.contains(label) ? _on.remove(label) : _on.add(label)),
    );
    return Center(
      child: FlToolbar(
        variant: FlToolbarVariant.attached,
        semanticLabel: 'Post actions',
        children: [
          FlButtonGroup(
            semanticLabel: 'Keep',
            items: [toggle('Favorite', FlIcons.heart), toggle('Bookmark', FlIcons.bookmark)],
          ),
          const FlSeparator(),
          FlButtonGroup(
            items: [
              FlButtonGroupItem.icon(label: 'Edit', icon: FlIcon(FlIcons.pencil), onPressed: () {}),
              FlButtonGroupItem.icon(label: 'Share', icon: FlIcon(FlIcons.arrowUpFromSquare), onPressed: () {}),
            ],
          ),
        ],
      ),
    );
  }
}

Custom styles

spacing packs the controls closer, and color paints the attached bar with another fill of your theme.

Toolbar, custom-styles state
import 'package:your_app/ui/fliin/components/toggle_button/fl_toggle_button.dart';
import 'package:your_app/ui/fliin/components/toolbar/fl_toolbar.dart';
import 'package:your_app/ui/fliin/core/fliin.dart';
import 'package:your_app/ui/fliin/icons/fl_icons.dart';
import 'package:flutter/widgets.dart';

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

  @override
  State<ToolbarCustomStylesExample> createState() => _ToolbarCustomStylesExampleState();
}

class _ToolbarCustomStylesExampleState extends State<ToolbarCustomStylesExample> {
  final _on = {'Favorite'};

  @override
  Widget build(BuildContext context) {
    final theme = FlScope.of(context);
    Widget toggle(String label, FlIconData icon, FlIconData selectedIcon) => FlToggleButton.icon(
      label: label,
      variant: FlToggleButtonVariant.ghost,
      selected: _on.contains(label),
      icon: FlIcon(icon),
      selectedIcon: FlIcon(selectedIcon),
      onChanged: (on) => setState(() => on ? _on.add(label) : _on.remove(label)),
    );
    return Center(
      child: FlToolbar(
        variant: FlToolbarVariant.attached,
        spacing: theme.space.xs,
        color: theme.colors.surfaceSecondary,
        semanticLabel: 'Keep',
        children: [
          toggle('Favorite', FlIcons.heart, FlIcons.heartFill),
          toggle('Star', FlIcons.star, FlIcons.starFill),
          toggle('Bookmark', FlIcons.bookmark, FlIcons.bookmark),
        ],
      ),
    );
  }
}

Usage

The code the reference images render:

final example = ColoredBox(
  color: FlScope.of(context).colors.surface,
  child: SingleChildScrollView(
    padding: EdgeInsetsDirectional.all(FlScope.of(context).space.lg),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      spacing: FlScope.of(context).space.sm,
      children: [
        Align(
          alignment: AlignmentDirectional.centerStart,
          child: FlToolbar(
            semanticLabel: 'Actions',
            children: [
              FlToggleButton.icon(
                label: 'Favorite',
                selected: true,
                variant: FlToggleButtonVariant.ghost,
                icon: FlIcon(FlIcons.heartFill),
                onChanged: (_) {},
              ),
              FlToggleButton.icon(
                label: 'Save',
                selected: false,
                variant: FlToggleButtonVariant.ghost,
                icon: FlIcon(FlIcons.bookmark),
                onChanged: (_) {},
              ),
              const FlSeparator(),
              FlButton.icon(
                label: 'Edit',
                variant: FlButtonVariant.ghost,
                icon: FlIcon(FlIcons.pencil),
                onPressed: () {},
              ),
              FlButton.icon(
                label: 'Share',
                variant: FlButtonVariant.ghost,
                icon: FlIcon(FlIcons.arrowUpFromSquare),
                onPressed: () {},
              ),
            ],
          ),
        ),
        Padding(
          padding: EdgeInsetsDirectional.symmetric(
            vertical: FlScope.of(context).space.md,
          ),
          child: Center(
            child: FlToolbar(
              variant: FlToolbarVariant.attached,
              children: [
                FlButtonGroup(
                  items: [
                    FlButtonGroupItem.icon(
                      label: 'Edit',
                      icon: FlIcon(FlIcons.pencil),
                      onPressed: () {},
                    ),
                    FlButtonGroupItem.icon(
                      label: 'Share',
                      icon: FlIcon(FlIcons.arrowUpFromSquare),
                      onPressed: () {},
                    ),
                  ],
                ),
                const FlSeparator(),
                FlToggleButton.icon(
                  label: 'Favorite',
                  selected: true,
                  icon: FlIcon(FlIcons.starFill),
                  onChanged: (_) {},
                ),
                FlButton.icon(
                  label: 'Delete',
                  variant: FlButtonVariant.tertiary,
                  icon: FlIcon(FlIcons.trashBin),
                  onPressed: () {},
                ),
              ],
            ),
          ),
        ),
        Align(
          alignment: AlignmentDirectional.centerStart,
          child: FlToolbar(
            axis: Axis.vertical,
            children: [
              FlToggleButton.icon(
                label: 'Favorite',
                selected: false,
                variant: FlToggleButtonVariant.ghost,
                icon: FlIcon(FlIcons.heart),
                onChanged: (_) {},
              ),
              FlToggleButton.icon(
                label: 'Save',
                selected: false,
                variant: FlToggleButtonVariant.ghost,
                icon: FlIcon(FlIcons.bookmark),
                onChanged: (_) {},
              ),
              const FlSeparator(),
              FlButton.icon(
                label: 'Edit',
                variant: FlButtonVariant.ghost,
                icon: FlIcon(FlIcons.pencil),
                onPressed: () {},
              ),
              FlButton.icon(
                label: 'Share',
                variant: FlButtonVariant.ghost,
                icon: FlIcon(FlIcons.arrowUpFromSquare),
                onPressed: () {},
              ),
            ],
          ),
        ),
      ],
    ),
  ),
);

API

FlToolbarVariant: enum {plain, attached}
FlToolbarVariant.plain: static final FlToolbarVariant
FlToolbarVariant.attached: static final FlToolbarVariant
FlToolbar: class extends StatefulWidget
FlToolbar.new: const new({super.key, required List<Widget> children, Axis axis, FlToolbarVariant variant, String? semanticLabel, double? spacing, Color? color})
FlToolbar.children: get List<Widget>
FlToolbar.axis: get Axis
FlToolbar.variant: get FlToolbarVariant
FlToolbar.semanticLabel: get String?
FlToolbar.spacing: get double?
FlToolbar.color: get Color?
FlToolbar.createState: State<FlToolbar>()

Reference renders

The 16 images published with version 0.1.1, rendered by the shared harness on macOS with Flutter 3.47.7. fliin verify toolbar compares your copy to them.

iOS · Light
Toolbar, iOS, Light, text 1.0×, Left to right
Text 1.0× · Left to right
Toolbar, iOS, Light, text 1.0×, Right to left
Text 1.0× · Right to left
Toolbar, iOS, Light, text 2.0×, Left to right
Text 2.0× · Left to right
Toolbar, iOS, Light, text 2.0×, Right to left
Text 2.0× · Right to left
iOS · Dark
Toolbar, iOS, Dark, text 1.0×, Left to right
Text 1.0× · Left to right
Toolbar, iOS, Dark, text 1.0×, Right to left
Text 1.0× · Right to left
Toolbar, iOS, Dark, text 2.0×, Left to right
Text 2.0× · Left to right
Toolbar, iOS, Dark, text 2.0×, Right to left
Text 2.0× · Right to left
Android · Light
Toolbar, Android, Light, text 1.0×, Left to right
Text 1.0× · Left to right
Toolbar, Android, Light, text 1.0×, Right to left
Text 1.0× · Right to left
Toolbar, Android, Light, text 2.0×, Left to right
Text 2.0× · Left to right
Toolbar, Android, Light, text 2.0×, Right to left
Text 2.0× · Right to left
Android · Dark
Toolbar, Android, Dark, text 1.0×, Left to right
Text 1.0× · Left to right
Toolbar, Android, Dark, text 1.0×, Right to left
Text 1.0× · Right to left
Toolbar, Android, Dark, text 2.0×, Left to right
Text 2.0× · Left to right
Toolbar, Android, Dark, text 2.0×, Right to left
Text 2.0× · Right to left

Use it to

  • add a formatting toolbar above a text editor
  • keep edit, share and delete together above a photo
  • make a toolbar float over a document
  • move between toolbar buttons with the arrow keys
  • split two groups of tools with a divider
Not for
  • the main navigation between screens: use the tab bar (tab-bar)
  • a screen's title and actions: use the app bar (app-bar)
  • attached actions without a bar: use the button group (button-group)
States
  • Plain
  • Attached
  • Vertical

Source

The files fliin add toolbar copies into your app, before your prefix and imports are applied.

toolbar/fl_toolbar.dart

// Modified from the Toolbar of HeroUI React v3, Copyright 2025 NextUI Inc.,
// licensed under Apache 2.0: https://www.apache.org/licenses/LICENSE-2.0
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:fliin_ui/components/separator/fl_separator.dart';
import 'package:fliin_ui/core/fliin.dart';

/// The background of an [FlToolbar].
enum FlToolbarVariant {
  /// No background: the controls sit on the page.
  plain,

  /// A `surfaceOverlay` bar lifted by the `overlay` shadow, inset by
  /// `space.xs`: a floating bar over content, an
  /// editor's formatting bar.
  attached,
}

class _MoveIntent extends Intent {
  const _MoveIntent(this.step, {this.edge = false});

  /// +1 toward the end, -1 toward the start; with [edge], all the way.
  final int step;
  final bool edge;
}

/// Tab order inside the bar: only the control last reached, so that the bar
/// is one stop, entered again where the user left it (a roving tab stop).
class _RovingPolicy extends ReadingOrderTraversalPolicy {
  _RovingPolicy(this._state);

  final _FlToolbarState _state;

  @override
  Iterable<FocusNode> sortDescendants(Iterable<FocusNode> descendants, FocusNode currentNode) {
    final sorted = super.sortDescendants(descendants, currentNode).toList();
    if (sorted.isEmpty) return sorted;
    final focused = sorted.where((node) => node.hasPrimaryFocus).firstOrNull;
    final kept = focused ?? (sorted.contains(_state._last) ? _state._last! : sorted.first);
    _state._last = kept;
    return [kept];
  }
}

/// A row of controls that act on the same thing:
/// buttons, toggle buttons, button groups, `space.sm` apart; an
/// `FlSeparator` among [children] becomes a line across the bar, at half
/// a control's height.
///
/// With a keyboard, the bar is a single Tab stop, as a toolbar should be:
/// the arrow keys move the focus from control to control in reading order
/// (reversed in RTL), Home and End to the first and the last, Tab leaves
/// the bar, and coming back lands on the control left last. Touch reaches
/// every control directly.
///
/// [spacing] tightens or loosens the controls, and [color] repaints the
/// bar of the `attached` variant.
///
/// The screen reader reads [semanticLabel], then each control.
///
/// ```dart
/// FlToolbar(semanticLabel: 'Formatting', children: [
///   FlButtonGroup(items: [/* bold, italic, underline */]),
///   const FlSeparator(),
///   FlButton.icon(label: 'Share', icon: FlIcon(FlIcons.arrowUpFromSquare), onPressed: share),
/// ])
/// ```
class FlToolbar extends StatefulWidget {
  /// A bar of [children].
  const FlToolbar({
    super.key,
    required this.children,
    this.axis = Axis.horizontal,
    this.variant = FlToolbarVariant.plain,
    this.semanticLabel,
    this.spacing,
    this.color,
  });

  /// The controls, in reading order.
  final List<Widget> children;

  /// A row, or a column aligned at the start.
  final Axis axis;

  /// No background, or a floating bar; `plain` by default.
  final FlToolbarVariant variant;

  /// The bar's name for the screen reader ("Formatting").
  final String? semanticLabel;

  /// The space between two controls; `space.sm` by default.
  final double? spacing;

  /// The fill of an `attached` bar, in place of `surfaceOverlay`.
  final Color? color;

  @override
  State<FlToolbar> createState() => _FlToolbarState();
}

class _FlToolbarState extends State<FlToolbar> {
  final FocusNode _scope = FocusNode(debugLabel: 'FlToolbar', skipTraversal: true, canRequestFocus: false);
  late final _RovingPolicy _policy = _RovingPolicy(this);

  /// The control the bar's Tab stop lands on.
  FocusNode? _last;

  @override
  void dispose() {
    _scope.dispose();
    super.dispose();
  }

  /// The controls that take focus, in the order of the tree.
  List<FocusNode> get _controls => _scope.traversalDescendants.toList();

  void _move(_MoveIntent intent) {
    final controls = _controls;
    if (controls.isEmpty) return;
    final at = controls.indexOf(FocusManager.instance.primaryFocus ?? _scope);
    final int next;
    if (intent.edge) {
      next = intent.step > 0 ? controls.length - 1 : 0;
    } else if (at < 0) {
      next = 0;
    } else {
      next = (at + intent.step).clamp(0, controls.length - 1);
    }
    _last = controls[next];
    controls[next].requestFocus();
  }

  @override
  Widget build(BuildContext context) {
    final theme = FlScope.of(context);
    final horizontal = widget.axis == Axis.horizontal;
    final rtl = Directionality.of(context) == TextDirection.rtl;
    final forward = rtl ? -1 : 1;
    final half = theme.controlHeight / 2;
    final children = [
      for (final child in widget.children)
        if (child is FlSeparator)
          // A square of half a control: the line across it, centered.
          SizedBox(
            width: horizontal ? null : half,
            height: horizontal ? half : null,
            child: FlSeparator(
              axis: horizontal ? Axis.vertical : Axis.horizontal,
              variant: child.variant,
              thickness: child.thickness,
            ),
          )
        else
          child,
    ];
    final gap = widget.spacing ?? theme.space.sm;
    Widget bar = horizontal
        ? Row(mainAxisSize: MainAxisSize.min, spacing: gap, children: children)
        : Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            spacing: gap,
            children: [
              for (final (i, child) in children.indexed)
                widget.children[i] is FlSeparator
                    ? SizedBox(
                        width: theme.controlHeight,
                        child: Center(child: child),
                      )
                    : child,
            ],
          );
    if (widget.variant == FlToolbarVariant.attached) {
      final inset = theme.space.xs;
      final shape = switch (theme.shape.button) {
        FlButtonShape.square => const RoundedRectangleBorder(),
        // Concentric with the controls inside.
        FlButtonShape.rounded => RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(theme.shape.radiusButton + inset),
        ),
        FlButtonShape.pill => const StadiumBorder(),
      };
      bar = FlShadow(
        layers: theme.shadows.overlay,
        shape: shape,
        child: DecoratedBox(
          decoration: ShapeDecoration(color: widget.color ?? theme.colors.surfaceOverlay, shape: shape),
          child: Padding(padding: EdgeInsetsDirectional.all(inset), child: bar),
        ),
      );
    }
    final next = horizontal ? LogicalKeyboardKey.arrowRight : LogicalKeyboardKey.arrowDown;
    final previous = horizontal ? LogicalKeyboardKey.arrowLeft : LogicalKeyboardKey.arrowUp;
    return Shortcuts(
      shortcuts: {
        SingleActivator(next): _MoveIntent(horizontal ? forward : 1),
        SingleActivator(previous): _MoveIntent(horizontal ? -forward : -1),
        const SingleActivator(LogicalKeyboardKey.home): const _MoveIntent(-1, edge: true),
        const SingleActivator(LogicalKeyboardKey.end): const _MoveIntent(1, edge: true),
      },
      child: Actions(
        actions: {_MoveIntent: CallbackAction<_MoveIntent>(onInvoke: _move)},
        child: FocusTraversalGroup(
          policy: _policy,
          child: Focus(
            focusNode: _scope,
            child: Semantics(container: true, label: widget.semanticLabel, explicitChildNodes: true, child: bar),
          ),
        ),
      ),
    );
  }
}

Arrows to move, Enter to open, Escape to close.