Flutter Layouts & Responsive Design

Stack & Positioned

50 min Lesson 4 of 16

Introduction to Stack

While Row and Column arrange children linearly, the Stack widget allows you to overlap children on top of each other. Think of it as layering widgets like cards on a table -- the first child is at the bottom, and each subsequent child is painted on top.

Stack is essential for building UIs that require overlapping elements: badges on icons, text overlays on images, floating action buttons, and complex card layouts.

Note: Stack uses a coordinate system where (0, 0) is the top-left corner by default. Children are painted in order -- the first child in the list is at the back, and the last child is at the front.

Basic Stack Usage

A Stack sizes itself to contain all of its non-positioned children and then places each child at the top-left corner by default.

Simple Stack Example

Stack(
  children: <Widget>[
    Container(
      width: 200,
      height: 200,
      color: Colors.blue,
    ),
    Container(
      width: 150,
      height: 150,
      color: Colors.red,
    ),
    Container(
      width: 100,
      height: 100,
      color: Colors.yellow,
    ),
  ],
)
// Three overlapping squares: blue (back), red (middle), yellow (front)

All three containers start at the top-left corner of the Stack, creating a layered effect. The Stack’s size is determined by the largest non-positioned child (200x200 in this case).

The Positioned Widget

Positioned is used inside a Stack to control exactly where a child is placed. You specify distances from the Stack’s edges using top, bottom, left, and right properties.

Positioned Widget Example

Stack(
  children: <Widget>[
    Container(
      width: 300,
      height: 200,
      color: Colors.grey[300],
    ),
    Positioned(
      top: 10,
      left: 10,
      child: Container(
        width: 80,
        height: 80,
        color: Colors.blue,
        child: Center(child: Text('TL', style: TextStyle(color: Colors.white))),
      ),
    ),
    Positioned(
      bottom: 10,
      right: 10,
      child: Container(
        width: 80,
        height: 80,
        color: Colors.red,
        child: Center(child: Text('BR', style: TextStyle(color: Colors.white))),
      ),
    ),
  ],
)

The blue container is positioned 10px from the top and left edges. The red container is positioned 10px from the bottom and right edges. The grey container is non-positioned and determines the Stack’s size.

Tip: You can specify opposite edges simultaneously (e.g., both left and right) to stretch a Positioned child across the Stack. The child’s size is then determined by the distance between the edges.

Positioned.fill

A common need is making a Positioned child fill the entire Stack. Positioned.fill is a convenience constructor that sets all four edges to 0:

Positioned.fill Example

Stack(
  children: <Widget>[
    // Background image fills the entire Stack
    Positioned.fill(
      child: Image.network(
        'https://example.com/photo.jpg',
        fit: BoxFit.cover,
      ),
    ),
    // Dark overlay
    Positioned.fill(
      child: Container(color: Colors.black.withOpacity(0.4)),
    ),
    // Text on top
    Center(
      child: Text(
        'Welcome',
        style: TextStyle(color: Colors.white, fontSize: 32, fontWeight: FontWeight.bold),
      ),
    ),
  ],
)

Alignment in Stack

The alignment property controls where non-positioned children are placed within the Stack. By default, it is AlignmentDirectional.topStart.

Stack Alignment

// Center all non-positioned children
Stack(
  alignment: Alignment.center,
  children: <Widget>[
    Container(width: 200, height: 200, color: Colors.blue),
    Container(width: 100, height: 100, color: Colors.red),
    // Red is centered on blue
  ],
)

// Bottom-right alignment
Stack(
  alignment: Alignment.bottomRight,
  children: <Widget>[
    Container(width: 200, height: 200, color: Colors.blue),
    Container(width: 100, height: 100, color: Colors.red),
    // Red is at bottom-right of blue
  ],
)
Note: The alignment property only affects non-positioned children. Children wrapped in Positioned are placed according to their explicit top/bottom/left/right values and ignore the Stack’s alignment.

The fit Property

