← Back to Blog

Flutter IntrinsicWidth Localization: Smart Width Sizing for Multilingual Apps

flutterintrinsicwidthsizingbuttonslocalizationdialogs

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

  1. Use CrossAxisAlignment.stretch with IntrinsicWidth for uniform widths
  2. Combine with Column for vertical button/menu layouts
  3. Test with verbose languages like German to ensure adequate space
  4. Consider RTL when placing IntrinsicWidth content
  5. Use stepWidth for minimum size control when needed

Don'ts

  1. Don't nest IntrinsicWidth unnecessarily - performance cost compounds
  2. Don't use for very large content - calculate sizes differently
  3. Don't ignore text scale - accessibility may affect intrinsic widths
  4. 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.

Further Reading