Skip to content

Stack

A stack places its children one after another along an axis. ui.VStack arranges them from top to bottom, ui.HStack from leading to trailing. Most screens are built from nested stacks: a column of sections, each a row of controls.

Both functions return the same type, ui.Stack. It sets the space between the children, how they line up on the other axis, and how the free space is shared. Like any container, it can also carry padding, a background, a border and a corner radius. To layer children on top of each other, use a ZStack instead.

Stack Stack
bold := ui.MustFont(ui.FontQuery{Family: inter.Family, Weight: ui.WeightBold})

// A toolbar: the Spacer takes the free width and pushes the button to
// the trailing edge.
toolbar := ui.HStack(
	ui.Icon(outline.Inbox).Size(20),
	ui.Text("Inbox").Font(bold),
	ui.Spacer(),
	ui.Button(ui.Text("Compose").Foreground(ui.ColorOnAccent), func() {}).
		Background(ui.ColorAccent).CornerRadius(8),
).Gap(10).Align(geom.Center).
	Padding(12).Background(ui.ColorSurface).CornerRadius(12).
	Border(ui.Border{Width: 1, Color: ui.ColorSeparator})

// Align moves the children of a VStack along X.
bar := func(w float32) gift.View {
	return ui.Box().Frame(w, 10).Background(ui.ColorAccent).CornerRadius(5)
}
aligned := func(name string, a geom.Alignment) ui.Stack {
	return ui.VStack(
		ui.Text(name).FontSize(13).Foreground(ui.ColorSecondaryLabel),
		bar(90), bar(50), bar(120),
	).Gap(6).Align(a).Padding(12).Background(ui.ColorControl).CornerRadius(10)
}

// Flex splits the free width of the row: one, two and one share.
share := func(label string, flex float32) ui.Stack {
	return ui.VStack(ui.Text(label).Foreground(ui.ColorOnAccent)).
		Align(geom.Center).Padding(10).
		Background(ui.Fade(ui.ColorAccent, 0.4+0.3*flex)).CornerRadius(8).
		Flex(flex)
}

// AlignBaseline puts labels of different sizes on one line.
reading := ui.HStack(
	ui.Text("21").Font(bold).FontSize(44),
	ui.Text("°C").FontSize(20),
	ui.Text("Living room").Foreground(ui.ColorSecondaryLabel),
).Gap(6).AlignBaseline()

return ui.VStack(
	toolbar,
	ui.HStack(
		aligned("Leading", geom.Leading).Flex(1),
		aligned("Center", geom.Center).Flex(1),
		aligned("Trailing", geom.Trailing).Flex(1),
	).Gap(8),
	ui.HStack(share("Flex 1", 1), share("Flex 2", 2), share("Flex 1", 1)).Gap(8),
	reading,
).Gap(16).Frame(500, geom.Unbounded())

Full demo on GitHub ↗ · run it with go run ./docs/gallery -demo stack

Usage

Gap and alignment

Gap sets the space between two adjacent children. It never goes before the first or after the last child. A negative gap overlaps the children by that much.

Align takes a geom.Alignment and positions the children on the cross axis. A VStack reads only its X component, an HStack only its Y component, so geom.Leading, geom.Center and geom.Trailing work for both. Without Align, children sit at the leading edge of a VStack and at the top of an HStack:

ui.VStack(
	ui.Text("Wi-Fi"),
	ui.Text("Connected").Foreground(ui.ColorSecondaryLabel),
).Gap(4)

A stack does not stretch its children on the cross axis. Each child gets its own size, and Align places it in the width (or height) the stack ended up with. A child that needs a certain extent sets it with Frame or MinWidth. A Box is the exception: it has no content of its own, so it fills the cross axis and collapses to its padding on the main axis unless you give it a Frame, a MinHeight (or MinWidth) or a Flex.

Baselines

