Skip to content

Main Layout Structure

What you'll learn

  • What a container node is and why container nodes take over the position and size of their children.
  • The role of VBoxContainer, HBoxContainer, HSplitContainer, and Label — the four UI building blocks the rest of the game's screens compose out of.
  • The Full Rect anchor preset and why it only applies to the root of a UI subtree, not to children inside a container.
  • The difference between SIZE_FILL (default cross-axis behavior) and SIZE_EXPAND_FILL (claim leftover main-axis space), and how mixing them gives you a top-bar/content/status-bar layout for free.

How it applies

  • Resolution-independent UI by default. Container nodes plus the Full Rect anchor mean the layout described in this chapter renders identically at 1280×720, 1920×1080, and any aspect-locked size in between. The artwork or asset team does not have to ship one PSD per resolution; the player on a 4K monitor and the player on a 720p laptop see the same proportions, just scaled.
  • Test-matrix collapse. With anchors and SIZE flags driving every position, QA does not need to retest "does the status bar stay at the bottom?" at every resolution — it stays there because the engine's layout pass is doing the math, not the developer. One bug at one resolution is one bug, not N bugs.
  • Asset-team handoff. A UI artist mocking up the click-button screen in Figma can use the same 1280×720 canvas and the same regions (top bar, content area, status bar). The engineer does not have to translate "this button in the mockup is X pixels from the top" — they translate to "this button is in the top bar" and the layout system handles the rest.
  • Localized text without rework. German and Russian strings are routinely 30–60% longer than the English equivalent. Containers grow to fit their children's preferred sizes; a button labeled "Buy" in English and "Achetez maintenant" in French will just be wider in French, with no manual repositioning of neighbors.
  • Accessibility re-skinning. A player who increases system font size, or a developer adding a "large UI" toggle later, can scale a Label's theme_override_font_sizes/font_size and watch every container grow to accommodate it. Rigid pixel positioning would either truncate the larger text or require a parallel layout pass per font size.
  • Splitter-driven UX. HSplitContainer gives players a draggable divider between two regions for free — no signal handling, no drag math. Idle-game players who want to widen the upgrade list at the expense of the building list get that affordance without engineering effort.

Concepts

Container nodes

A container node is a Control subclass whose entire job is to position and size its children. The container has zero visual presence — no fill, no border, no rendered output — it is invisible scaffolding. What it does have is a layout policy: rules for where each child goes given the container's own rectangle and the children's preferences.

The single most important consequence: when a Control is a child of a container, the container takes over its position and size. The child's anchors and offsets are ignored. The child's size flags (size_flags_horizontal, size_flags_vertical) influence the layout, but the container has the final word.

This produces the trade-off that makes Godot's UI work: you give up direct pixel control of children, and in exchange you get layouts that re-flow correctly when the window resizes, fonts change size, languages swap, or content gets longer.

Example

You drop a Button directly under a CanvasLayer and set its anchor preset to Center. The button sits dead-center of the window, scaling with canvas_items stretch. You drop the same Button under a VBoxContainer. The button no longer obeys the Center anchor — the VBoxContainer decides where it goes, in row order with its siblings. Removing the button from the container restores anchor-driven positioning. The container is the difference.

VBoxContainer, HBoxContainer, HSplitContainer, Label

The four nodes M1.4 introduces, in the order you will use them:

  • VBoxContainer — vertical box. Stacks children top-to-bottom. Cross-axis (horizontal) is full width by default.
  • HBoxContainer — horizontal box. Stacks children left-to-right. Cross-axis (vertical) is full height by default. Use for top bars, button rows, inline label-and-value pairs.
  • HSplitContainer — two-pane horizontal split with a draggable divider. Hardcoded for exactly two children: a third child is silently not laid out. Useful for "main content on the left, side panel on the right" layouts where you want the player to drag the divider.
  • Label — plain text display. Not a container; a leaf Control with a text property. Use for read-only text: the resource counter, the status line, button labels (when not using a Button).

You will use them, in order, to build:

CanvasLayer
└── MainLayout (VBoxContainer)
    ├── TopBar (HBoxContainer)
    ├── ContentArea (HSplitContainer)
    └── StatusBar (Label)

