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
Never use fixed-width constraints on TextButton. Use
minimumSizeinstead offixedSizeso buttons grow to accommodate longer translations.Use
OverflowBarfor action button groups. It automatically stacks buttons vertically if horizontal space runs out.Prefer
TextButton.iconfor icon-text combinations. This constructor handles RTL icon mirroring automatically.Apply locale-aware padding and font sizing. Some scripts benefit from adjusted spacing.
Always provide semantic labels for accessibility. Use
Semanticsfor screen readers in the active locale.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.