Stack
Understand how to overlay and position widgets using Stack.
What is it?
Stack is a layout widget that allows you to overlay multiple children on top of each other. Children in a Stack are positioned relative to the edges of the Stack container, and can be placed at specific coordinates using Positioned widgets. This is useful for creating complex UIs where elements need to overlap, like badges, tooltips, floating buttons, or custom designs.
Why does it exist?
Stack exists to:
- Overlap widgets on top of each other
- Create complex layered UIs
- Position widgets relative to parent edges
- Build custom UI components like badges
- Implement floating elements
- Create overlay effects and animations
- Support complex design patterns
Basic Stack
Stack overlays children on top of each other.
// Basic Stack usage
class BasicStack extends StatelessWidget {
const BasicStack({super.key});
@override
Widget build(BuildContext context) {
return Stack(
children: [
// 1. Background layer
Container(
width: 200,
height: 200,
color: Colors.blue,
),
// 2. Middle layer
Container(
width: 150,
height: 150,
color: Colors.green.withOpacity(0.5),
),
// 3. Foreground layer
Container(
width: 100,
height: 100,
color: Colors.red.withOpacity(0.5),
),
// 4. Text on top
const Center(
child: Text(
'Stack',
style: TextStyle(
color: Colors.white,
fontSize: 24,
),
),
),
],
);
}
}
// Stack properties:
// 1. children - List of widgets (later children appear on top)
// 2. alignment - Default alignment for children
// 3. textDirection - LTR or RTL
// 4. fit - How to size the stack
// 5. clipBehavior - Clipping behavior
What's happening here? - Children are stacked in order - Later children appear on top - Widgets can overlap - Positioned widgets for specific placement - Center for automatic centering
Positioned Widget
Positioned places children at specific locations.
// Positioned widget examples
class PositionedExample extends StatelessWidget {
const PositionedExample({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
width: 300,
height: 300,
child: Stack(
children: [
// Background
Container(color: Colors.grey[200]),
// 1. Positioned with top-left
Positioned(
top: 20,
left: 20,
child: Container(
padding: const EdgeInsets.all(8),
color: Colors.red,
child: const Text('Top Left'),
),
),
// 2. Positioned with top-right
Positioned(
top: 20,
right: 20,
child: Container(
padding: const EdgeInsets.all(8),
color: Colors.green,
child: const Text('Top Right'),
),
),
// 3. Positioned with bottom-left
Positioned(
bottom: 20,
left: 20,
child: Container(
padding: const EdgeInsets.all(8),
color: Colors.blue,
child: const Text('Bottom Left'),
),
),
// 4. Positioned with bottom-right
Positioned(
bottom: 20,
right: 20,
child: Container(
padding: const EdgeInsets.all(8),
color: Colors.orange,
child: const Text('Bottom Right'),
),
),
// 5. Positioned with center
Positioned(
top: 0,
bottom: 0,
left: 0,
right: 0,
child: const Center(
child: Text(
'Center',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
);
}
}
// Positioned properties:
// 1. top - Distance from top edge
// 2. bottom - Distance from bottom edge
// 3. left - Distance from left edge
// 4. right - Distance from right edge
// 5. width - Fixed width
// 6. height - Fixed height
// 7. child - The widget to position
What's happening here? - Positioned places child at specific location - Can use top, bottom, left, right - Can use width and height constraints - Positions relative to Stack edges - Can combine edges for stretching
Stack Alignment
Alignment controls default child positioning.
// Stack alignment examples
class StackAlignmentExample extends StatelessWidget {
const StackAlignmentExample({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
// 1. Default alignment (top-left)
Container(
width: 200,
height: 200,
color: Colors.grey[200],
child: Stack(
children: [
Container(
width: 50,
height: 50,
color: Colors.red,
),
const Text('Default'),
],
),
),
const SizedBox(height: 10),
// 2. Center alignment
Container(
width: 200,
height: 200,
color: Colors.grey[200],
child: Stack(
alignment: Alignment.center,
children: [
Container(
width: 50,
height: 50,
color: Colors.red,
),
const Text('Center'),
],
),
),
const SizedBox(height: 10),
// 3. Bottom-right alignment
Container(
width: 200,
height: 200,
color: Colors.grey[200],
child: Stack(
alignment: Alignment.bottomRight,
children: [
Container(
width: 50,
height: 50,
color: Colors.red,
),
const Text('Bottom Right'),
],
),
),
],
);
}
}
// Alignment values:
// 1. Alignment.topLeft
// 2. Alignment.topCenter
// 3. Alignment.topRight
// 4. Alignment.centerLeft
// 5. Alignment.center
// 6. Alignment.centerRight
// 7. Alignment.bottomLeft
// 8. Alignment.bottomCenter
// 9. Alignment.bottomRight
What's happening here? - Alignment centers children by default - Applies to non-positioned children - Different alignment options available - Can be combined with Positioned - Default is top-left
Stack with Sized Box
Controlling Stack size and fit.
// Stack sizing examples
class StackSizingExample extends StatelessWidget {
const StackSizingExample({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
// 1. Stack with fixed size
SizedBox(
width: 200,
height: 200,
child: Stack(
children: [
Container(color: Colors.blue),
const Positioned(
top: 20,
left: 20,
child: Text('Fixed Size'),
),
],
),
),
const SizedBox(height: 10),
// 2. Stack with fit - expand
Container(
color: Colors.grey[200],
child: Stack(
fit: StackFit.expand,
children: [
Container(color: Colors.blue),
const Positioned(
top: 20,
left: 20,
child: Text('Expand Fit'),
),
],
),
),
const SizedBox(height: 10),
// 3. Stack with fit - loose
Container(
color: Colors.grey[200],
child: Stack(
fit: StackFit.loose,
children: [
Container(
width: 100,
height: 100,
color: Colors.blue,
),
const Positioned(
top: 20,
left: 20,
child: Text('Loose Fit'),
),
],
),
),
const SizedBox(height: 10),
// 4. Stack with fit - passthrough
Container(
color: Colors.grey[200],
child: Stack(
fit: StackFit.passthrough,
children: [
Container(
width: 100,
height: 100,
color: Colors.blue,
),
const Positioned(
top: 20,
left: 20,
child: Text('Passthrough'),
),
],
),
),
],
);
}
}
// StackFit options:
// 1. StackFit.loose - Children determine size
// 2. StackFit.expand - Stack expands to fill parent
// 3. StackFit.passthrough - Constraints passed through
What's happening here? - fit controls how stack sizes itself - expand fills parent - loose allows children to determine size - passthrough passes constraints - Choose based on layout needs
Real-World Examples
Common patterns using Stack.
// 1. Profile picture with badge
class ProfileWithBadge extends StatelessWidget {
const ProfileWithBadge({super.key});
@override
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.bottomRight,
children: [
// Profile picture
const CircleAvatar(
radius: 40,
backgroundImage: NetworkImage('https://example.com/avatar.jpg'),
),
// Online badge
Positioned(
bottom: 0,
right: 0,
child: Container(
width: 16,
height: 16,
decoration: const BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
border: Border.fromBorderSide(
BorderSide(color: Colors.white, width: 2),
),
),
),
),
],
);
}
}
// 2. Image with overlay
class ImageWithOverlay extends StatelessWidget {
const ImageWithOverlay({super.key});
@override
Widget build(BuildContext context) {
return Stack(
children: [
// Image
Image.network(
'https://example.com/image.jpg',
width: double.infinity,
height: 200,
fit: BoxFit.cover,
),
// Gradient overlay
Container(
height: 200,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black.withOpacity(0.7),
],
),
),
),
// Text overlay
Positioned(
bottom: 20,
left: 20,
right: 20,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Title',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'Subtitle goes here',
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 16,
),
),
],
),
),
],
);
}
}
// 3. Floating action button
class FloatingActionExample extends StatelessWidget {
const FloatingActionExample({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
// Content
Container(color: Colors.grey[200]),
// FAB with overlay
Positioned(
bottom: 30,
right: 30,
child: Stack(
alignment: Alignment.topRight,
children: [
// FAB
Container(
width: 56,
height: 56,
decoration: const BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
child: const Icon(
Icons.add,
color: Colors.white,
size: 30,
),
),
// Badge
Positioned(
top: 0,
right: 0,
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
child: const Text(
'3',
style: TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
],
),
);
}
}
// 4. Badge on widget
class BadgeWidget extends StatelessWidget {
const BadgeWidget({
super.key,
required this.child,
required this.count,
});
final Widget child;
final int count;
@override
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.topRight,
children: [
child,
if (count > 0)
Positioned(
top: 4,
right: 4,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
child: Text(
count > 99 ? '99+' : count.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
),
],
);
}
}
What's happening here? - Profile with online badge - Image with text overlay - Floating action button with badge - Reusable badge widget
Best Practices
Use Positioned for Exact Placement
// Good - Using Positioned for exact placement
@override
Widget build(BuildContext context) {
return Stack(
children: [
Container(color: Colors.blue),
Positioned(
top: 20,
left: 20,
child: const Text('Positioned'),
),
],
);
}
Use Alignment for Automatic Positioning
// Good - Using Alignment for centering
@override
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.center,
children: [
Container(color: Colors.blue),
const Text('Centered'),
],
);
}
Keep Stack Efficient
// Good - Stack with RepaintBoundary
@override
Widget build(BuildContext context) {
return RepaintBoundary(
child: Stack(
children: [
// Complex widgets
Container(color: Colors.blue),
Positioned(
top: 20,
left: 20,
child: const Text('Text'),
),
],
),
);
}
Common Mistakes
Positioned Without Stack
Wrong:
// Positioned must be inside Stack
Positioned(
top: 20,
left: 20,
child: const Text('Error'),
)
Correct:
// Positioned inside Stack
Stack(
children: [
Positioned(
top: 20,
left: 20,
child: const Text('Correct'),
),
],
)
Overlapping Without Stack
Wrong:
// Can't overlap without Stack
Container(color: Colors.blue),
Container(color: Colors.red), // Overlapping
Correct:
// Use Stack for overlapping
Stack(
children: [
Container(width: 100, height: 100, color: Colors.blue),
Container(width: 100, height: 100, color: Colors.red),
],
)
Summary
Stack overlays widgets on top of each other, enabling complex UI designs. Positioned places children at specific locations, while Alignment provides default positioning. Stack is essential for badges, overlays, floating elements, and custom UI designs.
Next Steps
Did You Know?
- Later children appear on top
- Positioned places widgets relative to Stack edges
- Alignment centers non-positioned children
- Stack can be nested inside Stack
- RepaintBoundary optimizes Stack performance
- Stack is used in many Flutter widgets
- Positioned can stretch with top/bottom/left/right
- Stack supports RTL text direction