Skip to content

Spacing and Borders

The box model

Counterweight's layout follows the CSS box model. Every element's box is four nested rectangles:

  • Content: where the element's content goes. A Div's content is its children, and a Text's content is its text.
  • Padding: space between the content and the border.
  • Border: the box-drawing characters around the padding.
  • Margin: space between the border and the element's neighbors.

The example below colors each rectangle:

@component
def root() -> Div:
    return Div(
        style=col,
        children=[
            Div(
                style=fill(1)
                | content_color("green", 500)
                | padding_color("orange", 500)
                | pad_x(2)
                | pad_y(1)
                | border
                | border_lightrounded
                | border_bg("blue", 500)
                | margin_color("red", 500)
                | margin_x(2)
                | margin_y(1)
            )
        ],
    )

The box model

  ╭────────────────────────╮  
  │                        │  
  │                        │  
  │                        │  
  │                        │  
  │                        │  
  │                        │  
  ╰────────────────────────╯  

Terminal cells are not square

Terminal cells are about twice as tall as they are wide, so one row of vertical padding or margin looks about as big as two columns of horizontal. The example above uses twice as much horizontal spacing as vertical for that reason, and horizontal spacing alone is often enough.

Gap, margin and padding

All three put space around children, but they belong to different elements:

  • gap is set on the parent, and puts space between its children only.
  • margin is set on a child, and puts space all around it, including at the ends of the row. Margins don't collapse into each other, so two children with margin_x(1) are two cells apart.
  • pad is set on the parent, and puts space between its border and all of its children.

margin_color and padding_color color the margin and padding, in red and blue here.

@component
def gap_margin_pad() -> Div:
    return Div(
        style=col,
        children=[
            Div(
                style=row | gap(2) | border | border_heavy,
                children=[Text(style=border, content=f"gap(2) on the row {n}") for n in range(2)],
            ),
            Div(
                style=row | border | border_heavy,
                children=[
                    Text(style=margin_x(1) | margin_color("red", 600) | border, content=f"margin_x(1) {n}")
                    for n in range(2)
                ],
            ),
            Div(
                style=row | pad_x(2) | padding_color("blue", 600) | border | border_heavy,
                children=[Text(style=border, content=f"pad_x(2) on the row {n}") for n in range(2)],
            ),
        ],
    )

Gap, margin and padding

┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃┌───────────────────┐  ┌───────────────────┐              ┃
┃│gap(2) on the row 0│  │gap(2) on the row 1│              ┃
┃└───────────────────┘  └───────────────────┘              ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┌─────────────┐  ┌─────────────┐                         ┃
┃ │margin_x(1) 0│  │margin_x(1) 1│                         ┃
┃ └─────────────┘  └─────────────┘                         ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃  ┌─────────────────────┐┌─────────────────────┐          ┃
┃  │pad_x(2) on the row 0││pad_x(2) on the row 1│          ┃
┃  └─────────────────────┘└─────────────────────┘          ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

Sharing borders between neighbors

border_collapse on a parent overlaps its children's borders by one cell, so neighbors share a border instead of drawing two side by side. Where the shared borders meet, border healing joins them with the right junction characters.

@component
def collapse() -> Div:
    return Div(
        style=col,
        children=[
            Div(
                style=row,
                children=[Text(style=border, content="side by side") for _ in range(3)],
            ),
            Div(
                style=row | border_collapse,
                children=[Text(style=border, content="border_collapse") for _ in range(3)],
            ),
        ],
    )

Collapsed borders

┌────────────┐┌────────────┐┌────────────┐       
│side by side││side by side││side by side│       
└────────────┘└────────────┘└────────────┘       
┌───────────────┬───────────────┬───────────────┐
│border_collapse│border_collapse│border_collapse│
└───────────────┴───────────────┴───────────────┘

Borders on some sides

Borders work as they do in Tailwind. A side is drawn where its border width is 1, and layout reserves a cell for it. border sets all four sides, border_top, border_bottom, border_left and border_right set one, and border_x and border_y set a pair. Each has a _0 form, such as border_right_0, that takes its sides away. Sides start at 0, so border_top | border_left draws just those two.

Merging is ordered, as it is for every style: the right side wins. border | border_right_0 leaves the right side off, but border_right_0 | border turns it back on.

The border kind only chooses the characters, and defaults to BorderKind.Light, so border alone draws a light border and border | border_heavy a heavy one. A kind with no widths draws nothing. border_none removes the border along with its space. border_sides sets all four sides at once from a set of side names.

@component
def sides() -> Div:
    return Div(
        style=row | align_children_start | gap(2),
        children=[
            Text(style=border_y, content="border_y"),
            Text(style=border_top | border_left, content="border_top\n| border_left"),
            Text(style=border | border_right_0, content="border\n| border_right_0"),
            Text(style=border_right_0 | border, content="border_right_0\n| border"),
        ],
    )

Borders on some sides

────────  ┌─────────────  ┌────────────────  ┌──────────────┐
border_y  │border_top     │border            │border_right_0│
────────  │| border_left  │| border_right_0  │| border      │
                          └────────────────  └──────────────┘

Shortening edges next to missing sides

border_contract(n) stops each edge n cells short at an end next to a side that isn't drawn.

@component
def contract() -> Div:
    return Div(
        style=row | align_children_start | gap(2),
        children=[
            Text(
                style=border_top | border_left | pad_x(1),
                content="top and left\nsides",
            ),
            Text(
                style=border_top | border_left | border_contract(2) | pad_x(1),
                content="top and left\nsides\nborder_contract(2)",
            ),
        ],
    )

Contracted borders

┌──────────────  ┌────────────────── 
│ top and left   │ top and left      
│ sides            sides             
                   border_contract(2)