Overview

ZIndex is a widget that controls the layering order (z-index) of its child widget. This is a unique widget specific to Flitter (not available in Flutter itself), allowing you to determine the front-to-back order of widgets similar to CSS z-index.

Use it when you need to display specific widgets on top of others or create complex layer structures. When used together with Stack widget, you can implement more sophisticated layouts.

When to Use?

  • When you need to explicitly control widget rendering order
  • When displaying popups or overlays on top of other widgets
  • When bringing specific widgets to the front in complex layer structures
  • When moving selected elements to the top during drag and drop
  • When managing element depth in diagrams or games

Basic Usage

import { ZIndex, Container, Stack } from 'flitter';

// Basic z-index application
const LayeredWidget = Stack({
  children: [
    Container({
      width: 200,
      height: 200,
      color: '#e74c3c'
    }),
    ZIndex({
      zIndex: 10,
      child: Container({
        width: 100,
        height: 100,
        color: '#3498db'
      })
    })
  ]
});

Props

zIndex (required)

Value: number

A number representing the layer order of the widget. Higher values render in front (on top).

  • Positive numbers: Display in front of the default layer
  • 0: Default layer (baseline)
  • Negative numbers: Display behind the default layer
// Display at the very front
zIndex: 9999

// Default layer
zIndex: 0

// Display at the very back
zIndex: -1

child (optional)

Value: Widget

The child widget to which the z-index will be applied. Can only have one child.

Stacking Context

ZIndex uses a system similar to CSS stacking contexts:

Basic Rules

  1. Higher z-index displays in front of lower z-index
  2. When z-index values are equal, document order (creation order) determines precedence
  3. Nested ZIndex widgets are constrained by their parent’s stacking context

Context Isolation

// If parent zIndex is 1, child zIndex 9999 may still 
// appear behind another sibling with zIndex 2
ZIndex({
  zIndex: 1,
  child: ZIndex({
    zIndex: 9999,
    child: Container({ color: 'red' })
  })
})

Real-World Examples

Example 1: Basic Layering

import { ZIndex, Container, Stack, Positioned } from 'flitter';

const BasicLayering = Stack({
  children: [
    // Background (no z-index, default 0)
    Container({
      width: 300,
      height: 300,
      color: '#ecf0f1'
    }),
    
    // Middle layer
    Positioned({
      top: 50,
      left: 50,
      child: ZIndex({
        zIndex: 1,
        child: Container({
          width: 200,
          height: 200,
          color: '#3498db',
          child: Center({
            child: Text('Middle Layer', {
              style: { color: 'white', fontSize: 16 }
            })
          })
        })
      })
    }),
    
    // Top layer
    Positioned({
      top: 100,
      left: 100,
      child: ZIndex({
        zIndex: 10,
        child: Container({
          width: 100,
          height: 100,
          color: '#e74c3c',
          child: Center({
            child: Text('Top', {
              style: { color: 'white', fontSize: 14 }
            })
          })
        })
      })
    }),
    
    // Behind background (negative z-index)
    Positioned({
      top: 25,
      left: 25,
      child: ZIndex({
        zIndex: -1,
        child: Container({
          width: 50,
          height: 50,
          color: '#95a5a6',
          child: Center({
            child: Text('Back', {
              style: { color: 'white', fontSize: 12 }
            })
          })
        })
      })
    })
  ]
});

Example 2: Dropdown Menu

import { ZIndex, Container, Column, GestureDetector } from 'flitter';

class DropdownMenu extends StatefulWidget {
  createState() {
    return new DropdownMenuState();
  }
}

class DropdownMenuState extends State {
  isOpen = false;
  
  toggleDropdown = () => {
    this.setState(() => {
      this.isOpen = !this.isOpen;
    });
  };
  
