Skip to content

Sizing One Box

A box that doesn't set its own size takes its content size along its parent's main axis, and stretches across it (see How layout sizes things). The utilities on this page override either.

A fixed size

width, height and size set a box's size in cells. An axis left unset keeps its default: the height(4) box below still stretches across the column.

@component
def fixed() -> Div:
    return Div(
        style=col,
        children=[
            Text(style=size(24, 4) | border, content="size(24, 4)"),
            Text(style=width(36) | border, content="width(36)"),
            Text(style=height(4) | border, content="height(4)"),
        ],
    )

Fixed sizes

┌──────────────────────┐                          
│size(24, 4)           │                          
│                      │                          
└──────────────────────┘                          
┌──────────────────────────────────┐              
│width(36)                         │              
└──────────────────────────────────┘              
┌────────────────────────────────────────────────┐
│height(4)                                       │
│                                                │
└────────────────────────────────────────────────┘

Fit to content

Along the main axis, a box already fits its content. Across it, a box stretches unless its alignment says otherwise: align_self_start (or any alignment except stretch) shrinks it to its content.

@component
def fit() -> Div:
    return Div(
        style=col,
        children=[
            Text(style=border, content="stretched by its col"),
            Text(style=align_self_start | border, content="align_self_start"),
        ],
    )

Fitting content

┌────────────────────────────────────────────────┐
│stretched by its col                            │
└────────────────────────────────────────────────┘
┌────────────────┐                                
│align_self_start│                                
└────────────────┘                                

Sizing keywords

The sizing keywords set a box's width or height from its content, whichever axis it is on:

  • min_content_width is as narrow as the content goes without breaking a word: for wrapping text, its widest word.
  • max_content_width is as wide as the content is unwrapped.
  • fit_content_width is the max-content width, capped at the space available (but never below the min-content width), as a wrapping paragraph is in CSS. It differs from max_content_width only when the content doesn't fit.
  • stretch_width fills the space available, after the box's margins.

Each has a _height counterpart. Here each box is the only child of a row:

KEYWORDS = {
    "min_content_width": min_content_width,
    "max_content_width": max_content_width,
    "fit_content_width": fit_content_width,
    "stretch_width": stretch_width,
}


@component
def content_keywords() -> Div:
    return Div(
        style=col,
        children=[
            Div(
                style=row,
                children=[Text(style=keyword | border | text_wrap_stable, content=f"{name} sizes this")],
            )
            for name, keyword in KEYWORDS.items()
        ],
    )

Sizing keywords

┌─────────────────┐                     
│min_content_width│                     
│sizes this       │                     
└─────────────────┘                     
┌────────────────────────────┐          
│max_content_width sizes this│          
└────────────────────────────┘          
┌────────────────────────────┐          
│fit_content_width sizes this│          
└────────────────────────────┘          
┌──────────────────────────────────────┐
│stretch_width sizes this              │
└──────────────────────────────────────┘

In a col, a width keyword doesn't size wrapping text correctly: taffy measures the box's height at the column's full width, before applying the keyword, and doesn't measure it again. Below, min_content_width keeps the one row its text takes at full width, so sizes this is cut off. A max_content_width box wider than the column keeps extra rows for the same reason. Put the box in a row, as above, or in a grid.

@component
def content_keywords_col() -> Div:
    return Div(
        style=col,
        children=[
            Text(style=keyword | border | text_wrap_stable, content=f"{name} sizes this")
            for name, keyword in KEYWORDS.items()
        ],
    )

Sizing keywords in a column

┌─────────────────┐                     
│min_content_width│                     
└─────────────────┘                     
┌────────────────────────────┐          
│max_content_width sizes this│          
└────────────────────────────┘          
┌────────────────────────────┐          
│fit_content_width sizes this│          
└────────────────────────────┘          
┌──────────────────────────────────────┐
│stretch_width sizes this              │
└──────────────────────────────────────┘

Fill the parent

