Layout System

Learn how to arrange widgets on screen using Shaft’s layout system.

Shaft provides powerful layout widgets like Column, Row, and Stack to arrange your UI. The layout algorithm works by passing constraints down and sizes up - for details, see Layout Algorithm.

Layout Widgets

Column - Vertical Layout

Arranges children vertically from top to bottom.

Column Layout

Key properties:

  • mainAxisAlignment: How to align children along vertical axis (.start, .center, .end, .spaceBetween, .spaceAround, .spaceEvenly)
  • crossAxisAlignment: How to align children horizontally (.start, .center, .end, .stretch)
  • mainAxisSize: Whether to take up all available vertical space (.max) or just what children need (.min)
  • spacing: Adds fixed spacing between children
Column(
    mainAxisAlignment: .center,
    crossAxisAlignment: .start,
    mainAxisSize: .min,
    spacing: 16
) {
    Text("First")
    Text("Second")
    Text("Third")
}

Row - Horizontal Layout

Arranges children horizontally from left to right.

Row Layout

Key properties:

  • mainAxisAlignment: How to align children along horizontal axis
  • crossAxisAlignment: How to align children vertically
  • mainAxisSize: Whether to take up all available horizontal space (.max) or just what children need (.min)
  • spacing: Adds fixed spacing between children
Row(
    mainAxisAlignment: .spaceBetween,
    crossAxisAlignment: .center,
    spacing: 8
) {
    Text("Left")
    Text("Center")
    Text("Right")
}

Stack - Layered Layout

Places children on top of each other, like layers.

Stack Layout

Key properties:

  • alignment: How to align all children (.topLeft, .center, .bottomRight, etc.)
  • fit: How to size non-positioned children (.loose, .expand)
Stack(alignment: Alignment.center) {
    // Background
    SizedBox(width: 200, height: 200)
        .decoration(.box(color: Color(0xFF_BLUE)))
    
    // Foreground
    Text("Overlay")
        .textStyle(.init(color: Color(0xFF_FFFFFF)))
}

Positioned children: Use Positioned to place children at specific locations:

Stack {
    // Background fills the stack
    SizedBox.expand()
        .decoration(.box(color: Color(0xFF_F0F0F0)))
    
    // Top-left corner
    Positioned(top: 10, left: 10) {
        Text("Top Left")
    }
    
    // Bottom-right corner
    Positioned(bottom: 10, right: 10) {
        Text("Bottom Right")
    }
}

Flexible Layouts

Expanded

Makes a child take up all remaining space in a Column or Row:

Row {
    Text("Fixed")
    
    Expanded(flex: 1) {
        Text("Takes remaining space")
            .decoration(.box(color: Color(0xFF_E0E0E0)))
    }
    
    Text("Fixed")
}

Multiple Expanded children share space based on their flex value:

Row {
    Expanded(flex: 1) {  // Gets 1/3 of space
        Text("1")
    }
    
    Expanded(flex: 2) {  // Gets 2/3 of space
        Text("2")
    }
}

Flexible

Similar to Expanded, but allows the child to be smaller than the available space:

Row {
    Flexible(flex: 1, fit: .loose) {
        Text("Can be smaller than allocated space")
    }
}

Difference:

  • Expanded: Forces child to fill space (fit: .tight)
  • Flexible: Allows child to be smaller (fit: .loose)

Using Expanded for Flexible Space

Use Expanded with an empty child to create flexible space:

Row {
    Text("Left")
    Expanded {
        SizedBox()  // Takes up remaining space
    }
    Text("Right")
}

For fixed spacing, use SizedBox:

Column {
    Text("Top")
    SizedBox(height: 20)  // Fixed 20pt spacing
    Text("Bottom")
}

Or better, use the spacing parameter:

Column(spacing: 20) {
    Text("Top")
    Text("Bottom")
}

Sizing Widgets

SizedBox

Forces a specific size:

SizedBox(width: 100, height: 50) {
    Text("100x50 box")
}

// Expand to fill parent
SizedBox.expand() {
    Text("Fills parent")
}

// Shrink to zero
SizedBox.shrink()  // Useful for conditional rendering

Constraints

Add size constraints to any widget:

Text("Constrained")
    .constrained(width: 200, height: 100)

// Or use minWidth/maxWidth
Text("Max width")
    .constrained(maxWidth: 300)

Alignment Widgets

Center

Centers its child:

Center {
    Text("I'm centered!")
}

// Or use the modifier
Text("Centered")
    .center()

Align

Positions child with fine-grained control:

Align(alignment: Alignment.topRight) {
    Text("Top Right")
}

// Or use the modifier
Text("Bottom Left")
    .align(Alignment.bottomLeft)

Common alignments:

  • Alignment.topLeft, Alignment.topCenter, Alignment.topRight
  • Alignment.centerLeft, Alignment.center, Alignment.centerRight
  • Alignment.bottomLeft, Alignment.bottomCenter, Alignment.bottomRight

Padding & Spacing

Padding

Adds space around a widget:

Text("Padded")
    .padding(.all(16))  // 16pt on all sides

Text("Custom padding")
    .padding(.only(top: 8, bottom: 8, left: 16, right: 16))

Text("Symmetric")
    .padding(.symmetric(vertical: 8, horizontal: 16))

Layout Debugging

Understanding Constraints

When layouts don’t work as expected, check:

  1. What constraints is my widget receiving?

    • Is parent giving tight or loose constraints?
    • Are constraints bounded or unbounded?
  2. What size is my widget choosing?

    • Is it respecting parent constraints?
    • Does it have intrinsic size requirements?
  3. Where is my widget being placed?

    • Check parent’s alignment settings
    • Look for Positioned, Align, or Center widgets

Next Steps

Additional Resources