Flutter IntrinsicWidth Localization: Smart Width Sizing for Multilingual Apps
IntrinsicWidth is Flutter's widget for sizing children to their intrinsic width. In multilingual applications, this capability becomes essential for creating layouts that adapt gracefully to varying text lengths across different languages.
Understanding IntrinsicWidth in Localization Context
IntrinsicWidth sizes its child to the child's maximum intrinsic width. This is particularly useful when:
- Button widths need to match across different translations
- Column children should align to the widest element
- Dialog content needs consistent sizing regardless of language
- Form labels and inputs require uniform widths
Localization Challenges IntrinsicWidth Solves
Different languages present width challenges:
- German: Words like "Geschwindigkeitsbegrenzung" are significantly wider than English equivalents
- Thai: Script connections create varying character widths
- Japanese: Mix of wide (kanji) and narrow (hiragana) characters
- Arabic: Connected script with varying ligature widths
Basic IntrinsicWidth Implementation
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedIntrinsicWidthExample extends StatelessWidget {
const LocalizedIntrinsicWidthExample({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Center(
child: IntrinsicWidth(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton(
onPressed: () {},
child: Text(l10n.signInButton),
),
const SizedBox(height: 12),
OutlinedButton(
onPressed: () {},
child: Text(l10n.createAccountButton),
),
const SizedBox(height: 12),
TextButton(
onPressed: () {},
child: Text(l10n.forgotPasswordButton),
),
],
),
),
);
}
}
Equal Width Buttons Pattern
Localized Action Buttons
class LocalizedActionButtons extends StatelessWidget {
const LocalizedActionButtons({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return IntrinsicWidth(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_ActionButton(
icon: Icons.edit,
label: l10n.editAction,
onPressed: () {},
),
const SizedBox(height: 8),
_ActionButton(
icon: Icons.share,
label: l10n.shareAction,
onPressed: () {},
),
const SizedBox(height: 8),
_ActionButton(
icon: Icons.delete,
label: l10n.deleteAction,
onPressed: () {},
isDestructive: true,
),
],
),
);
}
}
class _ActionButton extends StatelessWidget {
final IconData icon;
final String label;
final VoidCallback onPressed;
final bool isDestructive;
const _ActionButton({
required this.icon,
required this.label,
required this.onPressed,
this.isDestructive = false,
});
@override
Widget build(BuildContext context) {
final color = isDestructive
? Theme.of(context).colorScheme.error
: null;
return OutlinedButton.icon(
onPressed: onPressed,
icon: Icon(icon, color: color),
label: Text(
label,
style: TextStyle(color: color),
),
style: OutlinedButton.styleFrom(
side: isDestructive
? BorderSide(color: Theme.of(context).colorScheme.error)
: null,
),
);
}
}
Dialog with IntrinsicWidth
Localized Confirmation Dialog
class LocalizedConfirmationDialog extends StatelessWidget {
final String title;
final String message;
final String confirmLabel;
final String cancelLabel;
final VoidCallback onConfirm;
final VoidCallback onCancel;
const LocalizedConfirmationDialog({
super.key,
required this.title,
required this.message,
required this.confirmLabel,
required this.cancelLabel,
required this.onConfirm,
required this.onCancel,
});
@override
Widget build(BuildContext context) {
return Dialog(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
Text(
message,
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
IntrinsicWidth(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ElevatedButton(
onPressed: onConfirm,
child: Text(confirmLabel),
),
const SizedBox(height: 8),
TextButton(
onPressed: onCancel,
child: Text(cancelLabel),
),
],
),
),
],
),
),
);
}
static Future<bool?> show(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return showDialog<bool>(
context: context,
builder: (context) => LocalizedConfirmationDialog(
title: l10n.confirmDialogTitle,
message: l10n.confirmDialogMessage,
confirmLabel: l10n.confirmButton,
cancelLabel: l10n.cancelButton,
onConfirm: () => Navigator.pop(context, true),
onCancel: () => Navigator.pop(context, false),
),
);
}
}
Form Layout with IntrinsicWidth
Aligned Form Fields
class LocalizedFormLayout extends StatelessWidget {
const LocalizedFormLayout({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Padding(
padding: const EdgeInsets.all(16),
child: IntrinsicWidth(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_FormField(
label: l10n.usernameLabel,
hint: l10n.usernameHint,
),
const SizedBox(height: 16),
_FormField(
label: l10n.emailLabel,
hint: l10n.emailHint,
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 16),
_FormField(
label: l10n.passwordLabel,
hint: l10n.passwordHint,
obscureText: true,
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {},
child: Text(l10n.submitButton),
),
],
),
),
);
}
}
class _FormField extends StatelessWidget {
final String label;
final String hint;
final bool obscureText;
final TextInputType? keyboardType;
const _FormField({
required this.label,
required this.hint,
this.obscureText = false,
this.keyboardType,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: Theme.of(context).textTheme.labelLarge,
),
const SizedBox(height: 8),
TextField(
obscureText: obscureText,
keyboardType: keyboardType,
decoration: InputDecoration(
hintText: hint,
border: const OutlineInputBorder(),
),
),
],
);
}
}
RTL-Aware IntrinsicWidth Layouts
Bidirectional Menu
class LocalizedMenu extends StatelessWidget {
const LocalizedMenu({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final textDirection = Directionality.of(context);
return Card(
child: IntrinsicWidth(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_MenuItem(
icon: Icons.person,
label: l10n.menuProfile,
textDirection: textDirection,
),
const Divider(height: 1),
_MenuItem(
icon: Icons.settings,
label: l10n.menuSettings,
textDirection: textDirection,
),
const Divider(height: 1),
_MenuItem(
icon: Icons.help_outline,
label: l10n.menuHelp,
textDirection: textDirection,
),
const Divider(height: 1),
_MenuItem(
icon: Icons.logout,
label: l10n.menuLogout,
textDirection: textDirection,
isDestructive: true,
),
],
),
),
);
}
}
class _MenuItem extends StatelessWidget {
final IconData icon;
final String label;
final TextDirection textDirection;
final bool isDestructive;
const _MenuItem({
required this.icon,
required this.label,
required this.textDirection,
this.isDestructive = false,
});
@override
Widget build(BuildContext context) {
final color = isDestructive
? Theme.of(context).colorScheme.error
: null;
return InkWell(
onTap: () {},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
child: Row(
textDirection: textDirection,
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: color, size: 20),
const SizedBox(width: 12),
Text(
label,
style: TextStyle(color: color),
),
],
),
),
);
}
}
Dropdown with IntrinsicWidth
Localized Selection Dropdown
class LocalizedDropdown extends StatefulWidget {
const LocalizedDropdown({super.key});
@override
State<LocalizedDropdown> createState() => _LocalizedDropdownState();
}
class _LocalizedDropdownState extends State<LocalizedDropdown> {
String? _selectedValue;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final options = [
_DropdownOption(value: 'daily', label: l10n.frequencyDaily),
_DropdownOption(value: 'weekly', label: l10n.frequencyWeekly),
_DropdownOption(value: 'monthly', label: l10n.frequencyMonthly),
_DropdownOption(value: 'yearly', label: l10n.frequencyYearly),
];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.selectFrequencyLabel,
style: Theme.of(context).textTheme.labelLarge,
),
const SizedBox(height: 8),
IntrinsicWidth(
child: DropdownButtonFormField<String>(
value: _selectedValue,
hint: Text(l10n.selectFrequencyHint),
decoration: const InputDecoration(
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
items: options.map((option) {
return DropdownMenuItem(
value: option.value,
child: Text(option.label),
);
}).toList(),
onChanged: (value) {
setState(() {
_selectedValue = value;
});
},
),
),
],
);
}
}
class _DropdownOption {
final String value;
final String label;
const _DropdownOption({
required this.value,
required this.label,
});
}
Step Indicator with IntrinsicWidth
Localized Progress Steps
class LocalizedStepIndicator extends StatelessWidget {
final int currentStep;
const LocalizedStepIndicator({
super.key,
required this.currentStep,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final steps = [
l10n.stepAccount,
l10n.stepDetails,
l10n.stepReview,
l10n.stepComplete,
];
return IntrinsicWidth(
child: Row(
mainAxisSize: MainAxisSize.min,
children: steps.asMap().entries.map((entry) {
final index = entry.key;
final label = entry.value;
final isActive = index <= currentStep;
final isLast = index == steps.length - 1;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
_StepDot(
number: index + 1,
label: label,
isActive: isActive,
isCurrent: index == currentStep,
),
if (!isLast)
Container(
width: 40,
height: 2,
color: isActive
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outlineVariant,
),
],
);
}).toList(),
),
);
}
}
class _StepDot extends StatelessWidget {
final int number;
final String label;
final bool isActive;
final bool isCurrent;
const _StepDot({
required this.number,
required this.label,
required this.isActive,
required this.isCurrent,
});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isActive
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.surfaceContainerHighest,
border: isCurrent
? Border.all(
color: Theme.of(context).colorScheme.primary,
width: 2,
)
: null,
),
child: Center(
child: isActive && !isCurrent
? Icon(
Icons.check,
size: 16,
color: Theme.of(context).colorScheme.onPrimary,
)
: Text(
'$number',
style: TextStyle(
color: isActive
? Theme.of(context).colorScheme.onPrimary
: Theme.of(context).colorScheme.onSurfaceVariant,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(height: 4),
Text(
label,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: isActive
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
fontWeight: isCurrent ? FontWeight.bold : null,
),
),
],
);
}
}
Chip Group with IntrinsicWidth
Localized Filter Chips
class LocalizedChipGroup extends StatefulWidget {
const LocalizedChipGroup({super.key});
@override
State<LocalizedChipGroup> createState() => _LocalizedChipGroupState();
}
class _LocalizedChipGroupState extends State<LocalizedChipGroup> {
final Set<String> _selectedFilters = {};
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final filters = {
'all': l10n.filterAll,
'active': l10n.filterActive,
'pending': l10n.filterPending,
'completed': l10n.filterCompleted,
};
return IntrinsicWidth(
child: Wrap(
spacing: 8,
runSpacing: 8,
children: filters.entries.map((entry) {
final isSelected = _selectedFilters.contains(entry.key);
return FilterChip(
label: Text(entry.value),
selected: isSelected,
onSelected: (selected) {
setState(() {
if (selected) {
_selectedFilters.add(entry.key);
} else {
_selectedFilters.remove(entry.key);
}
});
},
);
}).toList(),
),
);
}
}
ARB File Structure
English (app_en.arb)
{
"@@locale": "en",
"signInButton": "Sign In",
"@signInButton": {
"description": "Button to sign in to account"
},
"createAccountButton": "Create Account",
"@createAccountButton": {
"description": "Button to create a new account"
},
"forgotPasswordButton": "Forgot Password?",
"@forgotPasswordButton": {
"description": "Link to password recovery"
},
"editAction": "Edit",
"@editAction": {
"description": "Edit action button"
},
"shareAction": "Share",
"@shareAction": {
"description": "Share action button"
},
"deleteAction": "Delete",
"@deleteAction": {
"description": "Delete action button"
},
"confirmDialogTitle": "Confirm Action",
"@confirmDialogTitle": {
"description": "Title for confirmation dialogs"
},
"confirmDialogMessage": "Are you sure you want to proceed with this action? This cannot be undone.",
"@confirmDialogMessage": {
"description": "Confirmation dialog message"
},
"confirmButton": "Confirm",
"cancelButton": "Cancel",
"usernameLabel": "Username",
"usernameHint": "Enter your username",
"emailLabel": "Email",
"emailHint": "Enter your email address",
"passwordLabel": "Password",
"passwordHint": "Enter your password",
"submitButton": "Submit",
"menuProfile": "Profile",
"menuSettings": "Settings",
"menuHelp": "Help & Support",
"menuLogout": "Log Out",
"selectFrequencyLabel": "Notification Frequency",
"selectFrequencyHint": "Select frequency",
"frequencyDaily": "Daily",
"frequencyWeekly": "Weekly",
"frequencyMonthly": "Monthly",
"frequencyYearly": "Yearly",
"stepAccount": "Account",
"stepDetails": "Details",
"stepReview": "Review",
"stepComplete": "Complete",
"filterAll": "All",
"filterActive": "Active",
"filterPending": "Pending",
"filterCompleted": "Completed"
}
German (app_de.arb)
{
"@@locale": "de",
"signInButton": "Anmelden",
"createAccountButton": "Konto erstellen",
"forgotPasswordButton": "Passwort vergessen?",
"editAction": "Bearbeiten",
"shareAction": "Teilen",
"deleteAction": "Löschen",
"confirmDialogTitle": "Aktion bestätigen",
"confirmDialogMessage": "Sind Sie sicher, dass Sie mit dieser Aktion fortfahren möchten? Dies kann nicht rückgängig gemacht werden.",
"confirmButton": "Bestätigen",
"cancelButton": "Abbrechen",
"usernameLabel": "Benutzername",
"usernameHint": "Geben Sie Ihren Benutzernamen ein",
"emailLabel": "E-Mail-Adresse",
"emailHint": "Geben Sie Ihre E-Mail-Adresse ein",
"passwordLabel": "Passwort",
"passwordHint": "Geben Sie Ihr Passwort ein",
"submitButton": "Absenden",
"menuProfile": "Profil",
"menuSettings": "Einstellungen",
"menuHelp": "Hilfe & Support",
"menuLogout": "Abmelden",
"selectFrequencyLabel": "Benachrichtigungshäufigkeit",
"selectFrequencyHint": "Häufigkeit auswählen",
"frequencyDaily": "Täglich",
"frequencyWeekly": "Wöchentlich",
"frequencyMonthly": "Monatlich",
"frequencyYearly": "Jährlich",
"stepAccount": "Konto",
"stepDetails": "Details",
"stepReview": "Überprüfung",
"stepComplete": "Abschluss",
"filterAll": "Alle",
"filterActive": "Aktiv",
"filterPending": "Ausstehend",
"filterCompleted": "Abgeschlossen"
}
Arabic (app_ar.arb)
{
"@@locale": "ar",
"signInButton": "تسجيل الدخول",
"createAccountButton": "إنشاء حساب",
"forgotPasswordButton": "نسيت كلمة المرور؟",
"editAction": "تعديل",
"shareAction": "مشاركة",
"deleteAction": "حذف",
"confirmDialogTitle": "تأكيد الإجراء",
"confirmDialogMessage": "هل أنت متأكد من أنك تريد المتابعة في هذا الإجراء؟ لا يمكن التراجع عن هذا.",
"confirmButton": "تأكيد",
"cancelButton": "إلغاء",
"usernameLabel": "اسم المستخدم",
"usernameHint": "أدخل اسم المستخدم",
"emailLabel": "البريد الإلكتروني",
"emailHint": "أدخل بريدك الإلكتروني",
"passwordLabel": "كلمة المرور",
"passwordHint": "أدخل كلمة المرور",
"submitButton": "إرسال",
"menuProfile": "الملف الشخصي",
"menuSettings": "الإعدادات",
"menuHelp": "المساعدة والدعم",
"menuLogout": "تسجيل الخروج",
"selectFrequencyLabel": "تكرار الإشعارات",
"selectFrequencyHint": "اختر التكرار",
"frequencyDaily": "يومياً",
"frequencyWeekly": "أسبوعياً",
"frequencyMonthly": "شهرياً",
"frequencyYearly": "سنوياً",
"stepAccount": "الحساب",
"stepDetails": "التفاصيل",
"stepReview": "المراجعة",
"stepComplete": "الإكمال",
"filterAll": "الكل",
"filterActive": "نشط",
"filterPending": "قيد الانتظار",
"filterCompleted": "مكتمل"
}
Best Practices Summary
Do's
- Use CrossAxisAlignment.stretch with IntrinsicWidth for uniform widths
- Combine with Column for vertical button/menu layouts
- Test with verbose languages like German to ensure adequate space
- Consider RTL when placing IntrinsicWidth content
- Use stepWidth for minimum size control when needed
Don'ts
- Don't nest IntrinsicWidth unnecessarily - performance cost compounds
- Don't use for very large content - calculate sizes differently
- Don't ignore text scale - accessibility may affect intrinsic widths
- Don't assume widths are static - they change with translations
Performance Considerations
class OptimizedIntrinsicWidth extends StatelessWidget {
const OptimizedIntrinsicWidth({super.key});
@override
Widget build(BuildContext context) {
// For repeated elements, measure once and apply
return LayoutBuilder(
builder: (context, constraints) {
// IntrinsicWidth is acceptable here as it's not in a list
return IntrinsicWidth(
// stepWidth ensures minimum width
stepWidth: 120,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Content here
],
),
);
},
);
}
}
Conclusion
IntrinsicWidth is a powerful tool for creating consistent, language-adaptive layouts in Flutter. By sizing content to match the widest child, it ensures visual harmony across translations. Use it thoughtfully—primarily for menus, button groups, and dialogs where uniform widths enhance user experience—and always test with multiple locales to verify your layouts handle varying text lengths gracefully.