Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / UI

layout primitives

Stack / Row / Box — flexbox layout with gap, align, justify, stickToBottom

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.

stable3 examples compiled by CIuilayout

Summary#

Osy# ships a small set of layout primitives you compose your UI from:

PrimitiveArranges its children
Stackin a column (top to bottom)
Rowin a row (left to right)
Boxa single container with no intrinsic direction
[Composable]
component ProductCard(string name, string price) {
  action AddToCart() { }
  render {
    Stack(gap: 2) {
      Text(name);
      Row(justify: Justify.SpaceBetween) {
        Text(price);
        Button("Add to cart", onPress: AddToCart);
      }
    }
  }
}

Signature#

Stack / Row / Boxflexbox 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#

Spacing — 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.

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

Alignment — align and justify#

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

aligneffect
Align.Startpack to the start
Align.Centercenter
Align.Endpack to the end
Align.Stretchstretch to fill
Align.Baselinealign text baselines
justifyeffect
Justify.Start / Justify.Center / Justify.Endpack to the start / center / end
Justify.SpaceBetweenequal space between children
Justify.SpaceAroundequal space around each child
Justify.SpaceEvenlyequal space between and at the edges
Row(align: Align.Center, justify: Justify.SpaceBetween) {
  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#

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:

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

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:

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#

  • component — declaring a component and its render block.
  • [[ui-component#render-tree]] — the full render vocabulary (text, conditionals, loops, bindings).
  • Markdown — rendering markdown text — rendering a message's text inside a transcript, including while it is still arriving.

Related

component

The one archetype for all UI: a bounded reactive unit — typed props, reactive members (fields, `live`…

component

The one archetype for all UI: a bounded reactive unit — typed props, reactive members (fields, `live`…

Markdown — rendering markdown text

`Markdown(text)` renders a markdown string as formatted content — headings, lists, tables, code, links. It is a…