The fit property controls how non-positioned children are sized relative to the Stack:

  • StackFit.loose (default) -- Non-positioned children can be any size up to the Stack’s size. The Stack imposes maximum constraints from its parent but allows children to be smaller.
  • StackFit.expand -- Non-positioned children are forced to fill the entire Stack. The constraints are set to be exactly the Stack’s size.
  • StackFit.passthrough -- The constraints from the Stack’s parent are passed directly to non-positioned children without modification.

StackFit Examples

// Children can be any size (default)
SizedBox(
  width: 300,
  height: 200,
  child: Stack(
    fit: StackFit.loose,
    children: [
      Container(width: 100, height: 100, color: Colors.blue),
      // Blue is 100x100 within a 300x200 Stack
    ],
  ),
)

// Children are forced to fill the Stack
SizedBox(
  width: 300,
  height: 200,
  child: Stack(
    fit: StackFit.expand,
    children: [
      Container(color: Colors.blue),
      // Blue fills the entire 300x200 Stack
    ],
  ),
)

clipBehavior

The clipBehavior property controls what happens when Positioned children extend beyond the Stack’s boundaries:

  • Clip.hardEdge (default) -- Children that overflow the Stack’s bounds are clipped.
  • Clip.none -- Children can visually extend beyond the Stack’s bounds. Useful for shadows, tooltips, or decorative elements.
  • Clip.antiAlias -- Clipped with anti-aliasing for smoother edges.

clipBehavior Example

Stack(
  clipBehavior: Clip.none,
  children: <Widget>[
    Container(width: 200, height: 200, color: Colors.blue),
    Positioned(
      top: -20,
      right: -20,
      child: Container(
        width: 60,
        height: 60,
        decoration: BoxDecoration(
          color: Colors.red,
          shape: BoxShape.circle,
        ),
        child: Center(child: Text('3', style: TextStyle(color: Colors.white))),
      ),
    ),
  ],
)
// Red circle extends 20px beyond top-right corner of the Stack
Warning: When using Clip.none, overflowing children are still painted but do not receive hit-test events outside the Stack’s bounds. This means users cannot tap on the overflowing portion of a button or interactive widget.

IndexedStack

IndexedStack is a variant of Stack that only displays one child at a time based on an index. All children are laid out and maintain their state, but only the child at the specified index is visible.

IndexedStack Example

class TabView extends StatefulWidget {
  @override
  State<TabView> createState() => _TabViewState();
}

class _TabViewState extends State<TabView> {
  int _selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Expanded(
          child: IndexedStack(
            index: _selectedIndex,
            children: <Widget>[
              Center(child: Text('Home Page', style: TextStyle(fontSize: 24))),
              Center(child: Text('Search Page', style: TextStyle(fontSize: 24))),
              Center(child: Text('Profile Page', style: TextStyle(fontSize: 24))),
            ],
          ),
        ),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: [
            IconButton(icon: Icon(Icons.home), onPressed: () => setState(() => _selectedIndex = 0)),
            IconButton(icon: Icon(Icons.search), onPressed: () => setState(() => _selectedIndex = 1)),
            IconButton(icon: Icon(Icons.person), onPressed: () => setState(() => _selectedIndex = 2)),
          ],
        ),
      ],
    );
  }
}
Tip: IndexedStack preserves the state of all children, making it ideal for tab-based navigation where you want to keep the scroll position or form data when switching tabs. However, be aware that all children are built in memory, so avoid using it with too many heavy widgets.

Practical Example: Badge on Icon

A notification badge on an icon is one of the most common Stack patterns:

Notification Badge

Stack(
  clipBehavior: Clip.none,
  children: <Widget>[
    Icon(Icons.notifications, size: 32, color: Colors.grey[700]),
    Positioned(
      top: -5,
      right: -5,
      child: Container(
        padding: EdgeInsets.all(4),
        decoration: BoxDecoration(
          color: Colors.red,
          shape: BoxShape.circle,
        ),
        constraints: BoxConstraints(minWidth: 20, minHeight: 20),
        child: Text(
          '5',
          style: TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold),
          textAlign: TextAlign.center,
        ),
      ),
    ),
  ],
)

