Using Layout

The Layout module provides higher-level layout combinators built on top of Box, inspired by CSS Flexbox and Grid. All helpers are pure functions that return standard Box values, composable with borders, annotations, padding, and all other Box primitives.

Flex

Flex distributes space among children using a flexbox-style algorithm. Children can be fixed-size, growable, or dynamically built from allocated space.

Child Types

  • Flex.fixed(box) — keeps its intrinsic size
  • Flex.grow(box, factor?) — stretches to fill remaining space
  • Flex.fill(builder, factor?) — receives allocated size and builds content to fit
  • Flex.spacer(factor?) — invisible child that pushes siblings apart

Horizontal Layout (Flex.row)

Use Flex.spacer() to push children apart, like CSS justify-content: space-between:

import * as Box from "effect-boxes/Box"
import { Flex } from "effect-boxes/Layout"
import { pipe } from "effect"
 
const navbar = Flex.row(
  [
    Flex.fixed(Box.text("◆ App Logo").pipe(Box.pad(0, 1))),
    Flex.spacer(),
    Flex.fixed(Box.text("Home  Reports  Settings").pipe(Box.pad(0, 1))),
    Flex.spacer(),
    Flex.fixed(Box.text("▸ Admin").pipe(Box.pad(0, 1))),
  ],
  65
)
╭─────────────────────────────────────────────────────────────────╮
│ ◆ App Logo            Home  Reports  Settings           ▸ Admin │
╰─────────────────────────────────────────────────────────────────╯

Proportional Splits

Grow and fill children share remaining space by factor. A factor of 2 gets twice the space of a factor of 1:

const split = Flex.row(
  [
    Flex.fill((w) => Box.text("1/3 width").pipe(
      Box.pad(0, 1), Box.minWidth(w), Box.border("rounded")
    ), 1),
    Flex.fill((w) => Box.text("2/3 width").pipe(
      Box.pad(0, 1), Box.minWidth(w), Box.border("rounded")
    ), 2),
  ],
  65,
  { gap: 1 }
)
┌─────────────────────────────────────────────────────────────────────┐
│╭──────────────────────╮ ╭──────────────────────────────────────────╮│
││ 1/3 width            │ │ 2/3 width                                ││
│╰──────────────────────╯ ╰──────────────────────────────────────────╯│
└─────────────────────────────────────────────────────────────────────┘

Fill Children

Fill children receive their allocated width and build content to fit exactly. This is useful for dynamic content like progress bars or separators:

const progress = Flex.row(
  [
    Flex.fixed(Box.text(" Progress ").pipe(Box.pad(0, 1))),
    Flex.fill((w) => Box.text(
      "━".repeat(Math.floor(w * 0.7)) + "╸" +
      "─".repeat(Math.max(0, w - Math.floor(w * 0.7) - 1))
    )),
    Flex.fixed(Box.text(" 70% ").pipe(Box.pad(0, 1))),
  ],
  65
)
╭─────────────────────────────────────────────────────────────────╮
│  Progress  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸─────────────  70%  │
╰─────────────────────────────────────────────────────────────────╯

Vertical Layout (Flex.col)

const page = Flex.col(
  [
    Flex.fixed(Box.text("┄ Header ┄").pipe(Box.alignHoriz(Box.center1, 65))),
    Flex.grow(Box.text("Body content goes here...").pipe(Box.pad(1, 2))),
    Flex.fixed(Box.text("┄ Footer ┄").pipe(Box.alignHoriz(Box.center1, 65))),
  ],
  10
)
╭─────────────────────────────────────────────────────────────────╮
│                           ┄ Header ┄                            │
│                                                                 │
│  Body content goes here...                                      │
│                                                                 │
│                                                                 │
│                                                                 │
│                                                                 │
│                                                                 │
│                                                                 │
│                           ┄ Footer ┄                            │
╰─────────────────────────────────────────────────────────────────╯

Options

Both Flex.row and Flex.col accept an options object:

  • gap — space between children (in columns for row, rows for col)
  • align — cross-axis alignment (Box.top, Box.center1, Box.bottom for row; Box.left, Box.center1, Box.right for col)

Container

Container provides dimension-aware building. It computes inner dimensions after padding, passes them to your builder function, then enforces the container width on the output.

import { Container } from "effect-boxes/Layout"
 
const panel = Container.make({ width: 65, padding: 2 }, (ctx) =>
  Box.vcat([
    Box.text(`outer width:  ${ctx.width}`),
    Box.text(`inner width:  ${ctx.innerWidth}`),
    Box.text(`padding:      2 cols each side`),
  ], Box.left)
)
┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│                                                                 │
│  outer width:  65                                               │
│  inner width:  61                                               │
│  padding:      2 cols each side                                 │
│                                                                 │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Container composes naturally with Flex — pass ctx.innerWidth to size flex layouts within the available space:

