Skip to content

VStack, HStack, and ZStack in SwiftUI | SwiftUI Bootcamp #10

Stacks are the primary layout containers in SwiftUI — nearly every screen you build will combine VStack, HStack, and ZStack in some way. After this lesson you'll understand the axis each stack controls, how their spacing and alignment parameters work, and the difference between using ZStack versus .background().

What You'll Learn

  • The three stack types and their layout axes: Vertical, Horizontal, and Depth (Z-order)
  • How ZStack and .background() differ in their effect on the parent view's size
  • The spacing and alignment parameters and how they shape stack layout

Mental Model

Stacks are like organizers for views. A VStack is a vertical stack of plates — each plate sits on top of the previous one, and the stack grows downward. An HStack is a horizontal shelf — items sit side by side and the shelf grows wider. A ZStack is a stack of glass slides under a microscope — views sit directly on top of each other, centered by default, and the whole stack is as large as the largest slide.

The sample shows something important: you can place text over a circle in two different ways — with ZStack (the text and circle are separate children of the same stack) or with .background() (the circle is attached to the text view as a background layer). Both produce the same visual, but they behave differently when the layout changes.

Detailed Explanation

VStack(alignment:spacing:) stacks views vertically. alignment controls horizontal positioning of children within the stack (.leading, .center, .trailing). spacing sets the gap between adjacent children. Default spacing uses the system value (~8pt), and passing spacing: 0 removes all gaps.

HStack(alignment:spacing:) stacks views horizontally. alignment controls vertical positioning of children (.top, .center, .bottom, .firstTextBaseline). The baseline alignments are especially useful for mixed font sizes where you want the text baselines to align.

ZStack(alignment:) layers views in Z-order — the first child is deepest, the last is on top. The alignment parameter controls where all children are anchored within the stack's bounding box. It defaults to .center. The stack's size is the bounding box of all children combined.

A key architectural decision is whether to use ZStack for layering or to use .background()/.overlay(). The difference: ZStack sizes itself to the largest child, so adding a large background shape expands the whole stack. .background() and .overlay() don't change the layout size of the primary view — they're purely cosmetic layers that respect the primary view's frame.

Code Structure

The sample is in StacksBootcamp.swift and demonstrates a VStack containing two equivalent approaches to placing text over a circle. The first uses an explicit ZStack, the second uses .background(). Comparing them side by side reveals the subtle difference between these two layering approaches.

Complete Code

StacksBootcamp.swift

swift
import SwiftUI

struct StacksBootcamp: View {
    // Vstacks -> Vertical
    // Hstacks -> Horizontal
    // Zstacks -> zIndex (back to front)
    var body: some View {
        VStack(spacing: 50) { // arranges children vertically with 50pt gaps between them
            
            ZStack { // layers children back-to-front; Circle() is behind, Text("1") is in front
                Circle()
                    .frame(width: 100, height: 100) // explicit size so the circle doesn't expand to fill
                
                Text("1")
                    .font(.title)
                    .foregroundColor(.white) // white so it's visible against the default dark circle
            }
            
            Text("1")
                .font(.title)
                .foregroundColor(.white) // same white text, but this time the circle is a background layer
                .background(
                    Circle()
                        .frame(width: 100, height: 100) // circle is visual-only; text view's size stays the same
                )
            
        }
    }
}

struct StacksBootcamp_Previews: PreviewProvider {
    static var previews: some View {
        StacksBootcamp()
    }
}

Code Walkthrough

  1. VStack(spacing: 50) — Creates the outer vertical layout with 50pt gaps. The two label groups sit 50pt apart. Without spacing: 50, they'd use the system default (~8pt) and appear much closer.
  2. First child: ZStack { ... } — The ZStack contains two independent children: the Circle at the bottom layer and the Text at the top layer. The stack's size is 100×100 (the size of the circle) because the text is smaller. Both views are centered within that 100pt box.
  3. Circle().frame(width: 100, height: 100) — The explicit frame is important here. Without it, the circle would expand to fill all space the ZStack offers, making the circle as large as the screen. Always frame shapes in ZStack unless you want them to expand.
  4. Second child: Text("1").background(Circle()...) — Visually identical to the first, but architecturally different. The Text view is the primary view; the Circle is its background. The Text controls the layout size; the circle is just paint behind it.
  5. Size comparison — The ZStack approach makes the layout item 100×100 (the circle's size). The .background() approach makes the layout item the size of the Text("1") — roughly 24×28pt. Add .background(Color.yellow) to each to see the difference clearly.

Common Mistakes

Mistake: Putting a full-screen background color in a ZStack and watching the content disappear When you place a Color.blue in a ZStack without a frame, the color expands to fill all available space — 100% of the screen. This is intentional for background screens. But if you also put content in the same ZStack, make sure the content is listed after the background color so it renders on top.

Mistake: Expecting VStack to fill the full screen widthVStack sizes itself to its widest child by default — it doesn't expand to fill available horizontal space. To make a VStack full-width, add .frame(maxWidth: .infinity) to it, or add .frame(maxWidth: .infinity) to the widest child.

Mistake: Using ZStack when .background() or .overlay() is the right tool Reach for ZStack when you have multiple independent views that should all participate in layout sizing (e.g., a full-screen background behind scrolling content). Reach for .background() or .overlay() when you want to attach decorative layers to a specific view without affecting the layout of its siblings.

Key Takeaways

  • VStack arranges vertically, HStack horizontally, ZStack by depth — they share the same alignment and spacing parameters but apply them on different axes
  • ZStack sizes to its largest child; .background() and .overlay() don't change the primary view's layout size
  • Always apply .frame() to shapes inside a ZStack to prevent them from expanding to fill the entire container

Last updated: June 27, 2026

Released under the MIT License.