← Back to Blog

Flutter TextButton Localization: Building Accessible Multilingual Button Interfaces

fluttertextbuttonbuttonmateriallocalizationrtl

Flutter TextButton Localization: Building Accessible Multilingual Button Interfaces

TextButton is a Flutter Material Design widget that displays a simple text-based button without elevation. In multilingual applications, TextButton plays a critical role because button labels are among the most frequently translated UI elements. Since translated text can vary dramatically in length across languages, TextButton must gracefully handle flexible sizing, RTL text direction, and locale-specific styling.

Understanding TextButton in Localization Context

TextButton renders a tappable label with Material ink splash effects, typically used for less prominent actions like "Cancel" or "Learn More." For multilingual apps, this enables:

  • Flexible width that adapts to varying translation lengths
  • Automatic text direction handling for RTL languages
  • Semantic labeling for screen readers in every locale
  • Consistent Material Design interaction patterns globally

Why TextButton Matters for Multilingual Apps

TextButton provides:

  • Elastic sizing: Button width grows naturally to fit longer translations like German or Finnish
  • RTL awareness: Icon and text order reverses automatically in Arabic and Hebrew
  • Theme integration: Button styles can be customized per locale for cultural fit
  • Accessibility: Semantic labels and tooltips adapt to the active language

Basic TextButton Implementation

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

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

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

    return Scaffold(
      appBar: AppBar(title: Text(l10n.appTitle)),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            TextButton(
              onPressed: () {},
              child: Text(l10n.learnMoreButton),
            ),
            const SizedBox(height: 16),
            TextButton(
              onPressed: () {},
              child: Text(l10n.cancelButton),
            ),
            const SizedBox(height: 16),
            TextButton(
              onPressed: () {
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(content: Text(l10n.actionConfirmed)),
                );
              },
              child: Text(l10n.viewDetailsButton),
            ),
          ],
        ),
      ),
    );
  }
}

Advanced TextButton Patterns for Localization

Adaptive Button Sizing for Translated Text

Different languages produce dramatically different label lengths. Use Wrap instead of Row to ensure buttons flow to the next line when translations are too long.

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

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

    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16),
      child: Wrap(
        spacing: 8,
        runSpacing: 8,
        alignment: WrapAlignment.center,
        children: [
          TextButton(
            onPressed: () {},
            style: TextButton.styleFrom(
              minimumSize: const Size(80, 40),
              padding: const EdgeInsets.symmetric(horizontal: 16),
            ),
            child: Text(l10n.skipButton),
          ),
          TextButton(
            onPressed: () {},
            style: TextButton.styleFrom(
              minimumSize: const Size(80, 40),
              padding: const EdgeInsets.symmetric(horizontal: 16),
            ),
            child: Text(l10n.retryButton),
          ),
          TextButton(
            onPressed: () {},
            style: TextButton.styleFrom(
              minimumSize: const Size(80, 40),
              padding: const EdgeInsets.symmetric(horizontal: 16),
            ),
            child: Text(l10n.submitButton),
          ),
        ],
      ),
    );
  }
}

Icon + Text Buttons with RTL Support

When combining icons with text labels, TextButton.icon handles RTL mirroring automatically.

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

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

    return Column(
      children: [
        TextButton.icon(
          onPressed: () {},
          icon: const Icon(Icons.arrow_forward),
          label: Text(l10n.continueButton),
        ),
        const SizedBox(height: 12),
        TextButton.icon(
          onPressed: () {},
          icon: const Icon(Icons.download),
          label: Text(l10n.downloadButton),
        ),
        const SizedBox(height: 12),
        TextButton(
          onPressed: () {},
          child: Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              Text(l10n.readMoreButton),
              const SizedBox(width: 4),
              Icon(
                isRtl ? Icons.arrow_back : Icons.arrow_forward,
                size: 16,
              ),
            ],
          ),
        ),
      ],
    );
  }
}

Button Groups and Action Bars

