Arranging Widgets#

bootstack arranges widgets with container widgets. A container both holds its children and decides how they are placed; a widget is parented to the nearest enclosing container — the with block it is created in. There is no parent= wiring in the common case and no manual coordinates.

Three containers do most of the work:

  • Column stacks its children top to bottom.

  • Row stacks them left to right.

  • Grid places them in rows and columns.

Nest them to build any layout — a toolbar (Row) above a body (Column), a form laid out in a Grid, and so on.

with bs.Column(gap=8, padding=16, horizontal_items="stretch"):
    bs.Label("Name")
    bs.TextField()
    with bs.Row(gap=8):
        bs.Button("OK", grow=True)
        bs.Button("Cancel")

The screen axes#

Placement is described against the screen, not the flow direction: the horizontal axis runs left–right and the vertical axis runs top–bottom, in every container. You never flip axes when you switch from a Row to a Column — horizontal always means horizontal.

Each axis takes a concrete edge value:

  • horizontal: 'left' · 'center' · 'right' · 'stretch'

  • vertical: 'top' · 'center' · 'bottom' · 'stretch'

'stretch' makes a child fill that axis; the edge values size it to its content and pin it to that edge.

Self versus items#

Two layers of placement share these words, distinguished by a suffix:

  • Bare keys (horizontal, vertical, grow) place the widget itself within its parent.

  • The ``_items`` keys (horizontal_items, vertical_items, grow_items) set the default for a container’s children. Any child overrides the default with its own bare key.

Because a container is also a widget, it can set both — how it sits in its parent and how it arranges its own children:

# This Column fills its parent's width (self), and stretches every
# child to its own width (items).
with bs.Column(horizontal="stretch", horizontal_items="stretch", gap=8):
    bs.TextField()
    bs.TextField()

Arranging the group#

On a stack’s stacking axis (vertical for a Column, horizontal for a Row), the _items key arranges the whole group: cluster it at an edge ('top'/'center'/'bottom' or 'left'/'center'/'right') or distribute it with 'space-between', 'space-around', or 'space-evenly'.

# Push a group to the right end of a toolbar
with bs.Row(horizontal_items="right", gap=8):
    bs.Button("Cancel")
    bs.Button("Save", accent="primary")
The six horizontal_items modes in a Row — light theme The six horizontal_items modes in a Row — dark theme

On the cross axis, the _items key aligns each child — 'center' (the default) lines up mixed-height widgets, 'stretch' makes them fill, and 'left'/'right' (or 'top'/'bottom') pin to an edge:

with bs.Row(gap=8, vertical_items="center"):   # label and field share a center line
    bs.Label("Search:")
    bs.TextField(grow=True)

Growing children#

grow lets a child claim and fill the leftover space along the stacking axis while its siblings keep their natural size — the equivalent of a flexible region. grow=True takes one share, grow=N takes N shares.

with bs.Column(gap=6, height=150, horizontal_items="stretch"):
    bs.Button("Header")
    bs.Button("Content", grow=True)   # fills the leftover height
    bs.Button("Footer")               # stays its natural size
A middle child growing to fill a column — light theme A middle child growing to fill a column — dark theme

Note

Data and canvas widgetsListView, DataTable, Tree, Gallery, Carousel, Picture, CodeEditor — have no natural size of their own and collapse without a directive. Give them grow=True (and horizontal="stretch" in a Column) to claim space.

For a fixed ratio rather than a single flexible child, set weights= on the container instead: weights=[1, 2, 1] sizes three children 1:2:1 across the stacking axis.

Spacer#

A Spacer is a composable break that pushes its neighbors apart. Where horizontal_items/vertical_items move the whole group, a Spacer opens a gap at one point — ideal for clustered toolbars and pinned footers, with no nesting:

with bs.Row(gap=4):
    bs.Button("New"); bs.Button("Open")
    bs.Spacer()                 # everything after is pushed to the right
    bs.Button("Settings")
A spacer splitting a toolbar into two groups — light theme A spacer splitting a toolbar into two groups — dark theme

Spacer(size=N) is instead a fixed gap, and Spacer(weight=N) shares slack with other spacers in proportion.

Spacing: padding versus gap versus margin#

Three knobs control whitespace, and keeping them straight removes most layout fiddling:

  • padding — space inside a container, between its edge and its children. Set it on the container.

  • gap — space between a container’s children. Set it on the container.

  • margin (and margin_x / margin_y) — extra space around one child, overriding the container’s gap for that widget. Set it on the child.

with bs.Column(padding=16, gap=8):     # 16px inset, 8px between rows
    bs.Label("Settings", font="heading-md")
    bs.Switch("Dark mode", margin_y=12)   # extra breathing room around this one

When a widget needs different spacing on each axis, use margin_x for left/right and margin_y for top/bottom; each also accepts a (before, after) pair for asymmetric spacing.

Grids#

Grid places children in cells. The columns= argument sets the column sizing: a list of weights ([1, 2] makes the second column twice as wide), or the shorthand columns=3 for three equal columns ("auto" sizes a column to its content, "120px" fixes a pixel width). Children flow into cells automatically, or you can pin them with row=/column= and span with rowspan=/columnspan=.

