← Back to Blog

Flutter CheckboxListTile Localization: Multi-Select Options for Multilingual Apps

fluttercheckboxlisttilecheckboxmateriallocalizationrtl

Flutter CheckboxListTile Localization: Multi-Select Options for Multilingual Apps

CheckboxListTile is a Flutter Material Design widget that combines a ListTile with a Checkbox, creating a tappable row for selecting or deselecting individual items. In multilingual applications, CheckboxListTile must handle translated option labels of varying lengths, support tristate checkboxes with localized state descriptions, adapt checkbox positioning for RTL layouts, and provide clear accessibility announcements in every supported language.

Understanding CheckboxListTile in Localization Context

CheckboxListTile renders a list tile with a leading or trailing checkbox, commonly used for multi-select preferences, consent forms, and filter lists. For multilingual apps, this enables:

  • Translated option labels that wrap naturally when longer than the source language
  • Automatic RTL layout reversal where checkboxes reposition correctly
  • Tristate checkboxes with localized descriptions for checked, unchecked, and indeterminate states
  • Accessible state announcements in the active language for screen readers

Why CheckboxListTile Matters for Multilingual Apps

CheckboxListTile provides:

  • Multi-select patterns: Users select multiple translated options from lists
  • RTL positioning: Checkbox control automatically moves to the correct side
  • Consent forms: Localized agreement checkboxes for terms, privacy, and marketing
  • Filter interfaces: Translated filter categories with select-all and clear-all support

Basic CheckboxListTile Implementation

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

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

  @override
  State<LocalizedCheckboxListTileExample> createState() =>
      _LocalizedCheckboxListTileExampleState();
}

class _LocalizedCheckboxListTileExampleState
    extends State<LocalizedCheckboxListTileExample> {
  bool _termsAccepted = false;
  bool _privacyAccepted = false;
  bool _marketingOptIn = false;

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

    return Scaffold(
      appBar: AppBar(title: Text(l10n.consentTitle)),
      body: ListView(
        children: [
          const SizedBox(height: 16),
          Padding(
            padding: const EdgeInsetsDirectional.symmetric(horizontal: 16),
            child: Text(
              l10n.consentHeading,
              style: Theme.of(context).textTheme.titleLarge,
            ),
          ),
          const SizedBox(height: 16),
          CheckboxListTile(
            title: Text(l10n.acceptTermsLabel),
            subtitle: Text(l10n.acceptTermsDescription),
            value: _termsAccepted,
            onChanged: (v) => setState(() => _termsAccepted = v ?? false),
          ),
          CheckboxListTile(
            title: Text(l10n.acceptPrivacyLabel),
            subtitle: Text(l10n.acceptPrivacyDescription),
            value: _privacyAccepted,
            onChanged: (v) => setState(() => _privacyAccepted = v ?? false),
          ),
          CheckboxListTile(
            title: Text(l10n.marketingOptInLabel),
            subtitle: Text(l10n.marketingOptInDescription),
            value: _marketingOptIn,
            onChanged: (v) => setState(() => _marketingOptIn = v ?? false),
          ),
          const SizedBox(height: 24),
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16),
            child: FilledButton(
              onPressed: _termsAccepted && _privacyAccepted ? () {} : null,
              child: Text(l10n.continueButton),
            ),
          ),
        ],
      ),
    );
  }
}

Advanced CheckboxListTile Patterns for Localization

Multi-Select Filter with Select All

Filter interfaces with translated category names need select-all and clear-all functionality with localized labels.

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

  @override
  State<LocalizedFilterCheckboxes> createState() =>
      _LocalizedFilterCheckboxesState();
}

class _LocalizedFilterCheckboxesState extends State<LocalizedFilterCheckboxes> {
  final Map<String, bool> _filters = {};

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    final l10n = AppLocalizations.of(context)!;
    if (_filters.isEmpty) {
      _filters.addAll({
        l10n.categoryElectronics: true,
        l10n.categoryClothing: true,
        l10n.categoryBooks: false,
        l10n.categoryHome: false,
        l10n.categorySports: true,
      });
    }
  }

  bool get _allSelected => _filters.values.every((v) => v);
  bool get _noneSelected => _filters.values.every((v) => !v);

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

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Padding(
          padding: const EdgeInsetsDirectional.fromSTEB(16, 16, 16, 8),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Text(
                l10n.filterByCategoryLabel,
                style: Theme.of(context).textTheme.titleMedium,
              ),
              TextButton(
                onPressed: () {
                  setState(() {
                    final newValue = !_allSelected;
                    for (final key in _filters.keys) {
                      _filters[key] = newValue;
                    }
                  });
                },
                child: Text(
                  _allSelected
                      ? l10n.clearAllButton
                      : l10n.selectAllButton,
                ),
              ),
            ],
          ),
        ),
        CheckboxListTile(
          title: Text(l10n.allCategoriesLabel),
          value: _allSelected
              ? true
              : _noneSelected
                  ? false
                  : null,
          tristate: true,
          onChanged: (v) {
            setState(() {
              final newValue = v ?? false;
              for (final key in _filters.keys) {
                _filters[key] = newValue;
              }
            });
          },
        ),
        const Divider(indent: 16, endIndent: 16),
        ..._filters.entries.map((entry) {
          return CheckboxListTile(
            title: Text(entry.key),
            value: entry.value,
            onChanged: (v) =>
                setState(() => _filters[entry.key] = v ?? false),
            contentPadding:
                const EdgeInsetsDirectional.only(start: 32, end: 16),
          );
        }),
        Padding(
          padding: const EdgeInsets.all(16),
          child: Text(
            l10n.selectedCountLabel(
              _filters.values.where((v) => v).length,
            ),
            style: Theme.of(context).textTheme.bodySmall,
          ),
        ),
      ],
    );
  }
}

