Skip to Content

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

ZStack ShowcaseOpen in new tab ↗

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

ParameterTypeRequiredDefaultDescription
childrenList<Widget>✓—The widgets to layer (first = bottom, last = top). Can include CustomPositioned.
alignmentAlignmentGeometry✗Alignment.centerHow to align non-positioned children within the stack
fitZStackFit✗ZStackFit.looseHow 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

ParameterTypeRequiredDefaultDescription
leftdouble?✗nullDistance from the left edge of the ZStack
topdouble?✗nullDistance from the top edge of the ZStack
rightdouble?✗nullDistance from the right edge of the ZStack
bottomdouble?✗nullDistance from the bottom edge of the ZStack
widthdouble?✗nullExplicit width for the positioned child
heightdouble?✗nullExplicit height for the positioned child
childWidget✓—The widget to position

Examples

Different Alignments (for non-positioned children)

ZStack( alignment: Alignment.center, children: [ Container(width: 200, height: 200, color: Colors.blue), Container(width: 100, height: 100, color: Colors.red), ], )

Positioned Children

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:

  1. Pass 1 (Non-Positioned Children): Measure non-positioned children first to determine the overall stack size (if fit is loose).

    if (!childParentData.isPositioned) { child.layout(nonPositionedConstraints, parentUsesSize: true); maxWidth = max(maxWidth, child.size.width); maxHeight = max(maxHeight, child.size.height); }
  2. Determine Stack Size: Based on fit mode and measured non-positioned children.

  3. Pass 2 (All Children): Layout all children. For CustomPositioned children, constraints are calculated based on left, top, right, bottom, width, and height properties. Non-positioned children are laid out using the stack’s alignment.

    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

FeatureZStackStack
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
PerformanceGood for simple casesOptimized 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.expand for full-size overlays or backgrounds
  • Use CustomPositioned.fill as a shortcut for CustomPositioned(left: 0, top: 0, right: 0, bottom: 0, child: ...)
  • Consider Opacity for semi-transparent overlays
  • Use IgnorePointer to make overlays non-interactive

Source Code

View the complete implementation in the GitHub repository .

Last updated on