← Back to Blog

Flutter ToggleButtons Localization: Multi-Option Selection for Multilingual Apps

fluttertogglebuttonsselectionmateriallocalizationrtl

Flutter ToggleButtons Localization: Multi-Option Selection for Multilingual Apps

ToggleButtons is a Flutter widget that displays a row of toggle buttons where one or more can be selected simultaneously. In multilingual applications, ToggleButtons is essential for providing translated option labels in a compact toggle group, handling variable-width buttons for different translation lengths, supporting RTL button order, and offering accessible selection announcements in the active language.

Understanding ToggleButtons in Localization Context

ToggleButtons renders a horizontal row of bordered buttons that can be toggled on and off individually or as a group. For multilingual apps, this enables:

  • Translated toggle labels in a compact horizontal layout
  • Adaptive button widths that accommodate translations of different lengths
  • RTL-aware button ordering that reverses automatically
  • Grouped selection states with localized feedback messages

Why ToggleButtons Matters for Multilingual Apps

ToggleButtons provides:

  • Compact selection: Multiple translated options in a single row
  • Flexible selection: Single-select or multi-select modes with localized labels
  • Visual grouping: Connected borders show related translated options
  • Accessibility: Each button's selected state is announced in the active language

Basic ToggleButtons Implementation

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

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

  @override
  State<LocalizedToggleButtonsExample> createState() =>
      _LocalizedToggleButtonsExampleState();
}

class _LocalizedToggleButtonsExampleState
    extends State<LocalizedToggleButtonsExample> {
  final List<bool> _isSelected = [true, false, false];

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Scaffold(
      appBar: AppBar(title: Text(l10n.viewOptionsTitle)),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              l10n.selectViewLabel,
              style: Theme.of(context).textTheme.titleMedium,
            ),
            const SizedBox(height: 16),
            ToggleButtons(
              isSelected: _isSelected,
              onPressed: (index) {
                setState(() {
                  for (int i = 0; i < _isSelected.length; i++) {
                    _isSelected[i] = i == index;
                  }
                });
              },
              children: [
                Padding(
                  padding: const EdgeInsets.symmetric(horizontal: 16),
                  child: Row(
                    children: [
                      const Icon(Icons.list, size: 18),
                      const SizedBox(width: 8),
                      Text(l10n.listViewLabel),
                    ],
                  ),
                ),
                Padding(
                  padding: const EdgeInsets.symmetric(horizontal: 16),
                  child: Row(
                    children: [
                      const Icon(Icons.grid_view, size: 18),
                      const SizedBox(width: 8),
                      Text(l10n.gridViewLabel),
                    ],
                  ),
                ),
                Padding(
                  padding: const EdgeInsets.symmetric(horizontal: 16),
                  child: Row(
                    children: [
                      const Icon(Icons.view_compact, size: 18),
                      const SizedBox(width: 8),
                      Text(l10n.compactViewLabel),
                    ],
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

Advanced ToggleButtons Patterns for Localization

Multi-Select Filters with Localized Labels

ToggleButtons configured for multi-select filtering with translated filter names.

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

  @override
  State<LocalizedFilterToggles> createState() =>
      _LocalizedFilterTogglesState();
}

class _LocalizedFilterTogglesState extends State<LocalizedFilterToggles> {
  final List<bool> _filters = [false, false, false, false];

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    final filterLabels = [
      l10n.filterNewLabel,
      l10n.filterPopularLabel,
      l10n.filterSaleLabel,
      l10n.filterFreeShippingLabel,
    ];

    final activeCount = _filters.where((f) => f).length;

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Padding(
          padding: const EdgeInsetsDirectional.fromSTEB(16, 16, 16, 8),
          child: Text(
            activeCount > 0
                ? l10n.activeFiltersLabel(activeCount)
                : l10n.noFiltersLabel,
            style: Theme.of(context).textTheme.titleSmall,
          ),
        ),
        SingleChildScrollView(
          scrollDirection: Axis.horizontal,
          padding: const EdgeInsetsDirectional.only(start: 16),
          child: ToggleButtons(
            isSelected: _filters,
            onPressed: (index) {
              setState(() => _filters[index] = !_filters[index]);
            },
            borderRadius: BorderRadius.circular(8),
            children: filterLabels.map((label) {
              return Padding(
                padding: const EdgeInsets.symmetric(horizontal: 12),
                child: Text(label),
              );
            }).toList(),
          ),
        ),
      ],
    );
  }
}

Text Formatting Toolbar

ToggleButtons for text formatting options with icon-only buttons and localized tooltips.

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

  @override
  State<LocalizedFormattingToolbar> createState() =>
      _LocalizedFormattingToolbarState();
}

class _LocalizedFormattingToolbarState
    extends State<LocalizedFormattingToolbar> {
  final List<bool> _formatting = [false, false, false];

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Column(
      children: [
        ToggleButtons(
          isSelected: _formatting,
          onPressed: (index) {
            setState(() => _formatting[index] = !_formatting[index]);
          },
          children: [
            Tooltip(
              message: l10n.boldTooltip,
              child: const Padding(
                padding: EdgeInsets.symmetric(horizontal: 12),
                child: Icon(Icons.format_bold),
              ),
            ),
            Tooltip(
              message: l10n.italicTooltip,
              child: const Padding(
                padding: EdgeInsets.symmetric(horizontal: 12),
                child: Icon(Icons.format_italic),
              ),
            ),
            Tooltip(
              message: l10n.underlineTooltip,
              child: const Padding(
                padding: EdgeInsets.symmetric(horizontal: 12),
                child: Icon(Icons.format_underlined),
              ),
            ),
          ],
        ),
        const SizedBox(height: 8),
        Text(
          l10n.formattingAppliedLabel(
            [
              if (_formatting[0]) l10n.boldLabel,
              if (_formatting[1]) l10n.italicLabel,
              if (_formatting[2]) l10n.underlineLabel,
            ].join(', '),
          ),
          style: Theme.of(context).textTheme.bodySmall,
        ),
      ],
    );
  }
}