The MainLayout is the trunk. Inside, three children stack vertically: a top bar, the main content area, a status bar at the bottom.

Example

The shape "top bar / content / status bar" is the same shape every desktop application has used since the 1980s — Visual Studio, Photoshop, the Windows File Explorer. Idle games inherit it because players already know how to read it. The top bar is "what I have" (resources), the content is "what I do" (clicks, upgrades, buildings), the status bar is "what just happened" (last event, current goal). Nothing about Godot dictates this layout; the convention does, and the engine's container nodes are tuned for it.

The Full Rect anchor preset

Every Control has anchors — four edge values from 0 to 1 that define its rectangle relative to its parent. The Full Rect preset(0, 0, 1, 1) with all offsets zero — makes the Control fill its parent's entire rectangle.

The catch: anchors only work when the parent is a regular Control, not a container. So you set Full Rect on the root of a UI subtree to make it claim the full window, and from there containers take over. In our tree:

  • MainLayout is a child of CanvasLayer. CanvasLayer is not a container, so MainLayout's anchors do matter — and Full Rect is what makes it claim the entire window.
  • TopBar, ContentArea, StatusBar are children of MainLayout, which is a container (VBoxContainer). Their anchors are ignored. The container decides their positions.

This is why you set the anchor preset exactly once, on MainLayout, and never touch it on the container children.

Example