Practical Example: Overlay Text on Image

A hero image with text overlay, commonly used in cards and banners:

Image with Text Overlay

ClipRRect(
  borderRadius: BorderRadius.circular(12),
  child: Stack(
    children: <Widget>[
      // Background image
      Image.network(
        'https://example.com/city.jpg',
        width: double.infinity,
        height: 200,
        fit: BoxFit.cover,
      ),
      // Gradient overlay
      Positioned.fill(
        child: Container(
          decoration: BoxDecoration(
            gradient: LinearGradient(
              begin: Alignment.topCenter,
              end: Alignment.bottomCenter,
              colors: [Colors.transparent, Colors.black.withOpacity(0.7)],
            ),
          ),
        ),
      ),
      // Text content
      Positioned(
        bottom: 16,
        left: 16,
        right: 16,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('New York City', style: TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold)),
            SizedBox(height: 4),
            Text('Explore the city that never sleeps', style: TextStyle(color: Colors.white70, fontSize: 14)),
          ],
        ),
      ),
    ],
  ),
)

Practical Example: Floating Labels

A text field with a custom floating label positioned outside the field boundary:

Custom Floating Label

Stack(
  clipBehavior: Clip.none,
  children: <Widget>[
    Container(
      padding: EdgeInsets.symmetric(horizontal: 12, vertical: 16),
      decoration: BoxDecoration(
        border: Border.all(color: Colors.blue, width: 2),
        borderRadius: BorderRadius.circular(8),
      ),
      child: Row(
        children: [
          Icon(Icons.email, color: Colors.blue),
          SizedBox(width: 8),
          Expanded(
            child: Text('user@example.com', style: TextStyle(fontSize: 16)),
          ),
        ],
      ),
    ),
    Positioned(
      top: -10,
      left: 12,
      child: Container(
        padding: EdgeInsets.symmetric(horizontal: 8),
        color: Colors.white,
        child: Text('Email', style: TextStyle(color: Colors.blue, fontSize: 12)),
      ),
    ),
  ],
)

Practical Example: Custom Card with Overlay

A product card with a discount badge, favorite button, and overlay content:

Product Card with Overlays

Card(
  clipBehavior: Clip.antiAlias,
  shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
  child: Stack(
    children: <Widget>[
      // Product image
      Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Image.network(
            'https://example.com/product.jpg',
            width: double.infinity,
            height: 180,
            fit: BoxFit.cover,
          ),
          Padding(
            padding: EdgeInsets.all(12),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text('Premium Headphones', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
                SizedBox(height: 4),
                Text('\$99.99', style: TextStyle(fontSize: 18, color: Colors.green, fontWeight: FontWeight.bold)),
              ],
            ),
          ),
        ],
      ),
      // Discount badge (top-left)
      Positioned(
        top: 8,
        left: 8,
        child: Container(
          padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
          decoration: BoxDecoration(
            color: Colors.red,
            borderRadius: BorderRadius.circular(4),
          ),
          child: Text('-30%', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12)),
        ),
      ),
      // Favorite button (top-right)
      Positioned(
        top: 8,
        right: 8,
        child: CircleAvatar(
          backgroundColor: Colors.white,
          radius: 18,
          child: Icon(Icons.favorite_border, color: Colors.red, size: 20),
        ),
      ),
    ],
  ),
)

Summary

  • Stack overlaps children on top of each other -- first child at the back, last at the front.
  • Positioned places a child at exact coordinates within the Stack using top, bottom, left, and right.
  • Positioned.fill stretches a child to cover the entire Stack.
  • The alignment property controls where non-positioned children are placed.
  • StackFit controls sizing: loose (children choose size), expand (children fill Stack), passthrough (parent constraints passed through).
  • clipBehavior controls overflow: Clip.none allows visible overflow but blocks hit-testing outside bounds.
  • IndexedStack shows only one child at a time while preserving all children’s state.
  • Common patterns: badges, image overlays, floating labels, and complex card layouts.