Sort Direction Toggle

ToggleButtons for selecting sort direction with translated labels.

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

  @override
  State<LocalizedSortToggle> createState() => _LocalizedSortToggleState();
}

class _LocalizedSortToggleState extends State<LocalizedSortToggle> {
  final List<bool> _sortDirection = [true, false];
  final List<bool> _sortField = [true, false, false];

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            l10n.sortByLabel,
            style: Theme.of(context).textTheme.titleSmall,
          ),
          const SizedBox(height: 8),
          ToggleButtons(
            isSelected: _sortField,
            onPressed: (index) {
              setState(() {
                for (int i = 0; i < _sortField.length; i++) {
                  _sortField[i] = i == index;
                }
              });
            },
            borderRadius: BorderRadius.circular(8),
            children: [
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 16),
                child: Text(l10n.sortByNameLabel),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 16),
                child: Text(l10n.sortByDateLabel),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 16),
                child: Text(l10n.sortBySizeLabel),
              ),
            ],
          ),
          const SizedBox(height: 16),
          Text(
            l10n.directionLabel,
            style: Theme.of(context).textTheme.titleSmall,
          ),
          const SizedBox(height: 8),
          ToggleButtons(
            isSelected: _sortDirection,
            onPressed: (index) {
              setState(() {
                for (int i = 0; i < _sortDirection.length; i++) {
                  _sortDirection[i] = i == index;
                }
              });
            },
            borderRadius: BorderRadius.circular(8),
            children: [
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 16),
                child: Row(
                  children: [
                    const Icon(Icons.arrow_upward, size: 16),
                    const SizedBox(width: 4),
                    Text(l10n.ascendingLabel),
                  ],
                ),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 16),
                child: Row(
                  children: [
                    const Icon(Icons.arrow_downward, size: 16),
                    const SizedBox(width: 4),
                    Text(l10n.descendingLabel),
                  ],
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

RTL Support and Bidirectional Layouts

ToggleButtons automatically reverses button order in RTL layouts. Icons and text within buttons also align correctly based on the active directionality.

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

  @override
  State<BidirectionalToggleButtons> createState() =>
      _BidirectionalToggleButtonsState();
}

class _BidirectionalToggleButtonsState
    extends State<BidirectionalToggleButtons> {
  final List<bool> _isSelected = [true, false, false];

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;

    return Padding(
      padding: const EdgeInsetsDirectional.all(16),
      child: ToggleButtons(
        isSelected: _isSelected,
        onPressed: (index) {
          setState(() {
            for (int i = 0; i < _isSelected.length; i++) {
              _isSelected[i] = i == index;
            }
          });
        },
        borderRadius: BorderRadius.circular(8),
        children: [
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16),
            child: Text(l10n.smallLabel),
          ),
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16),
            child: Text(l10n.mediumLabel),
          ),
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16),
            child: Text(l10n.largeLabel),
          ),
        ],
      ),
    );
  }
}

Testing ToggleButtons Localization

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

void main() {
  Widget buildTestWidget({Locale locale = const Locale('en')}) {
    return MaterialApp(
      locale: locale,
      localizationsDelegates: AppLocalizations.localizationsDelegates,
      supportedLocales: AppLocalizations.supportedLocales,
      home: const LocalizedToggleButtonsExample(),
    );
  }

  testWidgets('ToggleButtons renders localized labels', (tester) async {
    await tester.pumpWidget(buildTestWidget());
    await tester.pumpAndSettle();
    expect(find.byType(ToggleButtons), findsOneWidget);
  });

  testWidgets('ToggleButtons works in RTL', (tester) async {
    await tester.pumpWidget(buildTestWidget(locale: const Locale('ar')));
    await tester.pumpAndSettle();
    expect(tester.takeException(), isNull);
  });
}

Best Practices

  1. Wrap ToggleButtons in SingleChildScrollView with horizontal scroll when translated labels might overflow the screen width.

  2. Use borderRadius for rounded toggle groups that look polished with translated labels of any length.

  3. Provide Tooltip for icon-only toggles with translated tooltip text so users understand the action without relying on icons alone.

  4. Show active filter count with a parameterized translated label like activeFiltersLabel(count) for multi-select toggles.

  5. Combine icons with translated text in toggle buttons for clarity -- icons are universally understood while text provides locale-specific confirmation.

  6. Test with verbose translations (German, Finnish) to verify buttons don't overflow and remain tappable at all sizes.

Conclusion

ToggleButtons provides a compact multi-option selection widget for Flutter apps. For multilingual apps, it handles translated labels within connected bordered buttons, supports single-select and multi-select modes, and automatically reverses button order in RTL layouts. By combining ToggleButtons with filter counts, formatting toolbars, and sort controls, you can build compact selection interfaces that communicate clearly in every supported language.

Further Reading