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
Wrap ToggleButtons in
SingleChildScrollViewwith horizontal scroll when translated labels might overflow the screen width.Use
borderRadiusfor rounded toggle groups that look polished with translated labels of any length.Provide
Tooltipfor icon-only toggles with translated tooltip text so users understand the action without relying on icons alone.Show active filter count with a parameterized translated label like
activeFiltersLabel(count)for multi-select toggles.Combine icons with translated text in toggle buttons for clarity -- icons are universally understood while text provides locale-specific confirmation.
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.