ZStack
A layered stack layout component that positions children on top of each other with configurable alignment and absolute positioning.
Overview
ZStack provides z-ordering (layering) and absolute positioning capabilities using a custom RenderBox implementation. It demonstrates how Flutter’s layered layout works without relying on the native Stack widget, now with support for CustomPositioned.
Primitives Used: Custom RenderBox, CustomMultiChildLayout, ParentDataWidget
Basic Usage
import 'package:primitive_ui/primitive_ui.dart';
ZStack(
alignment: Alignment.center,
children: [
Container(width: 200, height: 200, color: Colors.blue),
CustomPositioned(
right: 10,
top: 10,
child: Container(width: 50, height: 50, color: Colors.red),
),
],
)Children are painted in order - the last child appears on top.
API Reference
ZStack Constructor
ZStack({
Key? key,
required List<Widget> children,
AlignmentGeometry alignment = Alignment.center,
ZStackFit fit = ZStackFit.loose,
})ZStack Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
children | List<Widget> | ✓ | — | The widgets to layer (first = bottom, last = top). Can include CustomPositioned. |
alignment | AlignmentGeometry | ✗ | Alignment.center | How to align non-positioned children within the stack |
fit | ZStackFit | ✗ | ZStackFit.loose | How non-positioned children should be sized to fit the stack |
ZStackFit Enum
enum ZStackFit {
loose, // Children use relaxed constraints
expand, // Children expanded to fill stack size
passthrough, // Children get exact stack constraints
}CustomPositioned Widget
Allows absolute positioning of children within a ZStack.
Constructor
CustomPositioned({
Key? key,
double? left,
double? top,
double? right,
double? bottom,
double? width,
double? height,
required Widget child,
})Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
left | double? | ✗ | null | Distance from the left edge of the ZStack |
top | double? | ✗ | null | Distance from the top edge of the ZStack |
right | double? | ✗ | null | Distance from the right edge of the ZStack |
bottom | double? | ✗ | null | Distance from the bottom edge of the ZStack |
width | double? | ✗ | null | Explicit width for the positioned child |
height | double? | ✗ | null | Explicit height for the positioned child |
child | Widget | ✓ | — | The widget to position |
Examples
Different Alignments (for non-positioned children)
Center
ZStack(
alignment: Alignment.center,
children: [
Container(width: 200, height: 200, color: Colors.blue),
Container(width: 100, height: 100, color: Colors.red),
],
)Positioned Children
Top Right
ZStack(
children: [
Container(width: 200, height: 200, color: Colors.blue[100]),
CustomPositioned(
right: 10,
top: 10,
child: Container(width: 50, height: 50, color: Colors.red),
),
],
)Badge Example
ZStack(
alignment: Alignment.topRight,
children: [
// Base icon
Icon(Icons.notifications, size: 48),
// Badge overlay
CustomPositioned(
right: 0,
top: 0,
child: Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
child: Center(
child: Text(
'3',
style: TextStyle(color: Colors.white, fontSize: 12),
),
),
),
),
],
)Image with Overlay
ZStack(
fit: ZStackFit.expand,
children: [
// Background image
Image.network(
'https://picsum.photos/id/237/200/300',
fit: BoxFit.cover,
),
// Dark overlay
Container(
color: Colors.black.withOpacity(0.3),
),
// Text on top
Center(
child: Text(
'Welcome',
style: TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.bold,
),
),
),
],
)Implementation Details
Layout Algorithm
The ZStack layout process now supports positioned children with a two-pass approach:
-
Pass 1 (Non-Positioned Children): Measure non-positioned children first to determine the overall stack size (if
fitisloose).if (!childParentData.isPositioned) { child.layout(nonPositionedConstraints, parentUsesSize: true); maxWidth = max(maxWidth, child.size.width); maxHeight = max(maxHeight, child.size.height); } -
Determine Stack Size: Based on
fitmode and measured non-positioned children. -
Pass 2 (All Children): Layout all children. For
CustomPositionedchildren, constraints are calculated based onleft,top,right,bottom,width, andheightproperties. Non-positioned children are laid out using the stack’salignment.if (childParentData.isPositioned) { // Calculate specific constraints and offset for positioned child } else { // Position non-positioned child based on ZStack's alignment }
Parent Data
ZStack uses _ZStackParentData to store positioning properties (left, top, right, bottom, width, height) for each child, populated by CustomPositioned.
Alignment Resolution
ZStack properly resolves alignment for non-positioned children based on textDirection:
final resolvedAlignment = alignment.resolve(textDirection);Paint Order
Children are painted in the order they appear in the list:
// First child painted first (bottom layer)
// Last child painted last (top layer)
defaultPaint(context, offset);Intrinsic Dimensions
ZStack calculates intrinsic dimensions based on its non-positioned children:
double computeMinIntrinsicWidth(double height) {
return computeMinIntrinsicWidthFromChildren(firstChild, height);
}Common Patterns
Layered Background Effect
ZStack(
fit: ZStackFit.expand,
children: [
// Bottom layer: gradient
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue, Colors.purple],
),
),
),
// Middle layer: pattern
CustomPositioned.fill(
child: Opacity(
opacity: 0.1,
child: Image.network('https://picsum.photos/seed/picsum/200/300', fit: BoxFit.cover),
),
),
// Top layer: content
Center(child: Text('Content')),
],
)Loading Overlay
ZStack(
children: [
// Main content
Container(height: 200, color: Colors.white, child: Center(child: Text('My Content'))),
// Loading overlay (conditional)
if (isLoading)
CustomPositioned.fill(
child: Container(
color: Colors.black54,
child: Center(
child: CircularProgressIndicator(),
),
),
),
],
)Multiple Badges
ZStack(
children: [
// Base widget
Container(width: 100, height: 100, color: Colors.blue),
// Top-right badge
CustomPositioned(
right: 0,
top: 0,
child: Container(
width: 20,
height: 20,
decoration: BoxDecoration(color: Colors.red, shape: BoxShape.circle),
child: Center(child: Text('N', style: TextStyle(color: Colors.white, fontSize: 10))),
),
),
// Bottom-left badge
CustomPositioned(
left: 0,
bottom: 0,
child: Container(
width: 20,
height: 20,
decoration: BoxDecoration(color: Colors.green, shape: BoxShape.circle),
child: Center(child: Text('A', style: TextStyle(color: Colors.white, fontSize: 10))),
),
),
],
)Differences from Stack
| Feature | ZStack | Stack |
|---|---|---|
| Positioned | ✅ Supported (via CustomPositioned) | ✅ Supported |
| Fit modes | ✅ Per-stack | ✅ Per-child |
| Clip behavior | ❌ No built-in | ✅ Built-in |
| Alignment | ✅ Supported | ✅ Supported |
| Intrinsic sizing | ✅ Implemented | ✅ Implemented |
| Performance | Good for simple cases | Optimized for all cases |
Best Practices
Use ZStack When:
- You need simple layering with alignment
- You need absolute positioning for children
- Learning how layering works
- Building educational projects
Use Stack Instead When:
- Complex clipping behavior is needed
- Different fit modes per child are required
Tips:
- Remember paint order: last child is on top
- Use
ZStackFit.expandfor full-size overlays or backgrounds - Use
CustomPositioned.fillas a shortcut forCustomPositioned(left: 0, top: 0, right: 0, bottom: 0, child: ...) - Consider
Opacityfor semi-transparent overlays - Use
IgnorePointerto make overlays non-interactive
Related Components
- VStack - For vertical layouts
- PrimitiveCard - Common ZStack child
Source Code
View the complete implementation in the GitHub repository .