fill(1) fills the free space along the main axis, after the other children take theirs. full_width and full_height (the same as stretch_width and stretch_height) fill the parent, after the box's margins, whatever the other children need.

@component
def filling() -> Div:
    return Div(
        style=col,
        children=[
            Text(style=border, content="content height"),
            Text(style=fill(1) | border, content="fill(1): the rest of the col"),
            Div(
                style=row,
                children=[Text(style=full_width | border, content="full_width: the whole row")],
            ),
        ],
    )

Filling the parent

┌────────────────────────────────────────────────┐
│content height                                  │
└────────────────────────────────────────────────┘
┌────────────────────────────────────────────────┐
│fill(1): the rest of the col                    │
│                                                │
└────────────────────────────────────────────────┘
┌────────────────────────────────────────────────┐
│full_width: the whole row                       │
└────────────────────────────────────────────────┘

Clamped with minimum and maximum sizes

max_width and max_height cap a box that would otherwise grow; min_width and min_height stop one shrinking. In the top row, the first box would take half the row but stops at 20 cells. In the bottom row, two 30-cell boxes don't fit in 40 cells, so both shrink, but the first stops at 24 cells and the second gives up the rest.

@component
def clamped() -> Div:
    return Div(
        style=col,
        children=[
            Div(
                style=row,
                children=[
                    Text(style=fill(1) | max_width(20) | border, content="fill(1)\nmax_width(20)"),
                    Text(style=fill(1) | border, content="fill(1)"),
                ],
            ),
            Div(
                style=row | width(40) | border | border_heavy,
                children=[
                    Text(style=width(30) | min_width(24) | border, content="width(30)\nmin_width(24)"),
                    Text(style=width(30) | border, content="width(30)"),
                ],
            ),
        ],
    )

Clamped sizes

┌──────────────────┐┌──────────────────────────────────────┐
│fill(1)           ││fill(1)                               │
│max_width(20)     ││                                      │
└──────────────────┘└──────────────────────────────────────┘
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓                    
┃┌──────────────────────┐┌────────────┐┃                    
┃│width(30)             ││width(30)   │┃                    
┃│min_width(24)         ││            │┃                    
┃└──────────────────────┘└────────────┘┃                    
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛                    

Aspect ratio

aspect_ratio sets a box's width divided by its height, so a box with a width and an aspect ratio gets its height from them. Terminal cells are about twice as tall as they are wide, so aspect_ratio(1) looks tall, and aspect_ratio(2) is the one that looks square. The parent here sets align_children_start: a box stretched across a row has its height from the row, and its aspect ratio has nothing to set.

@component
def aspect() -> Div:
    return Div(
        style=row | align_children_start,
        children=[
            Text(style=width(18) | aspect_ratio(1) | border, content="width(18)\naspect_ratio(1)"),
            Text(style=width(18) | aspect_ratio(2) | border, content="width(18)\naspect_ratio(2)"),
        ],
    )

Aspect ratios

┌────────────────┐┌────────────────┐
│width(18)       ││width(18)       │
│aspect_ratio(1) ││aspect_ratio(2) │
│                ││                │
│                ││                │
│                ││                │
│                ││                │
│                ││                │
│                │└────────────────┘
│                │                  
│                │                  
│                │                  
│                │                  
│                │                  
│                │                  
│                │                  
│                │                  
└────────────────┘                  

Border box and content box

Sizes include the border and padding by default, so a width(24) box is 24 cells wide overall. With content_box, the size applies to the content alone, and the border and padding are added outside it: here 24 cells of content, 4 of padding and 2 of border.

@component
def box_sizing() -> Div:
    return Div(
        style=col | align_children_start,
        children=[
            Text(style=width(24) | pad_x(2) | border, content="width(24)"),
            Text(style=content_box | width(24) | pad_x(2) | border, content="content_box | width(24)"),
        ],
    )

Border box and content box

┌──────────────────────┐      
│  width(24)           │      
└──────────────────────┘      
┌────────────────────────────┐
│  content_box | width(24)   │
└────────────────────────────┘