← Back to Blog

Flutter IntrinsicHeight Localization: Adaptive Sizing for Multilingual Layouts

flutterintrinsicheightsizingcardslocalizationtimelines

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

  1. Use IntrinsicHeight sparingly - it has performance implications
  2. Combine with CrossAxisAlignment.stretch for equal-height layouts
  3. Test with multiple locales to ensure layouts handle varying text lengths
  4. Consider alternatives like CustomMultiChildLayout for complex scenarios
  5. Cache measurements when possible for repeated layouts

Don'ts

  1. Don't nest IntrinsicHeight widgets - causes exponential layout cost
  2. Don't use in scrolling lists without optimization - measure once, not per frame
  3. Don't ignore RTL - ensure layouts work in both directions
  4. 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.

Further Reading