Flexboxes
How a flex container places its children, the properties that steer it, and live examples to try them on.
This page is a short tour, enough to lay out a screen. Flexbox has more to it, and the Yoga styling docs describe every property Reactive exposes.
How it works
A flex container lines its children up along one axis, called the main axis, which FlexDirection picks. A Row
runs left to right and a Column runs top to bottom, and the other axis is the cross axis. JustifyContent spreads
the children along the main axis when there's room to spare, and AlignItems places them across it. Gap keeps space
between neighbours, and Padding keeps all of them away from the container's edges. When a row runs out of room,
FlexWrap lets it continue on a new line instead of overflowing.
Try this sandbox, where five blocks of different sizes sit in a box and each control changes one of its properties:
A container is a row unless told otherwise, like on the web, but items don't shrink unless told to, like in React
Native. AlignItems only has work to do when there's free space across the axis, and JustifyContent only when there's
free space along it. The blocks above set a MinSize rather than a Size, because a fixed size can't stretch and a
minimum can.
Units
A size, padding, margin, gap or position is a YogaValue. 12.pt is twelve points, which are Unity units, and a plain
12f means the same. 50.pct is half of the parent, and YogaValue.Auto sizes from the content and is the default.
Yoga's other keywords are the YogaValue.FitContent, YogaValue.MaxContent and YogaValue.Stretch constants.
A YogaVector holds one value per axis, so new() { x = 20.pt } sets the width and leaves the height to the content.
A single value like Size = 7.pt sets both axes at once. A YogaFrame holds one value per edge, like
new() { left = 2.pt, right = 2.pt }, and a single value fills all four edges.
In this demo, a slider sets the same number for two bars, in points for one and in percent of the box for the other:

CreateRow(
width: amount.Map(x => x.pt),
text: amount.Map(x => $"{x:0}.pt")
),
CreateRow(
width: amount.Map(x => x.pct),
text: amount.Map(x => $"{x:0}.pct")
),
// As wide as the label inside it
new Background {
FlexItem = {
AlignSelf = Align.FlexStart,
Size = new() { x = YogaValue.Auto, y = 5.pt }
}
}The same number gives two different widths, because the percent bar is measured against the box. The Auto bar below them doesn't change at all, and stays as wide as its label.
Growing and shrinking
FlexGrow is an item's share of the leftover room, and FlexShrink is its share of the shortfall when there isn't
enough. Most of the time you'll use Flex = 1, which means grow 1 starting from nothing, so items take equal shares.
Unlike CSS it doesn't also make the item shrink, because Reactive uses Yoga's defaults rather than the web's. Two
siblings with Flex = 1 split a row in half, and Flex = 1 next to Flex = 2 splits it one to two. That's how a label
fills the room next to a button, or a page fills the room next to a sidebar. MinSize and MaxSize clamp whatever the
rest decided, which also keeps a growing item from being squeezed to nothing.
Below you can see two growing blocks sharing a row with a fixed one, each with a slider for its FlexGrow:

new Background {
FlexItem = {
sFlexGrow = growA
}
},
new Background {
FlexItem = {
sFlexGrow = growB
}
},
new Background {
FlexItem = {
Size = new() { x = 18.pt }
}
}As you move the sliders, the fixed block keeps its width while the other two split whatever is left. A few more properties shape a
single item. Margin pushes its neighbours and the container's edges away from it, and AlignSelf overrides the
container's AlignItems for it alone. AspectRatio works out one axis from the other, so a box with only a height and
an aspect ratio of 1 is square.
Absolute positioning
PositionType.Absolute takes an item out of the flow, so its siblings are laid out as if it wasn't there. Position
then pins it by its distance from the parent's edges, in points or percents, and only the edges you set count. A badge
in a corner, a settings button the rest of the screen shouldn't know about and anything that follows the pointer all work
this way. So does a progress fill behind a label, stretched with { left = 0.pt, top = 0.pt, bottom = 0.pt } and a
bound width.
Here's a badge you can move between the corners of a box without the two blocks inside it noticing:

new Background {
FlexItem = {
PositionType = PositionType.Absolute,
Size = new() { x = 14.pt, y = 5.pt },
sPosition = corner.Map(x => x switch {
Corner.TopLeft => new YogaFrame { top = 1.pt, left = 1.pt },
Corner.TopRight => new YogaFrame { top = 1.pt, right = 1.pt },
Corner.BottomLeft => new YogaFrame { bottom = 1.pt, left = 1.pt },
_ => new YogaFrame { bottom = 1.pt, right = 1.pt }
})
}
}Wherever the badge goes, the blocks stay centered. An absolute item is still placed by its parent, just not in the flow, and Layout System covers making the parent leave something alone entirely.
Constraining a root
A root is a layout that nobody else lays out, like the top of a screen, so it's sized against the rect of the transform
it's attached to. ConstrainHorizontal and ConstrainVertical on its controller decide how, one axis each, and both are
on by default. On a constrained axis the root fills that rect, and on an unconstrained one it's as big as its content. A
fixed Size on the root still wins over both, and a MaxSize makes it fit its content up to that limit. The flags come
from Reactive rather than Yoga, and mean nothing on a flex layout nested in another one, where the parent decides the
size. Layout System explains what makes a layout a root.
Turn either flag off below to see the root, the blue box, shrink to its label on that axis:

class Constraints : ReactiveComponent {
protected override GameObject Construct() {
var horizontal = Remember(true);
var vertical = Remember(true);
return new Layout {
FlexController = {
FlexDirection = FlexDirection.Column,
AlignItems = Align.Center,
Padding = new() { top = 4.pt },
Gap = 3.pt
},
Children = {
// Has no controller, so the background inside is a root
new Background {
FlexItem = {
Size = new() { x = 70.pt, y = 32.pt }
},
Sprite = BeatSaberResources.Sprites.background,
PixelsPerUnit = 10f,
Color = Color.white with { a = 0.08f },
Children = {
new Background {
FlexController = {
JustifyContent = Justify.Center,
AlignItems = Align.Center,
Padding = 2.pt,
sConstrainHorizontal = horizontal,
sConstrainVertical = vertical
},
Sprite = BeatSaberResources.Sprites.background,
PixelsPerUnit = 10f,
Color = new Color(0.35f, 0.75f, 1f),
Children = {
new Label {
Text = "Root",
FontSize = 3f,
Color = Color.black
}
}
}
}
},
new Layout {
FlexController = { Gap = 4.pt },
Children = {
new BsToggle {
Toggled = true,
OnToggleStateChanged = x => horizontal.Value = x
}.InNamedRail("Horizontal"),
new BsToggle {
Toggled = true,
OnToggleStateChanged = x => vertical.Value = x
}.InNamedRail("Vertical")
}
}
}
}.Use();
}
}Watch what happens with only one axis turned off: the box is as wide as its content and as tall as the space around it, which is how you'd build a sidebar.