Use OverflowBar to ensure buttons stack vertically when horizontal space is insufficient.

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

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

    return Card(
      margin: const EdgeInsets.all(16),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              l10n.confirmationTitle,
              style: Theme.of(context).textTheme.titleMedium,
            ),
            const SizedBox(height: 8),
            Text(l10n.confirmationMessage),
            const SizedBox(height: 16),
            OverflowBar(
              spacing: 8,
              overflowSpacing: 4,
              overflowAlignment: OverflowBarAlignment.end,
              children: [
                TextButton(
                  onPressed: () {},
                  child: Text(l10n.cancelButton),
                ),
                TextButton(
                  onPressed: () {},
                  child: Text(l10n.remindLaterButton),
                ),
                TextButton(
                  onPressed: () {},
                  style: TextButton.styleFrom(
                    foregroundColor: Theme.of(context).colorScheme.primary,
                  ),
                  child: Text(l10n.confirmButton),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

Loading and Disabled States with Localized Labels

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

  @override
  State<LocalizedStatefulTextButton> createState() =>
      _LocalizedStatefulTextButtonState();
}

class _LocalizedStatefulTextButtonState
    extends State<LocalizedStatefulTextButton> {
  bool _isLoading = false;

  Future<void> _handleSubmit() async {
    setState(() => _isLoading = true);
    await Future.delayed(const Duration(seconds: 2));
    if (mounted) setState(() => _isLoading = false);
  }

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

    return TextButton(
      onPressed: _isLoading ? null : _handleSubmit,
      child: _isLoading
          ? Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                const SizedBox(
                  width: 16,
                  height: 16,
                  child: CircularProgressIndicator(strokeWidth: 2),
                ),
                const SizedBox(width: 8),
                Text(l10n.submittingLabel),
              ],
            )
          : Text(l10n.submitButton),
    );
  }
}

RTL Support and Bidirectional Layouts

TextButton inherits its text direction from the ambient Directionality widget. Use EdgeInsetsDirectional and AlignmentDirectional for correct RTL handling.

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

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

    return Padding(
      padding: const EdgeInsetsDirectional.only(start: 16, end: 16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          TextButton(
            onPressed: () {},
            style: TextButton.styleFrom(
              alignment: AlignmentDirectional.centerStart,
              padding: const EdgeInsetsDirectional.only(start: 12, end: 12),
            ),
            child: Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                const Icon(Icons.navigate_next, size: 18),
                const SizedBox(width: 4),
                Text(l10n.nextStepButton),
              ],
            ),
          ),
          const Divider(height: 1),
          TextButton(
            onPressed: () {},
            style: TextButton.styleFrom(
              alignment: AlignmentDirectional.centerStart,
              padding: const EdgeInsetsDirectional.only(start: 12, end: 12),
            ),
            child: Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                const Icon(Icons.navigate_before, size: 18),
                const SizedBox(width: 4),
                Text(l10n.previousStepButton),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Testing TextButton 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 LocalizedTextButtonExample(),
    );
  }

  testWidgets('TextButton handles RTL locale correctly', (tester) async {
    await tester.pumpWidget(buildTestWidget(locale: const Locale('ar')));
    await tester.pumpAndSettle();

    final textDirection = Directionality.of(
      tester.element(find.byType(Scaffold)),
    );
    expect(textDirection, TextDirection.rtl);
  });

  testWidgets('TextButton is tappable with localized label', (tester) async {
    var tapped = false;
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: TextButton(
            onPressed: () => tapped = true,
            child: const Text('Submit'),
          ),
        ),
      ),
    );
    await tester.tap(find.text('Submit'));
    expect(tapped, isTrue);
  });
}

Best Practices

  1. Never use fixed-width constraints on TextButton. Use minimumSize instead of fixedSize so buttons grow to accommodate longer translations.

  2. Use OverflowBar for action button groups. It automatically stacks buttons vertically if horizontal space runs out.

  3. Prefer TextButton.icon for icon-text combinations. This constructor handles RTL icon mirroring automatically.

  4. Apply locale-aware padding and font sizing. Some scripts benefit from adjusted spacing.

  5. Always provide semantic labels for accessibility. Use Semantics for screen readers in the active locale.

  6. Test with pseudo-localization. Use artificially lengthened strings to catch overflow issues before real translations arrive.

Conclusion

TextButton is a foundational interaction widget in Flutter that demands careful attention in multilingual applications. By using adaptive layouts with Wrap and OverflowBar, leveraging directional-aware padding with EdgeInsetsDirectional, and thoroughly testing across locales, you can build TextButton interfaces that look and function correctly for every language your app supports.

Further Reading