Flutter FittedBox Localization: Auto-Scaling Content for Multilingual Apps
FittedBox scales and positions its child within itself according to a fit mode, automatically adjusting content to fit available space. In multilingual applications, FittedBox is essential for handling text that varies in length across languages, ensuring content remains visible and properly sized regardless of translation length. This guide covers comprehensive strategies for using FittedBox in Flutter localization.
Understanding FittedBox in Localization
FittedBox widgets benefit localization for:
- Dynamic text scaling: Automatically shrinking long translations to fit
- Button labels: Ensuring button text fits without overflow
- Headers and titles: Scaling headlines to available width
- Icon + text combinations: Maintaining proportions across languages
- Card content: Fitting variable-length content in fixed containers
- Responsive typography: Adapting text size based on space
Basic FittedBox with Localized Content
Start with simple text scaling:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedFittedBoxDemo extends StatelessWidget {
const LocalizedFittedBoxDemo({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: Text(l10n.fittedBoxTitle)),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
l10n.autoScalingLabel,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 16),
// Fixed width container with FittedBox
Container(
width: 200,
height: 50,
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.outline,
),
borderRadius: BorderRadius.circular(8),
),
child: FittedBox(
fit: BoxFit.scaleDown,
child: Padding(
padding: const EdgeInsets.all(8),
child: Text(
l10n.longTextExample,
style: Theme.of(context).textTheme.headlineSmall,
),
),
),
),
const SizedBox(height: 24),
// Comparison: without FittedBox
Text(
l10n.withoutFittedBoxLabel,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Container(
width: 200,
height: 50,
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.error,
),
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(8),
child: Text(
l10n.longTextExample,
style: Theme.of(context).textTheme.headlineSmall,
overflow: TextOverflow.ellipsis,
),
),
),
const SizedBox(height: 8),
Text(
l10n.overflowNote,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.error,
),
),
],
),
),
);
}
}
ARB File Structure for FittedBox
{
"fittedBoxTitle": "FittedBox Demo",
"@fittedBoxTitle": {
"description": "Title for fitted box demo page"
},
"autoScalingLabel": "Auto-scaling Text",
"longTextExample": "This is a very long text that might overflow in some languages",
"withoutFittedBoxLabel": "Without FittedBox",
"overflowNote": "Text overflows or gets truncated"
}
Buttons with Auto-Scaling Labels
Create buttons that adapt to translation length:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedAdaptiveButtons extends StatelessWidget {
const LocalizedAdaptiveButtons({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(title: Text(l10n.buttonsTitle)),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
l10n.fixedWidthButtonsLabel,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 16),
// Row of fixed-width buttons
Row(
children: [
Expanded(
child: _AdaptiveButton(
label: l10n.buttonSubmit,
onPressed: () {},
color: Theme.of(context).colorScheme.primary,
),
),
const SizedBox(width: 12),
Expanded(
child: _AdaptiveButton(
label: l10n.buttonCancel,
onPressed: () {},
color: Theme.of(context).colorScheme.secondary,
),
),
],
),
const SizedBox(height: 24),
// Action buttons with icons
Text(
l10n.iconButtonsLabel,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _AdaptiveIconButton(
label: l10n.buttonSaveChanges,
icon: Icons.save,
onPressed: () {},
),
),
const SizedBox(width: 12),
Expanded(
child: _AdaptiveIconButton(
label: l10n.buttonDeleteAll,
icon: Icons.delete,
onPressed: () {},
isDestructive: true,
),
),
],
),
const SizedBox(height: 24),
// Long button labels
Text(
l10n.longLabelsLabel,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 16),
SizedBox(
width: 180,
child: _AdaptiveButton(
label: l10n.buttonAcceptTermsAndConditions,
onPressed: () {},
color: Theme.of(context).colorScheme.primary,
),
),
],
),
),
);
}
}
class _AdaptiveButton extends StatelessWidget {
final String label;
final VoidCallback onPressed;
final Color color;
const _AdaptiveButton({
required this.label,
required this.onPressed,
required this.color,
});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: color,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 8),
),
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
label,
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
);
}
}
class _AdaptiveIconButton extends StatelessWidget {
final String label;
final IconData icon;
final VoidCallback onPressed;
final bool isDestructive;
const _AdaptiveIconButton({
required this.label,
required this.icon,
required this.onPressed,
this.isDestructive = false,
});
@override
Widget build(BuildContext context) {
final color = isDestructive
? Theme.of(context).colorScheme.error
: Theme.of(context).colorScheme.primary;
return OutlinedButton(
onPressed: onPressed,
style: OutlinedButton.styleFrom(
foregroundColor: color,
side: BorderSide(color: color),
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
),
child: FittedBox(
fit: BoxFit.scaleDown,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 18),
const SizedBox(width: 8),
Text(label),
],
),
),
);
}
}
Dynamic Headers and Titles
Scale titles to fit available space:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedDynamicHeaders extends StatelessWidget {
const LocalizedDynamicHeaders({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
return Scaffold(
body: CustomScrollView(
slivers: [
// App bar with fitted title
SliverAppBar(
expandedHeight: 200,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: FittedBox(
fit: BoxFit.scaleDown,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 48),
child: Text(l10n.welcomeTitle),
),
),
background: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Theme.of(context).colorScheme.primary,
Theme.of(context).colorScheme.primaryContainer,
],
),
),
),
),
),
// Content
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverList(
delegate: SliverChildListDelegate([
// Section headers with fitted text
_buildSection(
context,
l10n,
title: l10n.featuredSectionTitle,
subtitle: l10n.featuredSectionSubtitle,
),
const SizedBox(height: 24),
// Cards with fitted titles
...List.generate(3, (index) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: _buildFeatureCard(
context,
l10n,
isRtl,
title: l10n.featureTitle(index + 1),
description: l10n.featureDescription(index + 1),
icon: [Icons.star, Icons.speed, Icons.security][index],
),
);
}),
]),
),
),
],
),
);
}
Widget _buildSection(
BuildContext context,
AppLocalizations l10n, {
required String title,
required String subtitle,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Fitted section title
SizedBox(
height: 40,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Directionality.of(context) == TextDirection.rtl
? Alignment.centerRight
: Alignment.centerLeft,
child: Text(
title,
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
);
}
Widget _buildFeatureCard(
BuildContext context,
AppLocalizations l10n,
bool isRtl, {
required String title,
required String description,
required IconData icon,
}) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: Icon(
icon,
color: Theme.of(context).colorScheme.primary,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Fitted card title
SizedBox(
height: 24,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: isRtl
? Alignment.centerRight
: Alignment.centerLeft,
child: Text(
title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(height: 4),
Text(
description,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
Icon(
isRtl ? Icons.chevron_left : Icons.chevron_right,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
],
),
),
);
}
}
Fit Modes Comparison
Demonstrate different fit modes:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class FitModesComparison extends StatelessWidget {
const FitModesComparison({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final fitModes = [
(BoxFit.contain, l10n.fitContain, l10n.fitContainDescription),
(BoxFit.cover, l10n.fitCover, l10n.fitCoverDescription),
(BoxFit.fill, l10n.fitFill, l10n.fitFillDescription),
(BoxFit.scaleDown, l10n.fitScaleDown, l10n.fitScaleDownDescription),
(BoxFit.fitWidth, l10n.fitWidth, l10n.fitWidthDescription),
(BoxFit.fitHeight, l10n.fitHeight, l10n.fitHeightDescription),
];
return Scaffold(
appBar: AppBar(title: Text(l10n.fitModesTitle)),
body: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: fitModes.length,
itemBuilder: (context, index) {
final (fit, name, description) = fitModes[index];
return Card(
margin: const EdgeInsets.only(bottom: 16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
description,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 12),
// Demo container
Container(
height: 80,
width: double.infinity,
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.outline,
),
borderRadius: BorderRadius.circular(8),
),
child: FittedBox(
fit: fit,
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
padding: const EdgeInsets.all(16),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.text_fields,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
Text(
l10n.sampleText,
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimaryContainer,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
),
],
),
),
);
},
),
);
}
}
Price Tags and Labels
Create auto-scaling price displays:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedPriceTags extends StatelessWidget {
const LocalizedPriceTags({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
final products = [
{
'name': l10n.productName1,
'price': l10n.price('\$1,299.99'),
'discount': l10n.discountLabel('20%'),
'hasDiscount': true,
},
{
'name': l10n.productName2,
'price': l10n.price('\$49.99'),
'discount': '',
'hasDiscount': false,
},
{
'name': l10n.productName3,
'price': l10n.price('\$199.99'),
'discount': l10n.discountLabel('15%'),
'hasDiscount': true,
},
];
return Scaffold(
appBar: AppBar(title: Text(l10n.priceTagsTitle)),
body: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return Card(
margin: const EdgeInsets.only(bottom: 16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
// Product image placeholder
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.image,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(width: 16),
// Product details
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product['name'] as String,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Row(
children: [
// Price with FittedBox
Expanded(
child: SizedBox(
height: 28,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: isRtl
? Alignment.centerRight
: Alignment.centerLeft,
child: Text(
product['price'] as String,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
),
),
),
// Discount badge
if (product['hasDiscount'] as bool)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(4),
),
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
product['discount'] as String,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
),
],
),
],
),
),
],
),
),
);
},
),
);
}
}
Complete ARB File for FittedBox
{
"@@locale": "en",
"fittedBoxTitle": "FittedBox Demo",
"autoScalingLabel": "Auto-scaling Text",
"longTextExample": "This is a very long text that might overflow in some languages",
"withoutFittedBoxLabel": "Without FittedBox",
"overflowNote": "Text overflows or gets truncated",
"buttonsTitle": "Adaptive Buttons",
"fixedWidthButtonsLabel": "Fixed Width Buttons",
"buttonSubmit": "Submit",
"buttonCancel": "Cancel",
"iconButtonsLabel": "Buttons with Icons",
"buttonSaveChanges": "Save Changes",
"buttonDeleteAll": "Delete All",
"longLabelsLabel": "Long Labels",
"buttonAcceptTermsAndConditions": "Accept Terms and Conditions",
"welcomeTitle": "Welcome to Our Amazing Application",
"featuredSectionTitle": "Featured Highlights",
"featuredSectionSubtitle": "Discover what makes us special",
"featureTitle": "Feature {number}",
"@featureTitle": {
"placeholders": {"number": {"type": "int"}}
},
"featureDescription": "Description for feature {number} explaining its benefits and functionality.",
"@featureDescription": {
"placeholders": {"number": {"type": "int"}}
},
"fitModesTitle": "Fit Modes",
"fitContain": "BoxFit.contain",
"fitContainDescription": "Scales to fit within bounds while maintaining aspect ratio",
"fitCover": "BoxFit.cover",
"fitCoverDescription": "Scales to cover bounds, may clip content",
"fitFill": "BoxFit.fill",
"fitFillDescription": "Stretches to fill bounds, may distort content",
"fitScaleDown": "BoxFit.scaleDown",
"fitScaleDownDescription": "Only scales down if needed, never scales up",
"fitWidth": "BoxFit.fitWidth",
"fitWidthDescription": "Scales to fit width, may overflow height",
"fitHeight": "BoxFit.fitHeight",
"fitHeightDescription": "Scales to fit height, may overflow width",
"sampleText": "Sample Text",
"priceTagsTitle": "Price Tags",
"productName1": "Premium Wireless Headphones",
"productName2": "Phone Case",
"productName3": "Bluetooth Speaker",
"price": "{amount}",
"@price": {
"placeholders": {"amount": {"type": "String"}}
},
"discountLabel": "-{percent} OFF",
"@discountLabel": {
"placeholders": {"percent": {"type": "String"}}
}
}
Best Practices Summary
- Use scaleDown for text: Prevents text from becoming larger than intended
- Set alignment: Respect RTL with appropriate alignment values
- Wrap content properly: Include padding inside FittedBox child
- Fixed container size: FittedBox needs bounded parent dimensions
- Combine with constraints: Add min height/width to prevent excessive shrinking
- Test with long translations: German and Finnish text is often longer
- Avoid for body text: Use only for headers, labels, and buttons
- Consider readability: Don't let text scale too small
- Use with icons: Wrap icon+text rows to maintain proportions
- Performance: FittedBox is efficient, use freely for UI polish
Conclusion
FittedBox is invaluable for creating localization-friendly Flutter interfaces that gracefully handle text length variations across languages. By automatically scaling content to fit available space, FittedBox eliminates text overflow issues that commonly plague multilingual applications.
The key is choosing the appropriate fit mode—BoxFit.scaleDown is usually the best choice for text as it only shrinks when necessary and never enlarges content beyond its natural size. Combined with proper alignment settings for RTL support, FittedBox ensures your app looks polished in every language.