In an HStack, AlignBaseline lines the children up on their first text baseline instead of an edge or the center. Labels of different font sizes then sit on one line, as the temperature reading in the demo does. A child that reports no baseline, such as a Box, is placed by the alignment value of Align instead, so the two combine when Align comes first: .Align(geom.Center).AlignBaseline() centers the boxes and lines up the text. An Align after AlignBaseline switches baseline alignment off again.

AlignBaseline on a VStack panics. A baseline positions a child vertically, and in a vertical stack the stacking already decides that.

Size and flex

A stack measures its children first and is as large as they are together, plus the gaps and its padding, within the limits its parent allows. It offers each inflexible child as much of the main axis as it wants, so a child’s size never depends on its siblings.

A child with a flex takes part in a second pass: the space left after the other children and the gaps is shared between all flexible children in proportion to their flex. Most views have a Flex modifier, including Stack itself. Spacer is a flexible, invisible gap with a flex of one; it pushes its neighbors apart:

ui.HStack(
	ui.Text("Inbox"),
	ui.Spacer(),
	ui.Button(ui.Text("Compose"), compose),
).Gap(8).Align(geom.Center)

A fixed sidebar next to a content area that takes the rest of the width:

ui.HStack(
	sidebar.Frame(220, geom.Unbounded()),
	content.Flex(1),
)

Flex only shares space the stack has. When the main axis is unbounded, every flexible child gets zero. That is the case for the content of a Scroll View along its scroll axis, and for a stack that sits as an inflexible child in a parent stack along the same axis: an HStack inside another HStack is offered unlimited width. If a Spacer seems to do nothing, give the stack a Frame or a Flex of its own. Spacer().MinLength(16) keeps a gap of at least 16 points even when there is no space left.

Overflow

When the children need more than the stack is allowed to take, the stack still reports the allowed size. The children keep their real sizes and positions and are not cut off; they draw past the edge. Clip(true) confines them to the bounds of the stack; the clip is a rectangle, even with a corner radius. Every overflowing node is counted in gift.Diagnostics.OverflowNodes, and a test asserts that there is none with Harness.AssertNoOverflow from gifttest.

Style

Padding goes inside the stack, between its edge and the children. Background, Border, CornerRadius and Shadow turn a stack into a panel, as the toolbar of the demo shows:

ui.VStack(ui.Text("Settings")).
	Padding(16).
	Background(ui.ColorSurface).
	Border(ui.Border{Width: 1, Color: ui.ColorSeparator}).
	CornerRadius(12)

For the common case of a raised panel, a Card does this for you. The border is drawn inside the bounds and does not change the layout.

Children and keys

ui.VStack(a, b, c) is always safe. ui.VStack(items...) hands your slice to gift without copying it, so do not change the slice after that call. When the children come from a loop, give each one a Key, so that its state stays with it when items are inserted, removed or moved:

rows := make([]gift.View, 0, len(names))
for _, n := range names {
	rows = append(rows, ui.Text(n).Key(n))
}
ui.VStack(rows...).Gap(8)

A VStack and an HStack are different view types. Turning one into the other under the same key mounts a new subtree, and the state below it starts over.

API

Constructors

ConstructorDescription
func HStack(children ...gift.View) StackHStack arranges its children from leading to trailing.
func VStack(children ...gift.View) StackVStack arranges its children from top to bottom.

Methods

MethodDescription
AlignBaseline() StackAlignBaseline lines the children up on their first text baseline instead of on an edge or the centre.
Gap(v float32) StackGap sets the space inserted between two adjacent children.

Common modifiers: Align, Background, Border, Clip, CornerRadius, Flex, Frame, Key, MaxHeight, MaxWidth, MinHeight, MinWidth, Padding, PaddingInsets, Shadow. See Modifiers.

ui.Stack on pkg.go.dev ↗

Constructors

ConstructorDescription
func Spacer() SpacerViewSpacer returns a flexible gap with a flex of one and no minimum length.

Methods

MethodDescription
MinLength(v float32) SpacerViewMinLength sets the smallest main axis extent of the spacer.

Common modifiers: Flex, Key. See Modifiers.

ui.SpacerView on pkg.go.dev ↗

Related