Stack and Positioned: Flutter best practices explained
Most Flutter layouts flow in a single direction—rows and columns arrange widgets predictably along axes. But what happens when you need to layer widgets on top of each other, or place a notification badge in the exact corner of a button? Stack and Positioned: Flutter best practices explained shows how to break free from linear layouts and build complex overlapping UIs that feel native. This pattern is fundamental to modern app design, whether you're building badges, tooltips, image overlays, or custom animations.
Understanding Stack: layering widgets on top of each other
At its core, Stack is a widget that positions its children on top of one another. Unlike Row or Column, which arrange children in sequence, Stack treats each child as a layer in a z-axis perspective. The first child you add sits at the bottom; each subsequent child appears above the previous one.
Stack works without any positioning constraints by default. All children are laid out as if they occupy the full space of the Stack, and they overlap completely unless you explicitly position them. This is useful for backgrounds, decorative layers, or when you want children to share the same space but respond to gestures independently.
Stack(
children: [
Container(
width: 200,
height: 200,
color: Colors.blue,
),
Container(
width: 150,
height: 150,
color: Colors.red,
),
Container(
width: 100,
height: 100,
color: Colors.green,
),
],
);In this example, the green container sits on top of the red, which sits on top of the blue. All three are centered at the same point because Stack uses the alignment of its children (default is top-left). This simple structure becomes powerful when you combine it with Positioned.
Stack also accepts an alignment parameter that controls how non-positioned children are aligned. If you set alignment: Alignment.center, all unpositoned children will center themselves. This is useful for backgrounds or overlays where you want consistent placement without reaching for Positioned.
Positioned: placing children by edges
While Stack layers widgets, Positioned is the companion widget that gives you pixel-perfect control over placement. Positioned takes a Stack child and lets you specify distances from the top, bottom, left, right, width, and height—exactly like absolute positioning in CSS.
When you wrap a Stack child in Positioned, that child breaks out of normal alignment rules and gets placed according to your constraints. You can specify any combination of top, bottom, left, right, width, and height. Flutter will calculate the layout based on these constraints, respecting the Stack's own dimensions.
Stack(
children: [
Container(
width: 300,
height: 300,
color: Colors.grey[200],
),
Positioned(
top: 20,
left: 20,
child: Container(
width: 50,
height: 50,
color: Colors.blue,
),
),
Positioned(
bottom: 30,
right: 30,
child: Container(
width: 60,
height: 60,
color: Colors.red,
),
),
],
);Here, the blue container sits 20 pixels from the top-left, and the red container sits 30 pixels from the bottom-right. The grey background (unpositoned) fills the entire Stack. This gives you the flexibility to place elements exactly where you need them, making Positioned essential for complex layouts.
Be mindful of over-constraining. If you specify top: 10, bottom: 10, height: 50, Flutter cannot satisfy all three constraints. The widget will throw an error during layout. Generally, provide either two opposing edges (top + bottom) or one edge plus a dimension (top + height).
Great for badges and overlays
One of the most common real-world uses for Stack and Positioned is building badges—those small notification indicators you see on app icons or profile pictures. A badge sits on top of another widget and draws attention to new content.
Stack(
children: [
CircleAvatar(
radius: 40,
backgroundImage: NetworkImage('https://example.com/avatar.jpg'),
),
Positioned(
right: 0,
bottom: 0,
child: Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
child: Center(
child: Text(
'3',
style: TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold),
),
),
),
),
],
);This pattern stacks a circular avatar with a small red badge showing a count. The badge is positioned at the bottom-right corner and has a white border to separate it visually from the avatar. This is production-ready code you'll see across e-commerce apps, messaging platforms, and social networks.
Overlays are another common application. When you need to darken an image, add a gradient, or display loading indicators on top of content, Stack and Positioned let you layer semi-transparent containers or other widgets precisely. For example, a shopping app might layer a "Sale" banner across a product image:
Stack(
children: [
Image.network('https://example.com/product.jpg'),
Positioned(
top: 10,
left: 10,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(4),
),
child: Text(
'SALE',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
),
),
],
);The flexibility of Stack and Positioned makes these patterns straightforward. You're not trying to hack a Row or Column into doing something it wasn't designed for; you're using the right tool for layered layouts.
Minding hit testing on overlapping widgets
When widgets overlap, you need to think carefully about touch interactions. Hit testing determines which widget receives a tap or gesture. By default, Flutter uses a first-come-first-served approach: the topmost (last-added) widget that can handle the gesture gets it. Widgets beneath may never receive taps.
This is often the desired behavior. If you have a button on top of a background, you want taps to go to the button, not the background. But sometimes overlapping widgets both need to be interactive, and you must be intentional about ordering and hit testing behavior.
Use IgnorePointer to prevent a widget from consuming gestures:
Stack(
children: [
GestureDetector(
onTap: () => print('Background tapped'),
child: Container(
width: 200,
height: 200,
color: Colors.blue,
),
),
Positioned(
top: 50,
left: 50,
child: IgnorePointer(
child: Container(
width: 100,
height: 100,
color: Colors.red,
),
),
),
],
);Here, the red container is decorative and transparent to gestures. Taps pass through it to the blue background below. Without IgnorePointer, taps on the red container would be swallowed, and the background tap handler never fires.
Conversely, use Opacity or Visibility if you want a widget to be invisible but still consume gestures. Or use AbsorbPointer explicitly if you want to block gestures from reaching children below. The key is being deliberate: overlapping widgets create ambiguity, and you must resolve it in code.
In production apps, we often combine Stack with GestureDetector or InkWell on multiple children, making careful hit testing order critical. Test your overlays on real devices and with real fingers, not just in the simulator, because the touch behavior and visual spacing may feel different.
Common mistakes
- Over-constraining Positioned children: Specifying all four edges (top, bottom, left, right) plus width and height leads to layout conflicts. Provide only the constraints you need—typically two opposing edges or one edge plus a dimension.
- Ignoring Stack sizing: If you don't constrain the Stack itself (e.g., by wrapping it in a Container with fixed dimensions or SizedBox), it will try to expand to fill available space. Unpositoned children will size the Stack, potentially causing unexpected layouts.
- Forgetting hit testing implications: Placing an interactive widget on top of another interactive widget means the top widget captures all taps. If that's not your intent, use
IgnorePointeror reorder children to ensure the right widget gets gestures. - Not accounting for overflow: Positioned children that extend outside the Stack bounds may be clipped depending on the
overflowparameter. The default isOverflow.clip. UseOverflow.visibleif you need children to render outside the Stack's boundaries, but be aware this can cause visual glitches or performance issues. - Misaligning unpositoned children: When mixing Positioned and unpositoned children, remember that unpositoned children are aligned according to the Stack's
alignmentparameter. If you don't set it explicitly, it defaults to top-left, which may not be what you expect.
Key takeaways
- Stack layers widgets on top of each other along a z-axis, making it ideal for overlapping UIs.
- Positioned is a Stack child that breaks out of normal alignment and lets you place children by specifying edges (top, bottom, left, right) and dimensions.
- Badges, image overlays, and modal dialogs are classic use cases for Stack and Positioned combinations.
- Always consider hit testing: overlapping widgets create ambiguity about which one should respond to taps. Use
IgnorePointer,AbsorbPointer, or child ordering to clarify intent. - Avoid over-constraining Positioned children and be aware of how Stack handles sizing when mixing Positioned and unpositoned children.
- Test overlapping layouts on real devices to catch spacing, clipping, and touch response issues that the simulator may hide.






Comments
Loading comments…