Skip to content

Splitting Space

Each split on this page can be built two ways. With flexbox, each child carries its own share. With grid, the parent lists the shares as track sizes, and children fill the tracks in order. Both give the same screenshot.

The flexbox versions use the constraint utilities, named after the constraints ratatui splits an area with:

Utility The child takes
length(n) n cells
percentage(p) p percent of the parent
ratio(a, b) a / b of the parent
fill(n) n shares of the space the others leave

Each acts along its parent's main axis, so the same utility works in a row and in a col, and none of them lets content push a child past its share. They are ordinary Styles, so you can override one part and keep the rest: length(20) | grow(1) takes 20 cells and then a share of whatever is left, and fill(1) | min_width(15) fills but never gets narrower than 15 cells.

The grid versions list the same constraints as track sizes: a plain int is that many cells, and fr(n) is n shares of the space the others leave.

Equal shares

@component
def equal_flex() -> Div:
    return Div(
        style=row,
        children=[
            Text(style=fill(1) | border, content="fill(1)"),
            Text(style=fill(1) | border, content="fill(1)"),
            Text(style=fill(1) | border, content="fill(1)"),
        ],
    )

Equal shares with flexbox

┌───────────────────────┐┌───────────────────────┐┌───────────────────────┐
│fill(1)                ││fill(1)                ││fill(1)                │
└───────────────────────┘└───────────────────────┘└───────────────────────┘
@component
def equal_grid() -> Div:
    return Div(
        style=display_grid | grid_template_columns(fr(1), fr(1), fr(1)),
        children=[
            Text(style=border, content="fr(1)"),
            Text(style=border, content="fr(1)"),
            Text(style=border, content="fr(1)"),
        ],
    )

Equal shares with grid

┌───────────────────────┐┌───────────────────────┐┌───────────────────────┐
│fr(1)                  ││fr(1)                  ││fr(1)                  │
└───────────────────────┘└───────────────────────┘└───────────────────────┘

fill(1) starts every child from nothing, whatever its content, so the whole row is shared out by the fill factors.

A fixed sidebar beside a filling pane

@component
def sidebar_flex() -> Div:
    return Div(
        style=row,
        children=[
            Text(style=length(20) | border, content="length(20)"),
            Text(style=fill(1) | border, content="fill(1)"),
        ],
    )

A sidebar with flexbox

┌──────────────────┐┌──────────────────────────────────────┐
│length(20)        ││fill(1)                               │
└──────────────────┘└──────────────────────────────────────┘
@component
def sidebar_grid() -> Div:
    return Div(
        style=display_grid | grid_template_columns(20, fr(1)),
        children=[
            Text(style=border, content="20"),
            Text(style=border, content="fr(1)"),
        ],
    )

A sidebar with grid

┌──────────────────┐┌──────────────────────────────────────┐
│20                ││fr(1)                                 │
└──────────────────┘└──────────────────────────────────────┘

Ratios

Fill factors and fractions divide the space in proportion, so 1 and 2 give the second pane twice the width of the first.

@component
def ratio_flex() -> Div:
    return Div(
        style=row,
        children=[
            Text(style=fill(1) | border, content="fill(1)"),
            Text(style=fill(2) | border, content="fill(2)"),
        ],
    )

A 1:2 split with flexbox

┌───────────────────┐┌─────────────────────────────────────┐
│fill(1)            ││fill(2)                              │
└───────────────────┘└─────────────────────────────────────┘
@component
def ratio_grid() -> Div:
    return Div(
        style=display_grid | grid_template_columns(fr(1), fr(2)),
        children=[
            Text(style=border, content="fr(1)"),
            Text(style=border, content="fr(2)"),
        ],
    )

A 1:2 split with grid

┌──────────────────┐┌──────────────────────────────────────┐
│fr(1)             ││fr(2)                                 │
└──────────────────┘└──────────────────────────────────────┘
@component
def ratio_exact() -> Div:
    return Div(
        style=row,
        children=[
            Text(style=ratio(1, 3) | border, content="ratio(1, 3)"),
            Text(style=ratio(2, 3) | border, content="ratio(2, 3)"),
        ],
    )

A 1:2 split with ratio

┌──────────────────┐┌──────────────────────────────────────┐
│ratio(1, 3)       ││ratio(2, 3)                           │
└──────────────────┘└──────────────────────────────────────┘

fill divides only the space left after each pane's own border, which a flex item can't shrink below, so its panes come out a cell away from an exact 1:2 split. Grid divides the whole width between the tracks first, and the borders go inside them. ratio and percentage are shares of the whole width too, so they split it exactly when you know every share up front.

