# 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.

### The value can come from anywhere   {#a-vocabulary-value}
`Align` and `Justify` are ordinary types, so anything of that type is a legal value — a local, a component
**parameter**, a field, a method's result, a `switch`, an element of a list. A component that lets its caller decide
how a row is arranged declares the parameter and passes it through, exactly as it would in C#:

```osy title="a caller decides the arrangement" test app=ui-layout-value
[Composable]
component Toolbar(Justify arrange, string label) {
  render {
    Row(justify: arrange, gap: 2) { Text(label); }
  }
}

[Composable]
component Bar(bool compact) {
  Align lane = Align.Center;
  render {
    Stack(align: lane, gap: 2) {
      Toolbar(arrange: compact ? Justify.Center : Justify.Between, label: "Tools");
    }
  }
}
```

A value the compiler can see — a member, or a conditional over members — is resolved where it is written and costs
the page nothing. A value it can only know the **type** of is looked up when the page renders, and the vocabulary
travels with **that page** so nothing else pays for it.

The type is checked either way: `justify:` takes a `Justify`, so an `Align`, a number or a `string` is a compile
error naming the whole set. `Justify?` is how a component makes the argument optional — a null one arranges nothing,
the same as leaving it out.

The one value that is refused is a **generic component's type parameter** — `component Passer<T>(T v)` used as
`justify: v`. A generic body is compiled once for every call site, so which vocabulary `T` stands for is not known
in it and a caller could bind it to anything. Declare the parameter as `Justify` (as `Toolbar` above does) and pass
it through; the compiler says exactly that if you hit it.

### 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.
