Flutter Stack Localization: Layered Layouts for Multilingual Apps
Stack is Flutter's widget for overlapping children, positioning them relative to the edges of the Stack. In multilingual applications, Stack is critical for creating layered interfaces such as badge overlays, image captions, notification indicators, and floating labels that must adapt correctly when switching between left-to-right and right-to-left languages.
Understanding Stack in Localization Context
Stack allows widgets to be painted on top of each other, with positioning controlled by Positioned, PositionedDirectional, and alignment properties. For multilingual apps, this matters because:
- Overlay positions must flip correctly in RTL languages like Arabic and Hebrew
- Badge and notification indicators need directional awareness
- Text overlays on images must respect reading direction
- Alignment behavior changes based on the ambient Directionality
Why Stack Matters for Multilingual Apps
Stack provides:
- Layered composition: Combine background content with localized overlays
- Directional positioning: PositionedDirectional and AlignmentDirectional adapt to text direction
- RTL badge support: Notification badges automatically reposition in RTL locales
- Flexible overlays: Localized text, labels, and captions overlay correctly in all languages
Basic Stack Implementation
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LocalizedStackExample extends StatelessWidget {
const LocalizedStackExample({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Stack(
alignment: AlignmentDirectional.topStart,
children: [
Container(
width: double.infinity,
height: 200,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Text(
l10n.mainContent,
style: Theme.of(context).textTheme.headlineSmall,
),
),
),
Container(
margin: const EdgeInsetsDirectional.only(start: 12, top: 12),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(8),
),
child: Text(
l10n.featuredLabel,
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimary,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
],
);
}
}
Advanced Stack Patterns for Localization
Directional Positioning with PositionedDirectional
PositionedDirectional uses start and end instead of left and right, automatically flipping positions in RTL layouts.
class DirectionalStackOverlay extends StatelessWidget {
const DirectionalStackOverlay({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Stack(
children: [
Container(
width: double.infinity,
height: 180,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(16),
),
child: Center(child: Text(l10n.cardContent)),
),
PositionedDirectional(
top: 8,
end: 8,
child: IconButton.filled(
icon: const Icon(Icons.close),
onPressed: () {},
tooltip: l10n.closeTooltip,
iconSize: 18,
),
),
PositionedDirectional(
bottom: 12,
start: 12,
end: 12,
child: Text(
l10n.captionText,
style: Theme.of(context).textTheme.bodySmall,
textAlign: TextAlign.start,
),
),
],
);
}
}
RTL-Aware Badge Overlays
Notification badges and status indicators must reposition to the correct corner based on locale direction.
class LocalizedBadgeStack extends StatelessWidget {
final int notificationCount;
const LocalizedBadgeStack({
super.key,
required this.notificationCount,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Stack(
clipBehavior: Clip.none,
children: [
IconButton(
icon: const Icon(Icons.notifications_outlined, size: 28),
onPressed: () {},
tooltip: l10n.notificationsTooltip,
),
if (notificationCount > 0)
PositionedDirectional(
top: 4,
end: 4,
child: Container(
constraints: const BoxConstraints(minWidth: 18, minHeight: 18),
padding: const EdgeInsets.symmetric(horizontal: 4),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.error,
borderRadius: BorderRadius.circular(9),
),
child: Center(
child: Text(
notificationCount > 99 ? '99+' : notificationCount.toString(),
style: TextStyle(
color: Theme.of(context).colorScheme.onError,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
);
}
}
Localized Text Overlays on Images
Image cards with text overlays are a common UI pattern. The text overlay must align to the correct edge when the locale switches between LTR and RTL.
class LocalizedImageOverlayStack extends StatelessWidget {
final String imageUrl;
const LocalizedImageOverlayStack({
super.key,
required this.imageUrl,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Stack(
children: [
AspectRatio(
aspectRatio: 16 / 9,
child: Image.network(
imageUrl,
fit: BoxFit.cover,
errorBuilder: (context, error, stack) => Container(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
child: const Icon(Icons.image, size: 48),
),
),
),
Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: AlignmentDirectional.topStart,
end: AlignmentDirectional.bottomEnd,
colors: [
Colors.transparent,
Colors.black.withOpacity(0.7),
],
stops: const [0.4, 1.0],
),
),
),
),
PositionedDirectional(
bottom: 16,
start: 16,
end: 16,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
l10n.imageTitle,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
l10n.imageSubtitle,
style: TextStyle(
color: Colors.white.withOpacity(0.85),
fontSize: 13,
),
),
],
),
),
],
),
);
}
}
AlignmentDirectional for Locale-Aware Alignment
Stack's alignment property accepts AlignmentDirectional, which resolves start and end based on the current text direction.
class AlignmentDirectionalStack extends StatelessWidget {
const AlignmentDirectionalStack({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Stack(
alignment: AlignmentDirectional.bottomEnd,
children: [
Container(
width: double.infinity,
height: 160,
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).colorScheme.outline),
borderRadius: BorderRadius.circular(12),
),
alignment: AlignmentDirectional.topStart,
padding: const EdgeInsetsDirectional.all(16),
child: Text(
l10n.sectionTitle,
style: Theme.of(context).textTheme.titleMedium,
),
),
Padding(
padding: const EdgeInsetsDirectional.only(end: 12, bottom: 12),
child: FloatingActionButton.small(
onPressed: () {},
tooltip: l10n.addTooltip,
child: const Icon(Icons.add),
),
),
],
);
}
}
RTL Support and Bidirectional Layouts
When building Stack layouts for bidirectional support, always prefer PositionedDirectional over Positioned. The standard Positioned uses physical left and right, which do not flip in RTL.
class BidirectionalStackLayout extends StatelessWidget {
const BidirectionalStackLayout({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isRtl = Directionality.of(context) == TextDirection.rtl;
return Stack(
children: [
Container(
width: double.infinity,
height: 120,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(12),
),
),
PositionedDirectional(
top: 16,
start: 16,
child: Icon(
isRtl ? Icons.arrow_forward : Icons.arrow_back,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
Positioned.fill(
child: Center(
child: Text(
l10n.headerTitle,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
),
),
PositionedDirectional(
top: 16,
end: 16,
child: IconButton(
icon: const Icon(Icons.settings),
onPressed: () {},
tooltip: l10n.settingsTooltip,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
],
);
}
}
Testing Stack Localization
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void main() {
group('LocalizedStackExample', () {
testWidgets('positions badge at end in LTR', (tester) async {
await tester.pumpWidget(
const MaterialApp(
locale: Locale('en'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Scaffold(
body: LocalizedBadgeStack(notificationCount: 5),
),
),
);
final badgeFinder = find.text('5');
expect(badgeFinder, findsOneWidget);
});
testWidgets('positions badge at start in RTL', (tester) async {
await tester.pumpWidget(
const MaterialApp(
locale: Locale('ar'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Scaffold(
body: LocalizedBadgeStack(notificationCount: 3),
),
),
);
final badgeFinder = find.text('3');
expect(badgeFinder, findsOneWidget);
});
});
}
Best Practices
Always use PositionedDirectional instead of Positioned when your app supports RTL languages. The
startandendproperties automatically flip.Set AlignmentDirectional on Stack rather than
Alignmentfor non-positioned children.Use EdgeInsetsDirectional for internal padding within Stack children.
Set clipBehavior: Clip.none when overlays extend beyond Stack bounds, such as notification badges.
Test with long translations and RTL locales to verify that overlaid text does not overflow or overlap.
Wrap interactive positioned elements with proper semantics so that screen readers announce overlay content in the correct locale.
Conclusion
Stack is a foundational widget for building layered, overlapping interfaces in Flutter. For multilingual applications, using PositionedDirectional and AlignmentDirectional ensures that badges, text overlays, notification indicators, and floating elements all adapt correctly to LTR and RTL layouts. By combining Stack with Flutter's built-in localization framework and directional-aware positioning, you can create polished, locale-responsive interfaces that work seamlessly across every language your app supports.