Checklist with Completion Tracking

Task checklists and onboarding steps use checkboxes with localized progress indicators.

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

  @override
  State<LocalizedChecklist> createState() => _LocalizedChecklistState();
}

class _LocalizedChecklistState extends State<LocalizedChecklist> {
  final Map<String, bool> _tasks = {};

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    final l10n = AppLocalizations.of(context)!;
    if (_tasks.isEmpty) {
      _tasks.addAll({
        l10n.setupProfileTask: true,
        l10n.verifyEmailTask: true,
        l10n.addPaymentTask: false,
        l10n.inviteTeamTask: false,
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    final l10n = AppLocalizations.of(context)!;
    final completedCount = _tasks.values.where((v) => v).length;

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                l10n.setupProgressTitle,
                style: Theme.of(context).textTheme.titleLarge,
              ),
              const SizedBox(height: 8),
              LinearProgressIndicator(
                value: completedCount / _tasks.length,
              ),
              const SizedBox(height: 4),
              Text(
                l10n.progressLabel(completedCount, _tasks.length),
                style: Theme.of(context).textTheme.bodySmall,
              ),
            ],
          ),
        ),
        ..._tasks.entries.map((entry) {
          return CheckboxListTile(
            title: Text(
              entry.key,
              style: entry.value
                  ? const TextStyle(decoration: TextDecoration.lineThrough)
                  : null,
            ),
            value: entry.value,
            onChanged: (v) =>
                setState(() => _tasks[entry.key] = v ?? false),
          );
        }),
      ],
    );
  }
}

RTL Support and Bidirectional Layouts

CheckboxListTile automatically reverses its layout in RTL locales. The checkbox moves to the start side (right in RTL), and text alignment adapts accordingly.

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

  @override
  State<BidirectionalCheckboxList> createState() =>
      _BidirectionalCheckboxListState();
}

class _BidirectionalCheckboxListState extends State<BidirectionalCheckboxList> {
  bool _option1 = true;
  bool _option2 = false;

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

    return Padding(
      padding: const EdgeInsetsDirectional.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            l10n.preferencesHeading,
            style: Theme.of(context).textTheme.titleMedium,
          ),
          CheckboxListTile(
            title: Text(l10n.showTipsLabel),
            subtitle: Text(l10n.showTipsDescription),
            value: _option1,
            onChanged: (v) => setState(() => _option1 = v ?? false),
          ),
          CheckboxListTile(
            title: Text(l10n.compactViewLabel),
            subtitle: Text(l10n.compactViewDescription),
            value: _option2,
            onChanged: (v) => setState(() => _option2 = v ?? false),
          ),
        ],
      ),
    );
  }
}

Testing CheckboxListTile 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 LocalizedCheckboxListTileExample(),
    );
  }

  testWidgets('CheckboxListTile displays translated labels', (tester) async {
    await tester.pumpWidget(buildTestWidget());
    await tester.pumpAndSettle();
    expect(find.byType(CheckboxListTile), findsWidgets);
  });

  testWidgets('Checkbox toggles on tap', (tester) async {
    await tester.pumpWidget(buildTestWidget());
    await tester.pumpAndSettle();
    await tester.tap(find.byType(CheckboxListTile).first);
    await tester.pumpAndSettle();
  });

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

Best Practices

  1. Use tristate checkboxes with localized state descriptions for select-all headers that show checked, unchecked, and indeterminate states.

  2. Provide subtitles for important checkboxes like consent forms, using translated explanations of what each option means.

  3. Show localized count labels like "3 of 5 selected" using parameterized ARB translations with pluralization support.

  4. Use contentPadding to indent sub-items under a select-all checkbox, creating visual hierarchy that works in both LTR and RTL.

  5. Disable the continue button until required checkboxes are checked, with localized messaging explaining what's needed.

  6. Test with verbose translations to verify that long labels wrap correctly without overlapping the checkbox control.

Conclusion

CheckboxListTile is the standard widget for multi-select options, consent forms, and filter interfaces in Flutter. For multilingual apps, it provides automatic RTL layout reversal, tristate support for select-all patterns, and flexible text wrapping for translated labels. By implementing localized filter groups with select-all, consent checkboxes with translated descriptions, and progress-tracked checklists, you can build checkbox interfaces that work clearly across all supported languages.

Further Reading