const statusPanel = pipe(
  Container.make({ width: 65, padding: 1 }, (ctx) =>
    Box.vcat([
      Flex.row(
        [
          Flex.fixed(Box.text("◆ Status").pipe(Box.pad(0, 1))),
          Flex.spacer(),
          Flex.fixed(Box.text("● Online").pipe(Box.pad(0, 1))),
        ],
        ctx.innerWidth
      ),
      Box.text("─".repeat(ctx.innerWidth)),
      Flex.row(
        [
          Flex.fixed(Box.text("Users: 1,247").pipe(Box.pad(0, 1))),
          Flex.fixed(Box.text("Uptime: 99.9%").pipe(Box.pad(0, 1))),
          Flex.spacer(),
          Flex.fixed(Box.text("v2.4.1").pipe(Box.pad(0, 1))),
        ],
        ctx.innerWidth
      ),
    ], Box.left)
  ),
  Box.border("rounded")
)
╭─────────────────────────────────────────────────────────────────╮
│                                                                 │
│  ◆ Status                                             ● Online  │
│ ─────────────────────────────────────────────────────────────── │
│  Users: 1,247  Uptime: 99.9%                            v2.4.1  │
│                                                                 │
╰─────────────────────────────────────────────────────────────────╯

Grid

Grid arranges items in rows and columns with uniform column widths.

Fixed-column Grid

import { Grid } from "effect-boxes/Layout"
 
const cards = ["Alpha", "Beta", "Gamma", "Delta", "Epsilon", "Zeta"]
  .map((name) =>
    Box.vcat([Box.text(`■ ${name}`), Box.text("  Item card")], Box.left)
      .pipe(Box.pad(0, 1), Box.minWidth(17), Box.border("rounded"))
  )
 
const grid = Grid.make(cards, { cols: 3, colWidth: 21, gap: [1, 1] })
┌─────────────────────────────────────────────────────────────────┐
│╭─────────────────╮   ╭─────────────────╮   ╭─────────────────╮  │
││ ■ Alpha         │   │ ■ Beta          │   │ ■ Gamma         │  │
││   Item card     │   │   Item card     │   │   Item card     │  │
│╰─────────────────╯   ╰─────────────────╯   ╰─────────────────╯  │
│                                                                 │
│╭─────────────────╮   ╭─────────────────╮   ╭─────────────────╮  │
││ ■ Delta         │   │ ■ Epsilon       │   │ ■ Zeta          │  │
││   Item card     │   │   Item card     │   │   Item card     │  │
│╰─────────────────╯   ╰─────────────────╯   ╰─────────────────╯  │
└─────────────────────────────────────────────────────────────────┘

Auto-column Grid

Grid.auto calculates the number of columns from a container width and minimum column width — useful for responsive layouts:

// Same cards, but columns adapt to available width
const grid = Grid.auto(cards, 50, { minColWidth: 20 })
┌─────────────────────────────────────────────────┐
│╭─────────────────╮      ╭─────────────────╮     │
││ ■ Alpha         │      │ ■ Beta          │     │
││   Item card     │      │   Item card     │     │
│╰─────────────────╯      ╰─────────────────╯     │
│╭─────────────────╮      ╭─────────────────╮     │
││ ■ Gamma         │      │ ■ Delta         │     │
││   Item card     │      │   Item card     │     │
│╰─────────────────╯      ╰─────────────────╯     │
│╭─────────────────╮      ╭─────────────────╮     │
││ ■ Epsilon       │      │ ■ Zeta          │     │
││   Item card     │      │   Item card     │     │
│╰─────────────────╯      ╰─────────────────╯     │
└─────────────────────────────────────────────────┘

Grid Options

OptionDescription
colsNumber of columns (fixed grid only)
colWidthWidth of each column (fixed grid only)
minColWidthMinimum column width (auto grid only)
maxColWidthMaximum column width (auto grid only)
gap[horizontal, vertical] spacing between cells
alignHorizontal alignment within cells
stretchWhether to stretch items to fill column width

Composing with Box Primitives

Layout functions return standard Box values, so they compose with all Box operations. Here's a dashboard combining Container, Flex, and Grid:

const dashboard = pipe(
  Container.make({ width: 65, padding: 1 }, (ctx) => {
    const cols = 3
    const gap = 1
    const colWidth = Math.floor((ctx.innerWidth - (cols - 1) * gap) / cols)
    const kpis = Grid.make(
      [
        Box.vcat([Box.text("Users"), Box.text("1,247")], Box.left)
          .pipe(Box.pad(0, 1), Box.minWidth(colWidth - 2), Box.border("rounded")),
        Box.vcat([Box.text("Revenue"), Box.text("$48,290")], Box.left)
          .pipe(Box.pad(0, 1), Box.minWidth(colWidth - 2), Box.border("rounded")),
        Box.vcat([Box.text("Errors"), Box.text("23")], Box.left)
          .pipe(Box.pad(0, 1), Box.minWidth(colWidth - 2), Box.border("rounded")),
      ],
      { cols, colWidth, gap: [gap, 0] }
    )
    const header = Flex.row(
      [
        Flex.fixed(Box.text("◆ Dashboard")),
        Flex.spacer(),
        Flex.fixed(Box.text("Settings ▸")),
      ],
      ctx.innerWidth
    )
    return Box.vcat(
      [header, Box.text("─".repeat(ctx.innerWidth)), kpis],
      Box.left
    )
  }),
  Box.border("rounded")
)
╭─────────────────────────────────────────────────────────────────╮
│                                                                 │
│ ◆ Dashboard                                          Settings ▸ │
│ ─────────────────────────────────────────────────────────────── │
│ ╭──────────────────╮ ╭──────────────────╮ ╭──────────────────╮  │
│ │ Users            │ │ Revenue          │ │ Errors           │  │
│ │ 1,247            │ │ $48,290          │ │ 23               │  │
│ ╰──────────────────╯ ╰──────────────────╯ ╰──────────────────╯  │
│                                                                 │
╰─────────────────────────────────────────────────────────────────╯