  build() {
    return Stack({
      children: [
        // Main button
        GestureDetector({
          onTap: this.toggleDropdown,
          child: Container({
            padding: EdgeInsets.symmetric({ horizontal: 16, vertical: 12 }),
            decoration: new BoxDecoration({
              color: '#3498db',
              borderRadius: BorderRadius.circular(4)
            }),
            child: Row({
              mainAxisSize: MainAxisSize.min,
              children: [
                Text('Menu', { style: { color: 'white' } }),
                SizedBox({ width: 8 }),
                Icon(
                  this.isOpen ? Icons.arrow_drop_up : Icons.arrow_drop_down,
                  { color: 'white' }
                )
              ]
            })
          })
        }),
        
        // Dropdown menu (high z-index to display above other elements)
        if (this.isOpen)
          Positioned({
            top: 50,
            left: 0,
            child: ZIndex({
              zIndex: 1000,  // High z-index to display above all elements
              child: Container({
                width: 150,
                decoration: new BoxDecoration({
                  color: 'white',
                  borderRadius: BorderRadius.circular(4),
                  boxShadow: [
                    new BoxShadow({
                      color: 'rgba(0, 0, 0, 0.1)',
                      offset: new Offset({ x: 0, y: 2 }),
                      blurRadius: 8
                    })
                  ]
                }),
                child: Column({
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: [
                    _buildMenuItem('Option 1'),
                    _buildMenuItem('Option 2'),
                    _buildMenuItem('Option 3')
                  ]
                })
              })
            })
          })
      ]
    });
  }
  
  _buildMenuItem(text: string) {
    return GestureDetector({
      onTap: () => {
        console.log(`${text} selected`);
        this.toggleDropdown();
      },
      child: Container({
        padding: EdgeInsets.all(12),
        child: Text(text, { style: { fontSize: 14 } })
      })
    });
  }
}

Example 3: Modal Dialog

import { ZIndex, Container, Stack, Positioned, GestureDetector } from 'flitter';

const Modal = ({ isVisible, onClose, children }) => {
  if (!isVisible) return null;
  
  return Stack({
    children: [
      // Background overlay (medium z-index)
      Positioned.fill({
        child: ZIndex({
          zIndex: 100,
          child: GestureDetector({
            onTap: onClose,
            child: Container({
              color: 'rgba(0, 0, 0, 0.5)'
            })
          })
        })
      }),
      
      // Modal content (highest z-index)
      Center({
        child: ZIndex({
          zIndex: 200,
          child: Container({
            width: 300,
            padding: EdgeInsets.all(24),
            decoration: new BoxDecoration({
              color: 'white',
              borderRadius: BorderRadius.circular(12),
              boxShadow: [
                new BoxShadow({
                  color: 'rgba(0, 0, 0, 0.2)',
                  offset: new Offset({ x: 0, y: 4 }),
                  blurRadius: 16
                })
              ]
            }),
            child: children
          })
        })
      })
    ]
  });
};

Example 4: Draggable Card System

import { ZIndex, Container, GestureDetector } from 'flitter';

class DraggableCard extends StatefulWidget {
  constructor(
    private color: string,
    private initialZIndex: number,
    private title: string
  ) {
    super();
  }
  
  createState() {
    return new DraggableCardState();
  }
}

class DraggableCardState extends State {
  isDragging = false;
  position = new Offset({ x: 0, y: 0 });
  
  get currentZIndex() {
    // Use highest z-index while dragging
    return this.isDragging ? 9999 : this.widget.initialZIndex;
  }
  
