Flutter FilledButton Localization: Primary Actions with Material 3 in Multilingual Apps
FilledButton is a Material 3 button widget in Flutter that displays a filled, prominent button for primary actions. As the recommended replacement for the older ElevatedButton in Material 3 design systems, FilledButton must handle localized labels of varying lengths, adapt icon placement for RTL languages, and integrate seamlessly with locale-aware theming.
Understanding FilledButton in Localization Context
FilledButton renders a filled container with a text label, representing the highest-emphasis action on a screen. For multilingual apps, this enables:
- Dynamic width that expands naturally for longer translated labels
- Automatic RTL text direction and icon mirroring
- Tonal variants that adapt to localized theme configurations
- Consistent visual prominence as the primary CTA across all locales
Why FilledButton Matters for Multilingual Apps
FilledButton provides:
- Material 3 design: Modern filled styling that works consistently across locales
- Flexible sizing: Intrinsic width accommodates translations of any length
- Tonal variant:
FilledButton.tonaloffers a lower-emphasis alternative for secondary actions - RTL support: Icon-label pairs automatically reverse in RTL contexts
Basic FilledButton Implementation
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedFilledButtonExample extends StatelessWidget {
const LocalizedFilledButtonExample({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: [
FilledButton(
onPressed: () {},
child: Text(l10n.continueButton),
),
const SizedBox(height: 16),
FilledButton.tonal(
onPressed: () {},
child: Text(l10n.saveAsDraftButton),
),
const SizedBox(height: 16),
FilledButton.icon(
onPressed: () {},
icon: const Icon(Icons.add),
label: Text(l10n.createNewButton),
),
],
),
),
);
}
}
Advanced FilledButton Patterns for Localization
Filled vs Tonal Variants for Action Hierarchy
Material 3 introduces FilledButton.tonal for medium-emphasis actions. In multilingual interfaces, pairing filled and tonal buttons creates a clear action hierarchy regardless of language.
class LocalizedActionHierarchy extends StatelessWidget {
const LocalizedActionHierarchy({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.subscriptionExpiredTitle,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text(
l10n.subscriptionExpiredMessage,
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 20),
OverflowBar(
spacing: 12,
overflowSpacing: 8,
overflowAlignment: OverflowBarAlignment.end,
children: [
FilledButton.tonal(
onPressed: () {},
child: Text(l10n.remindLaterButton),
),
FilledButton(
onPressed: () {},
child: Text(l10n.renewNowButton),
),
],
),
],
),
),
);
}
}
Full-Width CTA with Loading State
Onboarding flows and checkout screens often use full-width filled buttons. Loading states must show localized progress text.
class LocalizedFilledCta extends StatefulWidget {
const LocalizedFilledCta({super.key});
@override
State<LocalizedFilledCta> createState() => _LocalizedFilledCtaState();
}
class _LocalizedFilledCtaState extends State<LocalizedFilledCta> {
bool _isLoading = false;
Future<void> _handleAction() 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 Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
FilledButton(
onPressed: _isLoading ? null : _handleAction,
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: _isLoading
? Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
),
const SizedBox(width: 12),
Text(l10n.processingLabel),
],
)
: Text(l10n.checkoutButton),
),
const SizedBox(height: 12),
FilledButton.tonal(
onPressed: () {},
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(l10n.continueBrowsingButton),
),
],
),
);
}
}
Icon-Label FilledButton for RTL
FilledButton.icon automatically mirrors icon and label positions in RTL. For directional icons like arrows, explicit handling is needed.
class LocalizedFilledIconButtons extends StatelessWidget {
const LocalizedFilledIconButtons({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
FilledButton.icon(
onPressed: () {},
icon: const Icon(Icons.cloud_upload),
label: Text(l10n.uploadButton),
),
const SizedBox(height: 12),
FilledButton.icon(
onPressed: () {},
icon: const Icon(Icons.shopping_cart),
label: Text(l10n.addToCartButton),
),
const SizedBox(height: 12),
FilledButton(
onPressed: () {},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(l10n.nextStepButton),
const SizedBox(width: 8),
Icon(
isRtl ? Icons.arrow_back : Icons.arrow_forward,
size: 18,
),
],
),
),
],
);
}
}
Themed FilledButton per Locale
Some designs require locale-specific button styling, such as adjusted padding for CJK scripts or different border radius for cultural aesthetics.
class LocaleThemedFilledButton extends StatelessWidget {
const LocaleThemedFilledButton({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final locale = Localizations.localeOf(context);
final buttonStyle = FilledButton.styleFrom(
padding: EdgeInsets.symmetric(
horizontal: ['de', 'fi'].contains(locale.languageCode) ? 28 : 20,
vertical: 14,
),
textStyle: TextStyle(
fontSize: ['ja', 'zh', 'ko'].contains(locale.languageCode) ? 15 : 14,
fontWeight: FontWeight.w600,
),
);
return Wrap(
spacing: 12,
runSpacing: 12,
alignment: WrapAlignment.center,
children: [
FilledButton(
onPressed: () {},
style: buttonStyle,
child: Text(l10n.confirmButton),
),
FilledButton.tonal(
onPressed: () {},
style: buttonStyle,
child: Text(l10n.cancelButton),
),
],
);
}
}
RTL Support and Bidirectional Layouts
class BidirectionalFilledButtons extends StatelessWidget {
const BidirectionalFilledButtons({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
return Padding(
padding: const EdgeInsetsDirectional.all(24),
child: Row(
children: [
Expanded(
child: FilledButton.tonal(
onPressed: () {},
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(isRtl ? Icons.arrow_forward : Icons.arrow_back, size: 18),
const SizedBox(width: 8),
Text(l10n.previousButton),
],
),
),
),
const SizedBox(width: 16),
Expanded(
child: FilledButton(
onPressed: () {},
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(l10n.nextButton),
const SizedBox(width: 8),
Icon(isRtl ? Icons.arrow_back : Icons.arrow_forward, size: 18),
],
),
),
),
],
),
);
}
}
Testing FilledButton 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 LocalizedFilledButtonExample(),
);
}
testWidgets('FilledButton displays localized text', (tester) async {
await tester.pumpWidget(buildTestWidget());
await tester.pumpAndSettle();
expect(find.byType(FilledButton), findsWidgets);
});
testWidgets('FilledButton adapts in RTL locale', (tester) async {
await tester.pumpWidget(buildTestWidget(locale: const Locale('ar')));
await tester.pumpAndSettle();
final direction = tester.widget<Directionality>(
find.byType(Directionality).first,
);
expect(direction.textDirection, TextDirection.rtl);
});
}
Best Practices
Use FilledButton for primary actions and FilledButton.tonal for secondary to create clear visual hierarchy that works across all locales.
Avoid fixed-width buttons -- use
minimumSizeso buttons grow naturally with longer translations.Use
OverflowBarfor button groups to handle text expansion gracefully by wrapping to the next line.Use
FilledButton.iconfor automatic RTL mirroring of icon-label pairs instead of building custom Row layouts.Maintain consistent padding per locale -- CJK scripts may need slightly different sizing than Latin scripts.
Test loading states in all locales to verify that progress indicators and localized labels display correctly together.
Conclusion
FilledButton is the Material 3 standard for primary action buttons in Flutter. Its filled styling provides clear visual prominence, while its intrinsic sizing and RTL support make it well-suited for multilingual applications. By leveraging the tonal variant for action hierarchy, OverflowBar for responsive layouts, and explicit directional icon handling, you can build FilledButton interfaces that deliver a polished experience across every supported language.