Nested splits

With flexbox, a split inside a split is a container inside a container: here a col that fills the rest of the row, split in turn between two children. fill works on the column's vertical axis just as it does on the row's horizontal one. With grid, one parent can hold both axes, and a child spans tracks to cover the space a nested container would have.

@component
def nested_flex() -> Div:
    return Div(
        style=row,
        children=[
            Text(style=length(20) | border, content="length(20)"),
            Div(
                style=col | fill(1),
                children=[
                    Text(style=fill(1) | border, content="fill(1)"),
                    Text(style=fill(1) | border, content="fill(1)"),
                ],
            ),
        ],
    )

Nested splits with flexbox

┌──────────────────┐┌──────────────────────────────────────┐
│length(20)        ││fill(1)                               │
│                  ││                                      │
│                  │└──────────────────────────────────────┘
│                  │┌──────────────────────────────────────┐
│                  ││fill(1)                               │
│                  ││                                      │
└──────────────────┘└──────────────────────────────────────┘
@component
def nested_grid() -> Div:
    return Div(
        style=display_grid | grid_template_columns(20, fr(1)) | grid_template_rows(fr(1), fr(1)),
        children=[
            Text(
                style=grid_row(1, span(2)) | border,
                content="grid_row(\n  1, span(2),\n)",
            ),
            Text(style=border, content="fr(1)"),
            Text(style=border, content="fr(1)"),
        ],
    )

Nested splits with grid

┌──────────────────┐┌──────────────────────────────────────┐
│grid_row(         ││fr(1)                                 │
│  1, span(2),     ││                                      │
│)                 │└──────────────────────────────────────┘
│                  │┌──────────────────────────────────────┐
│                  ││fr(1)                                 │
│                  ││                                      │
└──────────────────┘└──────────────────────────────────────┘

Content wider than its share

A bare waxy.Fraction track has the same floor as a flex item: it is never narrower than its content's minimum size. In the top grid below, two lines of unwrapped text push their columns past the grid's edge. fr(1) is CSS's minmax(0, 1fr), which lowers the floor to zero, so in the bottom grid the columns split the space and the text is cut off instead.

@component
def overflow_grid() -> Div:
    return Div(
        style=col,
        children=[
            Div(
                style=display_grid | grid_template_columns(waxy.Fraction(1), waxy.Fraction(1)) | border | border_heavy,
                children=[
                    Text(style=border, content=LONG),
                    Text(style=border, content=LONG),
                ],
            ),
            Div(
                style=display_grid | grid_template_columns(fr(1), fr(1)) | border | border_heavy,
                children=[
                    Text(style=border, content=LONG),
                    Text(style=border, content=LONG),
                ],
            ),
        ],
    )

Grid columns overflowing, and fixed

┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃┌─────────────────────────────────────────────┐┌───────────
┃│a line of text much longer than half the grid││a line of t
┃└─────────────────────────────────────────────┘└───────────
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃┌───────────────────────────┐┌───────────────────────────┐┃
┃│a line of text much longer ││a line of text much longer │┃
┃└───────────────────────────┘└───────────────────────────┘┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

For the flexbox version of the same failure, see Content sets a minimum size.

Percentages and gaps

A percentage is a share of the parent's whole content box, and gaps aren't subtracted first, so two halves and a gap don't fit. percentage doesn't shrink, so in the bottom row the halves keep their size and overflow the row by the width of the gap. With shrink(1), as in the top row, each half gives up one cell to the gap instead. To split a row with gaps into equal shares, use fill as in Equal shares.

@component
def percent_gap() -> Div:
    return Div(
        style=col,
        children=[
            Div(
                style=row | gap(2) | border | border_heavy,
                children=[
                    Text(style=percentage(50) | shrink(1) | border, content="percentage(50) | shrink(1)"),
                    Text(style=percentage(50) | shrink(1) | border, content="percentage(50) | shrink(1)"),
                ],
            ),
            Div(
                style=row | gap(2) | border | border_heavy,
                children=[
                    Text(style=percentage(50) | border, content="percentage(50)"),
                    Text(style=percentage(50) | border, content="percentage(50)"),
                ],
            ),
        ],
    )

Percentages with a gap

┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃┌──────────────────────────┐  ┌──────────────────────────┐┃
┃│percentage(50) | shrink(1)│  │percentage(50) | shrink(1)│┃
┃└──────────────────────────┘  └──────────────────────────┘┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃┌───────────────────────────┐  ┌───────────────────────────
┃│percentage(50)             │  │percentage(50)             
┃└───────────────────────────┘  └───────────────────────────
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