with bs.Grid(columns=["auto", 1], gap=8):
    bs.Label("Name");  bs.TextField()
    bs.Label("Email"); bs.TextField()

In-cell alignment uses the same axis words: horizontal_items and vertical_items set how every child sits in its cell (both default to 'stretch', so children fill their cells), overridable per child with horizontal/vertical.

with bs.Grid(columns=[1, 1, 1], gap=8, vertical_items="center"):
    for label in ("Equal", "Weight", "Columns"):
        bs.Button(label)
Grid column weights — light theme Grid column weights — dark theme

Bordered containers#

For a visually grouped region, use Card (an elevated panel) or GroupBox (a labeled border). Both lay out their children like a Column by default (pass layout="row" or layout="grid" to switch). Give them padding — a border drawn flush against its content looks cramped:

with bs.GroupBox("Account", padding=16, gap=8, horizontal_items="stretch"):
    bs.TextField(label="Username")
    bs.PasswordField(label="Password")

Common quirks#

A handful of behaviors trip people up the first time:

  • Children center on the cross axis by default. A Column centers its children horizontally and a Row centers them vertically, so a label and a field in a Row line up on a center line with no kwarg. For a full-width form column set horizontal_items="stretch"; to left-align, horizontal_items="left". The stacking axis still starts at the top/left — use horizontal_items / vertical_items or a Spacer to move the whole group.

  • Input fields top-align in a Row instead. A field grows taller the moment it carries a validation rule, because it reserves a row for the message. Centering would then drop its unvalidated neighbors by half a message row, so fields ask for 'top' on their own and a row of them lines up regardless of which ones are validated. Passing vertical_items yourself applies to every child, fields included.

  • Data and canvas widgets collapse without `grow`. A ListView or Tree with no grow/stretch shrinks to nothing — give it grow=True (and horizontal="stretch" in a Column).

  • Setting `width=`/`height=` on a stack fixes that size. Use grow and horizontal/vertical="stretch" for the axes you still want to flex rather than a hard size.

  • A bordered container needs `padding`. Without it, Card / GroupBox draw their border directly against the content.

  • Legacy placement kwargs raise. Passing fill=/expand=/anchor=/ sticky= to a child raises a clear error instead of silently collapsing it. The replacement depends on how the container places its children: in a Row or Column, use grow / horizontal / vertical; in a grid cell — a Grid child, a page or pane, or any container built with layout="grid" — use horizontal / vertical and weight the row or column on the container, since grow does not apply there.

Placement options reference#

Every widget accepts these self-placement options as keyword arguments. Which ones apply depends on the parent container.

All widgets accept self-placement kwargs via **kwargs. The parent container determines which options apply — Column / Row parents use the layout kwargs below, grid-based parents use grid kwargs.

Column (vertical layout)

Used inside a Column, App, or any other container with a column layout. Children are arranged top-to-bottom, so horizontal aligns each child across the width and grow shares the vertical space. (vertical does not apply — the order of the children sets their top-to-bottom position.)

horizontal

Cross-axis placement of the widget: 'left', 'center', 'right', or 'stretch' to fill the available width.

grow

Claim and fill a share of the leftover vertical space (the layout direction). True or False.

margin

External spacing in pixels. Accepts an integer (equal on all sides), a 2-tuple (horizontal, vertical), or a 4-tuple (left, top, right, bottom).

margin_x

Horizontal external spacing (left and right). Accepts an integer or a 2-tuple (left, right) for asymmetric spacing. Overrides the horizontal component of margin=.

margin_y

Vertical external spacing (top and bottom). Accepts an integer or a 2-tuple (top, bottom) for asymmetric spacing. Overrides the vertical component of margin=.

Row (horizontal layout)

Used inside a Row or any other container with a row layout. Children are arranged left-to-right, so vertical aligns each child across the height and grow shares the horizontal space. (horizontal does not apply — the order of the children sets their left-to-right position.)

vertical

Cross-axis placement of the widget: 'top', 'center', 'bottom', or 'stretch' to fill the available height.

grow

Claim and fill a share of the leftover horizontal space (the layout direction). True or False.

margin

External spacing in pixels. Accepts an integer (equal on all sides), a 2-tuple (horizontal, vertical), or a 4-tuple (left, top, right, bottom).

margin_x

Horizontal external spacing (left and right). Accepts an integer or a 2-tuple (left, right) for asymmetric spacing. Overrides the horizontal component of margin=.

margin_y

Vertical external spacing (top and bottom). Accepts an integer or a 2-tuple (top, bottom) for asymmetric spacing. Overrides the vertical component of margin=.

Grid

Used inside a Grid container.

row / column

Zero-based row and column indices.

rowspan / columnspan

Number of rows or columns to span.

horizontal

Horizontal placement within the grid cell: 'left', 'center', 'right', or 'stretch' to fill the cell width.

vertical

Vertical placement within the grid cell: 'top', 'center', 'bottom', or 'stretch' to fill the cell height.

margin

External spacing in pixels. Accepts an integer, a 2-tuple (horizontal, vertical), or a 4-tuple (left, top, right, bottom).

margin_x

Horizontal external spacing. Accepts an integer or (left, right).

margin_y

Vertical external spacing. Accepts an integer or (top, bottom).

See also#