Flutter IntrinsicHeight Localization: Adaptive Sizing for Multilingual Layouts
IntrinsicHeight is a powerful Flutter widget that sizes its child to the child's intrinsic height. In multilingual applications, this becomes particularly valuable when dealing with content that varies dramatically in size across different languages and scripts.
Understanding IntrinsicHeight in Localization Context
IntrinsicHeight queries its child's intrinsic height and sizes itself accordingly. This is especially useful in localization scenarios where:
- Text content varies in length and line count across languages
- Row children need equal heights despite different content
- Card layouts must adapt to varying translation lengths
- Dynamic content needs to size correctly without hardcoded dimensions
Why IntrinsicHeight Matters for Localized Apps
Different languages present unique challenges:
- German and Finnish: Often 30-40% longer than English
- Chinese and Japanese: May use fewer characters but different line heights
- Arabic and Hebrew: RTL scripts with different typographic characteristics
- Thai and Hindi: Complex scripts with varying vertical metrics
Basic IntrinsicHeight Implementation
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedIntrinsicHeightExample extends StatelessWidget {
const LocalizedIntrinsicHeightExample({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.featureOneTitle,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text(l10n.featureOneDescription),
],
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.featureTwoTitle,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text(l10n.featureTwoDescription),
],
),
),
),
),
],
),
);
}
}
Equal Height Cards for Localized Content
Feature Comparison Layout
class LocalizedFeatureComparison extends StatelessWidget {
const LocalizedFeatureComparison({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return IntrinsicHeight(
child: Row(
children: [
Expanded(
child: _PlanCard(
title: l10n.basicPlanTitle,
price: l10n.basicPlanPrice,
features: [
l10n.basicFeature1,
l10n.basicFeature2,
l10n.basicFeature3,
],
isPrimary: false,
),
),
const SizedBox(width: 16),
Expanded(
child: _PlanCard(
title: l10n.proPlanTitle,
price: l10n.proPlanPrice,
features: [
l10n.proFeature1,
l10n.proFeature2,
l10n.proFeature3,
l10n.proFeature4,
],
isPrimary: true,
),
),
const SizedBox(width: 16),
Expanded(
child: _PlanCard(
title: l10n.enterprisePlanTitle,
price: l10n.enterprisePlanPrice,
features: [
l10n.enterpriseFeature1,
l10n.enterpriseFeature2,
l10n.enterpriseFeature3,
l10n.enterpriseFeature4,
l10n.enterpriseFeature5,
],
isPrimary: false,
),
),
],
),
);
}
}
class _PlanCard extends StatelessWidget {
final String title;
final String price;
final List<String> features;
final bool isPrimary;
const _PlanCard({
required this.title,
required this.price,
required this.features,
required this.isPrimary,
});
@override
Widget build(BuildContext context) {
return Card(
elevation: isPrimary ? 4 : 1,
color: isPrimary
? Theme.of(context).colorScheme.primaryContainer
: null,
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
price,
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
),
),
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 16),
...features.map((feature) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.check_circle,
size: 20,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
Expanded(child: Text(feature)),
],
),
)),
const Spacer(),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {},
child: Text(AppLocalizations.of(context)!.selectPlanButton),
),
),
],
),
),
);
}
}
IntrinsicHeight with RTL Support
Bidirectional Layout Handling
class RtlAwareIntrinsicHeight extends StatelessWidget {
const RtlAwareIntrinsicHeight({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
return IntrinsicHeight(
child: Row(
textDirection: Directionality.of(context),
children: [
Container(
width: 4,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(2),
),
),
SizedBox(width: isRtl ? 0 : 16),
SizedBox(width: isRtl ? 16 : 0),
Expanded(
child: Column(
crossAxisAlignment: isRtl
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: [
Text(
l10n.quoteTitle,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
l10n.quoteContent,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
fontStyle: FontStyle.italic,
),
),
const SizedBox(height: 8),
Text(
l10n.quoteAuthor,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
],
),
);
}
}
Dynamic Content with IntrinsicHeight
Localized Comment Thread
class LocalizedCommentThread extends StatelessWidget {
final List<CommentData> comments;
const LocalizedCommentThread({
super.key,
required this.comments,
});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: comments.length,
itemBuilder: (context, index) {
final comment = comments[index];
return IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
children: [
CircleAvatar(
radius: 20,
child: Text(comment.authorInitials),
),
if (index < comments.length - 1)
Expanded(
child: Container(
width: 2,
margin: const EdgeInsets.symmetric(vertical: 8),
color: Theme.of(context).dividerColor,
),
),
],
),
const SizedBox(width: 12),
Expanded(
child: Card(
margin: const EdgeInsets.only(bottom: 16),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
comment.authorName,
style: Theme.of(context).textTheme.titleSmall,
),
Text(
comment.timeAgo,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
const SizedBox(height: 8),
Text(comment.content),
],
),
),
),
),
],
),
);
},
);
}
}
class CommentData {
final String authorName;
final String authorInitials;
final String content;
final String timeAgo;
const CommentData({
required this.authorName,
required this.authorInitials,
required this.content,
required this.timeAgo,
});
}
Timeline Layout with IntrinsicHeight
Localized Activity Timeline
class LocalizedTimeline extends StatelessWidget {
const LocalizedTimeline({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final events = [
_TimelineEvent(
title: l10n.eventCreatedTitle,
description: l10n.eventCreatedDescription,
time: l10n.eventCreatedTime,
icon: Icons.create,
),
_TimelineEvent(
title: l10n.eventUpdatedTitle,
description: l10n.eventUpdatedDescription,
time: l10n.eventUpdatedTime,
icon: Icons.edit,
),
_TimelineEvent(
title: l10n.eventCompletedTitle,
description: l10n.eventCompletedDescription,
time: l10n.eventCompletedTime,
icon: Icons.check_circle,
),
];
return Column(
children: events.asMap().entries.map((entry) {
final index = entry.key;
final event = entry.value;
final isLast = index == events.length - 1;
return IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 60,
child: Column(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
shape: BoxShape.circle,
),
child: Icon(
event.icon,
size: 20,
color: Theme.of(context).colorScheme.primary,
),
),
if (!isLast)
Expanded(
child: Container(
width: 2,
margin: const EdgeInsets.symmetric(vertical: 4),
color: Theme.of(context).colorScheme.outlineVariant,
),
),
],
),
),
const SizedBox(width: 12),
Expanded(
child: Padding(
padding: const EdgeInsets.only(bottom: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
event.title,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 4),
Text(
event.time,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.outline,
),
),
const SizedBox(height: 8),
Text(event.description),
],
),
),
),
],
),
);
}).toList(),
);
}
}
class _TimelineEvent {
final String title;
final String description;
final String time;
final IconData icon;
const _TimelineEvent({
required this.title,
required this.description,
required this.time,
required this.icon,
});
}
Performance-Conscious IntrinsicHeight
Caching Intrinsic Measurements
class OptimizedIntrinsicHeightList extends StatelessWidget {
final List<LocalizedItem> items;
const OptimizedIntrinsicHeightList({
super.key,
required this.items,
});
@override
Widget build(BuildContext context) {
// For long lists, consider using CustomMultiChildLayout
// or measuring heights once and caching them
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
// Group items to reduce IntrinsicHeight usage
if (index % 2 == 0 && index + 1 < items.length) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: IntrinsicHeight(
child: Row(
children: [
Expanded(child: _ItemCard(item: items[index])),
const SizedBox(width: 16),
Expanded(child: _ItemCard(item: items[index + 1])),
],
),
),
);
} else if (index % 2 == 0) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
children: [
Expanded(child: _ItemCard(item: items[index])),
const SizedBox(width: 16),
const Expanded(child: SizedBox()),
],
),
);
}
return const SizedBox.shrink();
},
);
}
}
class LocalizedItem {
final String title;
final String description;
const LocalizedItem({
required this.title,
required this.description,
});
}
class _ItemCard extends StatelessWidget {
final LocalizedItem item;
const _ItemCard({required this.item});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.title,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text(item.description),
],
),
),
);
}
}
ARB File Structure
English (app_en.arb)
{
"@@locale": "en",
"featureOneTitle": "Fast Performance",
"@featureOneTitle": {
"description": "Title for the first feature card"
},
"featureOneDescription": "Experience blazing fast performance with our optimized architecture.",
"@featureOneDescription": {
"description": "Description for the first feature"
},
"featureTwoTitle": "Secure & Private",
"@featureTwoTitle": {
"description": "Title for the second feature card"
},
"featureTwoDescription": "Your data is protected with enterprise-grade security measures.",
"@featureTwoDescription": {
"description": "Description for the second feature"
},
"basicPlanTitle": "Basic",
"@basicPlanTitle": {
"description": "Title for basic pricing plan"
},
"basicPlanPrice": "$9/month",
"@basicPlanPrice": {
"description": "Price for basic plan"
},
"basicFeature1": "Up to 5 projects",
"basicFeature2": "Basic analytics",
"basicFeature3": "Email support",
"proPlanTitle": "Professional",
"@proPlanTitle": {
"description": "Title for professional pricing plan"
},
"proPlanPrice": "$29/month",
"proFeature1": "Unlimited projects",
"proFeature2": "Advanced analytics",
"proFeature3": "Priority support",
"proFeature4": "API access",
"enterprisePlanTitle": "Enterprise",
"@enterprisePlanTitle": {
"description": "Title for enterprise pricing plan"
},
"enterprisePlanPrice": "Custom",
"enterpriseFeature1": "Everything in Pro",
"enterpriseFeature2": "Dedicated support",
"enterpriseFeature3": "Custom integrations",
"enterpriseFeature4": "SLA guarantee",
"enterpriseFeature5": "On-premise deployment",
"selectPlanButton": "Select Plan",
"@selectPlanButton": {
"description": "Button to select a pricing plan"
},
"quoteTitle": "Customer Testimonial",
"@quoteTitle": {
"description": "Title for customer quote section"
},
"quoteContent": "This product has transformed how we work. The intuitive interface and powerful features have made our team significantly more productive.",
"@quoteContent": {
"description": "Customer testimonial quote"
},
"quoteAuthor": "— Jane Smith, CEO at TechCorp",
"@quoteAuthor": {
"description": "Author of the testimonial"
},
"eventCreatedTitle": "Project Created",
"eventCreatedDescription": "A new project was created with initial settings configured.",
"eventCreatedTime": "2 hours ago",
"eventUpdatedTitle": "Settings Updated",
"eventUpdatedDescription": "Project settings were modified to enable new features.",
"eventUpdatedTime": "1 hour ago",
"eventCompletedTitle": "Review Completed",
"eventCompletedDescription": "The final review was completed and approved by the team.",
"eventCompletedTime": "30 minutes ago"
}
German (app_de.arb)
{
"@@locale": "de",
"featureOneTitle": "Schnelle Leistung",
"featureOneDescription": "Erleben Sie blitzschnelle Leistung mit unserer optimierten Architektur.",
"featureTwoTitle": "Sicher & Privat",
"featureTwoDescription": "Ihre Daten sind mit Sicherheitsmaßnahmen auf Unternehmensniveau geschützt.",
"basicPlanTitle": "Basis",
"basicPlanPrice": "9 €/Monat",
"basicFeature1": "Bis zu 5 Projekte",
"basicFeature2": "Grundlegende Analysen",
"basicFeature3": "E-Mail-Support",
"proPlanTitle": "Professionell",
"proPlanPrice": "29 €/Monat",
"proFeature1": "Unbegrenzte Projekte",
"proFeature2": "Erweiterte Analysen",
"proFeature3": "Prioritäts-Support",
"proFeature4": "API-Zugang",
"enterprisePlanTitle": "Unternehmen",
"enterprisePlanPrice": "Individuell",
"enterpriseFeature1": "Alles aus Pro",
"enterpriseFeature2": "Dedizierter Support",
"enterpriseFeature3": "Individuelle Integrationen",
"enterpriseFeature4": "SLA-Garantie",
"enterpriseFeature5": "On-Premise-Bereitstellung",
"selectPlanButton": "Plan auswählen",
"quoteTitle": "Kundenstimme",
"quoteContent": "Dieses Produkt hat unsere Arbeitsweise verändert. Die intuitive Benutzeroberfläche und leistungsstarken Funktionen haben unser Team deutlich produktiver gemacht.",
"quoteAuthor": "— Maria Schmidt, CEO bei TechCorp",
"eventCreatedTitle": "Projekt erstellt",
"eventCreatedDescription": "Ein neues Projekt wurde mit Grundeinstellungen erstellt.",
"eventCreatedTime": "Vor 2 Stunden",
"eventUpdatedTitle": "Einstellungen aktualisiert",
"eventUpdatedDescription": "Projekteinstellungen wurden geändert um neue Funktionen zu aktivieren.",
"eventUpdatedTime": "Vor 1 Stunde",
"eventCompletedTitle": "Überprüfung abgeschlossen",
"eventCompletedDescription": "Die abschließende Überprüfung wurde vom Team abgeschlossen und genehmigt.",
"eventCompletedTime": "Vor 30 Minuten"
}
Arabic (app_ar.arb)
{
"@@locale": "ar",
"featureOneTitle": "أداء سريع",
"featureOneDescription": "استمتع بأداء فائق السرعة مع بنيتنا المحسّنة.",
"featureTwoTitle": "آمن وخاص",
"featureTwoDescription": "بياناتك محمية بإجراءات أمنية على مستوى المؤسسات.",
"basicPlanTitle": "الأساسي",
"basicPlanPrice": "٩ دولار/شهر",
"basicFeature1": "حتى ٥ مشاريع",
"basicFeature2": "تحليلات أساسية",
"basicFeature3": "دعم عبر البريد الإلكتروني",
"proPlanTitle": "الاحترافي",
"proPlanPrice": "٢٩ دولار/شهر",
"proFeature1": "مشاريع غير محدودة",
"proFeature2": "تحليلات متقدمة",
"proFeature3": "دعم ذو أولوية",
"proFeature4": "الوصول إلى API",
"enterprisePlanTitle": "المؤسسات",
"enterprisePlanPrice": "مخصص",
"enterpriseFeature1": "كل ما في الاحترافي",
"enterpriseFeature2": "دعم مخصص",
"enterpriseFeature3": "تكاملات مخصصة",
"enterpriseFeature4": "ضمان SLA",
"enterpriseFeature5": "نشر محلي",
"selectPlanButton": "اختر الخطة",
"quoteTitle": "شهادة العميل",
"quoteContent": "لقد غيّر هذا المنتج طريقة عملنا. الواجهة البديهية والميزات القوية جعلت فريقنا أكثر إنتاجية بشكل ملحوظ.",
"quoteAuthor": "— سارة أحمد، الرئيس التنفيذي في تك كورب",
"eventCreatedTitle": "تم إنشاء المشروع",
"eventCreatedDescription": "تم إنشاء مشروع جديد مع تكوين الإعدادات الأولية.",
"eventCreatedTime": "منذ ساعتين",
"eventUpdatedTitle": "تم تحديث الإعدادات",
"eventUpdatedDescription": "تم تعديل إعدادات المشروع لتمكين الميزات الجديدة.",
"eventUpdatedTime": "منذ ساعة",
"eventCompletedTitle": "اكتملت المراجعة",
"eventCompletedDescription": "اكتملت المراجعة النهائية وتمت الموافقة عليها من قبل الفريق.",
"eventCompletedTime": "منذ ٣٠ دقيقة"
}
Best Practices Summary
Do's
- Use IntrinsicHeight sparingly - it has performance implications
- Combine with CrossAxisAlignment.stretch for equal-height layouts
- Test with multiple locales to ensure layouts handle varying text lengths
- Consider alternatives like CustomMultiChildLayout for complex scenarios
- Cache measurements when possible for repeated layouts
Don'ts
- Don't nest IntrinsicHeight widgets - causes exponential layout cost
- Don't use in scrolling lists without optimization - measure once, not per frame
- Don't ignore RTL - ensure layouts work in both directions
- Don't assume fixed heights - let IntrinsicHeight calculate based on content
Accessibility Considerations
class AccessibleIntrinsicHeight extends StatelessWidget {
const AccessibleIntrinsicHeight({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Semantics(
container: true,
label: l10n.featureComparisonLabel,
child: IntrinsicHeight(
child: Row(
children: [
Expanded(
child: Semantics(
header: true,
child: _FeatureCard(
title: l10n.featureOneTitle,
description: l10n.featureOneDescription,
),
),
),
const SizedBox(width: 16),
Expanded(
child: Semantics(
header: true,
child: _FeatureCard(
title: l10n.featureTwoTitle,
description: l10n.featureTwoDescription,
),
),
),
],
),
),
);
}
}
Conclusion
IntrinsicHeight is an invaluable tool for creating adaptive layouts in multilingual Flutter applications. By forcing children to match heights based on content, it ensures visual consistency regardless of translation length. However, use it judiciously—understanding its performance characteristics and combining it with proper RTL support will help you build truly international applications.