  build() {
    return Positioned({
      left: this.position.x,
      top: this.position.y,
      child: ZIndex({
        zIndex: this.currentZIndex,
        child: GestureDetector({
          onPanStart: (details) => {
            this.setState(() => {
              this.isDragging = true;
            });
          },
          
          onPanUpdate: (details) => {
            this.setState(() => {
              this.position = this.position.plus(details.delta);
            });
          },
          
          onPanEnd: (details) => {
            this.setState(() => {
              this.isDragging = false;
            });
          },
          
          child: Container({
            width: 120,
            height: 80,
            decoration: new BoxDecoration({
              color: this.widget.color,
              borderRadius: BorderRadius.circular(8),
              boxShadow: this.isDragging ? [
                new BoxShadow({
                  color: 'rgba(0, 0, 0, 0.3)',
                  offset: new Offset({ x: 0, y: 8 }),
                  blurRadius: 16
                })
              ] : [
                new BoxShadow({
                  color: 'rgba(0, 0, 0, 0.1)',
                  offset: new Offset({ x: 0, y: 2 }),
                  blurRadius: 4
                })
              ]
            }),
            child: Center({
              child: Text(this.widget.title, {
                style: { 
                  color: 'white', 
                  fontWeight: 'bold',
                  fontSize: 14
                }
              })
            })
          })
        })
      })
    });
  }
}

// Usage example
const DraggableCards = Stack({
  children: [
    DraggableCard('#e74c3c', 1, 'Card 1'),
    DraggableCard('#3498db', 2, 'Card 2'),
    DraggableCard('#2ecc71', 3, 'Card 3')
  ]
});

Example 5: Tooltip System

import { ZIndex, Container, Positioned, GestureDetector } from 'flitter';

class TooltipWidget extends StatefulWidget {
  constructor(
    private tooltip: string,
    private child: Widget
  ) {
    super();
  }
  
  createState() {
    return new TooltipWidgetState();
  }
}

class TooltipWidgetState extends State {
  showTooltip = false;
  
  build() {
    return Stack({
      children: [
        // Base widget
        GestureDetector({
          onLongPress: () => this.setState(() => {
            this.showTooltip = true;
          }),
          onTapDown: () => this.setState(() => {
            this.showTooltip = false;
          }),
          child: this.widget.child
        }),
        
        // Tooltip (very high z-index)
        if (this.showTooltip)
          Positioned({
            bottom: 0,
            left: 0,
            child: ZIndex({
              zIndex: 9999,
              child: Container({
                padding: EdgeInsets.symmetric({
                  horizontal: 8,
                  vertical: 4
                }),
                decoration: new BoxDecoration({
                  color: 'rgba(0, 0, 0, 0.8)',
                  borderRadius: BorderRadius.circular(4)
                }),
                child: Text(this.widget.tooltip, {
                  style: { 
                    color: 'white', 
                    fontSize: 12 
                  }
                })
              })
            })
          })
      ]
    });
  }
}

Example 6: Game Object Layering

import { ZIndex, Container, Stack } from 'flitter';

const GameScene = Stack({
  children: [
    // Background layer (farthest back)
    ZIndex({
      zIndex: -10,
      child: Container({
        width: 800,
        height: 600,
        decoration: new BoxDecoration({
          gradient: LinearGradient({
            colors: ['#87CEEB', '#98FB98']
          })
        })
      })
    }),
    
    // Terrain layer
    ZIndex({
      zIndex: 0,
      child: TerrainWidget()
    }),
    
    // Game object layer
    ZIndex({
      zIndex: 10,
      child: PlayerWidget()
    }),
    
    // Enemy character layer
    ZIndex({
      zIndex: 15,
      child: EnemyWidget()
    }),
    
    // Particle effect layer
    ZIndex({
      zIndex: 20,
      child: ParticleEffectWidget()
    }),
    
    // UI layer (frontmost)
    ZIndex({
      zIndex: 100,
      child: GameUIWidget()
    })
  ]
});

Best Practices

  • Performance: Excessive ZIndex usage can impact rendering performance
  • Complexity: Nested stacking contexts can produce unexpected results
  • Debugging: When z-index doesn’t work as expected, check the stacking context hierarchy
  • Accessibility: Visual order and focus order may differ, so consider accessibility
  • Consistency: Maintain a consistent z-index value system throughout your project
  • Stack: When managing spatial positioning of widgets
  • Positioned: When specifying absolute positions within Stack
  • Overlay: When you need app-wide overlays
  • Transform: When expressing depth with 3D transformations