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 sizeFlex.grow(box, factor?)— stretches to fill remaining spaceFlex.fill(builder, factor?)— receives allocated size and builds content to fitFlex.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.bottomfor row;Box.left,Box.center1,Box.rightfor 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
| Option | Description |
|---|---|
cols | Number of columns (fixed grid only) |
colWidth | Width of each column (fixed grid only) |
minColWidth | Minimum column width (auto grid only) |
maxColWidth | Maximum column width (auto grid only) |
gap | [horizontal, vertical] spacing between cells |
align | Horizontal alignment within cells |
stretch | Whether 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 │ │
│ ╰──────────────────╯ ╰──────────────────╯ ╰──────────────────╯ │
│ │
╰─────────────────────────────────────────────────────────────────╯