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.
alignmentAlignmentGeometryAlignment.centerHow to align non-positioned children within the stack
fitZStackFitZStackFit.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
childWidgetThe 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