# layout primitives

> The built-in layout primitives and how they arrange children. `Stack` stacks children in a column, `Row` lays them in a row, and `Box` is a plain container; `gap`, `align`, and `justify` control spacing and alignment.

<!-- id: ui-layout · area: ui · stability: stable · html: https://osysharp.com/reference/ui/layout/ -->

## Summary        {#summary}
Osy# ships a small set of **layout primitives** you compose your UI from:

| Primitive | Arranges its children |
|---|---|
| `Stack` | in a **column** (top to bottom) |
| `Row` | in a **row** (left to right) |
| `Box` | a single container with no intrinsic direction |

```osy title="stack, row, and the gap between" test app=ui-layout
[Composable]
component ProductCard(string name, string price) {
  action AddToCart() { }
  render {
    Stack(gap: 2) {
      Text(name);
      Row(justify: Justify.Between) {
        Text(price);
        Button("Add to cart", onPress: AddToCart);
      }
    }
  }
}
```

## Signature      {#signature}
```osy syntax
Stack / Row / Box — flexbox layout with gap, align, justify

Stack(stickToBottom: true)       { … }    // follow new content, but never fight the reader
Stack(stickToBottom: following)  { … }    // …and tell me when they scroll away
```

## Description    {#description}

### Spacing — `gap`   {#gap}
`gap` sets the space **between** a layout's children, as a step on the spacing scale (a whole number). Larger
numbers mean more space; `gap: 0` (the default) means no gap.

```osy syntax
Stack(gap: 4) { … }   // more space between rows
Row(gap: 1) { … }     // a little space between columns
```

### Alignment — `align` and `justify`   {#alignment}
`align` and `justify` are **built in** — they need no `using`, and no UI kit. The platform maps them straight to
flexbox, so a typo (`align: Align.Centre`) is a compile error rather than a silent no-op, and no kit can change what
`Align.Center` means.

**Write the value qualified — `Align.Center`, never a bare `Center`.** In an argument slot a bare capitalised name
could be a theme token, an enum member or a style keyword, and all three are spelled alike; the group name is what
says which vocabulary you meant. `align`'s group is `Align` and `justify`'s is `Justify`, so the value always reads
as *group*`.`*member*. A bare name there is refused, and the refusal names the spelling to write.

`align` positions children on the **cross axis**, `justify` distributes them along the **main axis** (the axis the
primitive lays out on — vertical for `Stack`, horizontal for `Row`).

| `align` | effect |
|---|---|
| `Align.Start` | pack to the start |
| `Align.Center` | center |
| `Align.End` | pack to the end |
| `Align.Stretch` | stretch to fill |
| `Align.Baseline` | align text baselines |

| `justify` | effect |
|---|---|
| `Justify.Start` / `Justify.Center` / `Justify.End` | pack to the start / center / end |
| `Justify.Between` | equal space between children |
| `Justify.Around` | equal space around each child |
| `Justify.Evenly` | equal space between and at the edges |

```osy syntax
Row(align: Align.Center, justify: Justify.Between) {
  Text("Title");
  Button("Action", onPress: Act);
}
```

These names are fixed (they map to the browser's flexbox model), so a typo like `align: Align.Centre` is a compile
error, not a silent no-op.

### Following new content — `stickToBottom`   {#stick-to-bottom}
A surface that grows while someone is reading it — a chat transcript, a log, a build console — should show the
newest content. But it must not yank a reader who has deliberately scrolled up to re-read something earlier. That is
the rule everybody gets wrong, and it is one word here:

```osy title="a transcript that follows" test app=ui-layout-stick
component Transcript(string[] Lines) {
  render {
    Stack(overflowY: Overflow.Auto, gap: 2, stickToBottom: true) {
      foreach (var line in Lines) { Text(line); }
    }
  }
}
```

It applies to a **scrolling** container — one with `overflowY: Overflow.Auto`. New content scrolls into view while the reader
is at the bottom; the moment they scroll up, following stops, and it resumes by itself when they scroll back down.

#### Knowing whether it is following, and jumping back   {#following}
Give it a `bool` field instead of a literal and the field becomes the container's *following* state, in **both**
directions. The container writes `false` into it when the reader scrolls away and `true` when they return — so your
app can show a *jump to latest* affordance — and setting it back to `true` yourself scrolls to the bottom and
resumes following:

```osy title="jump to latest" test app=ui-layout-stick
component Chat(string[] Lines) {
  bool following = true;

  action Jump() { following = true; }

  render {
    Box {
      Stack(overflowY: Overflow.Auto, gap: 2, stickToBottom: following) {
        foreach (var line in Lines) { Text(line); }
      }
      if (!following) {
        Button("Jump to latest", onPress: Jump);
      }
    }
  }
}
```

The button is yours to draw and place — the platform ships none. Setting the field is the only way to scroll a
container from Osy#, which is why the write half exists at all.

Note the difference between the two forms: `stickToBottom: <a bool expression>` is an on/off **switch** ("follow only
while the Live tab is open"), while `stickToBottom: <a field you can assign>` is the following **state**. With a
field, the behaviour stays on for as long as the container exists — a `false` means "not following right now", not
"switched off" — which is what lets the reader resume simply by scrolling back down.

## See also   {#see-also}
- [component](https://osysharp.com/reference/ui/component/) — declaring a component and its `render` block.
- [[ui-component#render-tree]] — the full render vocabulary (text, conditionals, loops, bindings).
- [Markdown — rendering markdown text](https://osysharp.com/reference/ui/markdown/) — rendering a message's text inside a transcript, including while it is still arriving.