You set Full Rect on TopBar thinking "I want it full width." Nothing visible changes — the anchor was already going to be ignored by MainLayout's VBoxContainer layout policy. You then set TopBar's size_flags_horizontal to SIZE_FILL (it already is, that's the default), and that is what makes TopBar claim the full width. The anchor click did nothing; the size-flag setting did the work.

SIZE_FILL vs SIZE_EXPAND_FILL

A container child's size_flags_* properties tell the parent container how that child wants to be sized:

  • SIZE_FILL (the default) — "fill the space the container gives me, exactly." On the main axis of a VBoxContainer, this means each SIZE_FILL child is exactly its minimum height; the container packs them tightly with leftover space at the bottom.
  • SIZE_EXPAND_FILL — "claim leftover space on the main axis, then fill it." On a VBoxContainer with mixed children, the SIZE_EXPAND_FILL child grows to absorb everything left after the SIZE_FILL children take their minimum heights. Multiple SIZE_EXPAND_FILL siblings split the leftover space proportionally to their stretch_ratio (default 1, so they share equally).

For our tree:

  • TopBarSIZE_FILL vertical (default). Takes its minimum height.
  • StatusBarSIZE_FILL vertical (default). Takes its minimum height.
  • ContentAreaSIZE_EXPAND_FILL vertical. Absorbs everything between TopBar and StatusBar.

Result: the top bar pins to the top, the status bar pins to the bottom, the content area takes whatever vertical space remains. As the window grows, the content area grows; as the window shrinks, the content area shrinks. The bars stay their natural sizes.

Example

Set both TopBar and StatusBar to SIZE_EXPAND_FILL. Now the leftover space is split between the three children, and the top bar/status bar grow to absorb part of it. The visual is wrong — bars should be bars, not panels. The fix is SIZE_EXPAND_FILL only on the child that is meant to grow.

Example

Set ContentArea's stretch_ratio to 2 and StatusBar's size_flags_vertical to SIZE_EXPAND_FILL with default stretch_ratio = 1. Both children expand. Of the leftover vertical space, ContentArea takes ⅔ and StatusBar takes ⅓. Useful for layouts where you want a chunky log panel at the bottom that grows with the window. Not what we want for a status bar — but the mechanism exists.

Walkthrough

You will perform these in your own Godot editor with scenes/main.tscn open from M1.3.

  1. In the Scene dock, the root CanvasLayer should be selected. Right-click it and choose Add Child Node (or click the + icon at the top of the dock). The Create New Node dialog opens.
  2. Type VBoxContainer in the search field, double-click the result. A VBoxContainer appears as a child of CanvasLayer. By default it is named VBoxContainer.
  3. With the new node selected, double-click its name in the Scene dock and rename it to MainLayout. Press Enter.
  4. With MainLayout selected, look at the 2D viewport toolbar (above the editing area). There is a Layout dropdown showing "Custom"; click it. From the menu, pick Anchor Preset → Full Rect (the icon shows a rectangle filling the parent). The MainLayout rectangle in the viewport now spans the full window outline.
  5. Right-click MainLayoutAdd Child Node → search HBoxContainer → create. Rename it to TopBar.
  6. Right-click MainLayout (not TopBar — you want a sibling, not a child) → Add Child Node → search HSplitContainer → create. Rename it to ContentArea.
  7. Right-click MainLayoutAdd Child Node → search Label → create. Rename it to StatusBar. The Scene dock now shows three children under MainLayout, in order: TopBar, ContentArea, StatusBar.
  8. With StatusBar selected, find the Inspector panel (right side by default). Find the Text property and type Status: ready. The label is too small to see in the viewport because there's no vertical space yet — that is fixed in the next step.
  9. Select ContentArea. In the Inspector, scroll to the Layout group → Container Sizing sub-group. Find Vertical (under Size Flags) and click the dropdown; pick Expand Fill (the engine name for SIZE_EXPAND_FILL). The ContentArea rectangle in the viewport snaps to the leftover space between TopBar and StatusBar.
  10. Press Ctrl+S to save the scene.
  11. Press F5 to launch. The game window opens at 1280×720. You see: a thin bar at the top (empty TopBar), a wide empty middle (the HSplitContainer with no children — its splitter is invisible until it has two children), and the text "Status: ready" pinned at the very bottom of the window.
  12. Resize the game window. The status bar stays pinned to the bottom; the top bar stays at the top; the middle grows or shrinks. Close the window.

Optional sanity check. Re-open main.tscn in a text editor (outside Godot). The file should contain a MainLayout node with anchors_preset = 15 (the numeric ID for Full Rect), three children, and ContentArea with size_flags_vertical = 3 (the numeric value for SIZE_EXPAND_FILL). If anything is missing, the in-editor change did not save — Ctrl+S in the editor and re-check.

Self-check quiz

Q1 — MainLayout (VBoxContainer) has three children in this order: TopBar (SIZE_FILL), ContentArea (SIZE_EXPAND_FILL), StatusBar (SIZE_FILL). The window is 720 pixels tall. TopBar reports a minimum height of 40 px; StatusBar reports a minimum height of 30 px. How tall is ContentArea?

A. 240 (window height ÷ 3, since there are three children). B. 650 (720 − 40 − 30, the leftover after the two fixed-height bars). C. 720 (it expands to fill the entire window, ignoring the bars). D. 0 (the splitter has no children of its own, so the container collapses).

Reveal answer

B — 650. The VBoxContainer resolves layout in this order: every SIZE_FILL child takes its minimum size on the main axis, the leftover space is split among SIZE_EXPAND_FILL children proportionally to stretch_ratio. With one expanding child, the leftover (720 − 40 − 30 = 650) is its full vertical extent. A is wrong because the container does not divide space equally; size flags decide. C is wrong because EXPAND_FILL only consumes leftover space, not all space. D confuses an empty HSplitContainer (which still occupies its parent-allotted height) with a container that has no parent allotment.

Q2 — You add a fourth node to your scene, dragging it as a child of ContentArea. The Scene dock now shows ContentArea with three children: a Panel, then a Panel, then a VBoxContainer. What does HSplitContainer do with the third child?

A. Stacks all three children vertically with two draggable dividers. B. Lays out the first two as the split panes; silently does not lay out the third (it renders at (0, 0) with size (0, 0)). C. Throws a runtime error and refuses to render the scene. D. Lays out the first two; the third floats at the parent's center.

Reveal answer

B — first two are the split panes; the third is not laid out. HSplitContainer is hardcoded for exactly two children — that is the whole point of a split container. Extras are silently ignored, sized to zero. A is wrong: there is no built-in container for an N-pane split with (N − 1) dividers; that is what nested splitters or a BoxContainer with manual separators is for. C is too dramatic — Godot is permissive about extra children; it just does not lay them out. D would be a reasonable behavior, but it is not what the engine does.

Q3 — You set Full Rect on MainLayout and it visibly fills the window. You then set Full Rect on TopBar. Nothing visible changes. Why?

A. Full Rect does not work on HBoxContainer — it only works on VBoxContainer. B. The anchor preset was applied silently and is in effect; the visual is unchanged because the window is full-rect already. C. TopBar is a child of a container (MainLayout), so its anchors are ignored — the container decides position and size. D. Full Rect sets the position but not the size; you also have to drag the corners.

Reveal answer

C — container parents override anchors. Once TopBar is a child of MainLayout (a VBoxContainer), the container's layout policy decides where TopBar goes. Setting anchors on TopBar is harmless but has no effect; the size flags (size_flags_horizontal, size_flags_vertical) are what TopBar uses to communicate sizing preferences to its parent. A is wrong: anchors are a Control feature, not specific to a container type. B is wrong: anchors are set, they just do nothing in this context. D misdescribes anchors entirely.

Integration question

Q4 — open

In M1.3 you set the root of main.tscn to CanvasLayer. In M1.4 you set Full Rect on MainLayout (the VBoxContainer directly under CanvasLayer). Why does Full Rect work as expected here, given that "anchors are ignored when the parent is a container"?

Reveal expected answer

CanvasLayer is not a container — it is a render-layer separator. So MainLayout's anchors are consulted by the engine, which is what makes Full Rect actually fill the window. Once MainLayout (a container) has children, those children's anchors stop mattering, because their parent is a container. The Full Rect rule applies at exactly one level of the tree: the topmost UI Control whose parent is not a container. Below that level, size flags do all the work.

Glossary

Glossary

container node
A Control subclass whose job is to lay out its children. Takes over each child's position and size based on the container's layout policy. Child anchors are ignored when the parent is a container.
VBoxContainer
A container that stacks its children vertically (top to bottom). Each child gets the container's full width on the cross axis (default SIZE_FILL); main-axis (vertical) sizing is driven by each child's size_flags_vertical.
HBoxContainer
A container that stacks its children horizontally (left to right). Each child gets the container's full height on the cross axis (default SIZE_FILL); main-axis (horizontal) sizing is driven by each child's size_flags_horizontal.
HSplitContainer
A container with exactly two children separated by a draggable divider. Player can resize the split ratio at runtime. Extras beyond two children are silently not laid out.
Label
A plain text-display Control. Not a container. Has a text property and theme-driven font properties. Use for any read-only text in the UI.
anchors
Four properties (anchor_left, anchor_top, anchor_right, anchor_bottom) on every Control, valued 0–1, that define the Control's rectangle relative to its parent's. Only consulted when the parent is not a container.
Full Rect preset
An anchor preset (anchor_left=0, anchor_top=0, anchor_right=1, anchor_bottom=1, all offsets 0) that makes the Control fill its parent. Set via the Layout dropdown in the 2D viewport toolbar. Effective only when the parent is not a container.
size_flags_horizontal / size_flags_vertical
Two properties on every Control telling its parent container how this child wants to be sized on each axis. Default is SIZE_FILL on both axes.
SIZE_FILL
The default size flag. The child fills its allotted main-axis space exactly — no more, no less.
SIZE_EXPAND_FILL
Size flag combining EXPAND (claim leftover main-axis space) with FILL (occupy all of it). Multiple EXPAND_FILL siblings split leftover space proportionally to their stretch_ratio (default 1). The engine's mechanism for "this child grows; everything else stays compact."
### Ask Claude (side-channel) Try one of these in a Claude Code session if a piece did not land: - `Open scenes/main.tscn and walk me through every numeric value on MainLayout — what does anchors_preset = 15 mean, what's grow_horizontal, etc.` - `Re-explain SIZE_EXPAND_FILL using a CSS flexbox analogy.` - `What's the difference between HSplitContainer and HBoxContainer with two children — when would I pick one over the other?` - `Open the Godot 4.6 class reference for VBoxContainer and find every property the chapter didn't mention (alignment, separation, vertical, etc.). Which of them might you reach for in a future layout?`