| title | Layout |
|---|---|
| description | Flexbox with a column default, CSS grid, sizes and percentages, aspect ratio, absolute positioning, measuring with onLayout, responsive layouts and right-to-left, in Rust and TSX. |
| order | 34 |
| group | ui |
| demo | guide-layout |
NSH lays out every element with Taffy, a Rust implementation of CSS flexbox, grid and block layout, configured so that children stack vertically and do not shrink by default. Layout runs in the core, after each turn of your code, and only the frames that changed are sent to the native views. Text is measured by the platform's text engine on the same thread, so layout never waits for the main thread.
The layout properties below are style properties. In Rust they are typed style methods (.flex_row(), .gap_4(), .grid_cols(3), .absolute().inset_0()); in TSX they are keys of the style prop, and each has a Tailwind class (flex-row, gap-4, grid-cols-3, absolute inset-0, ...). Both lower to the same native styles.
A view lays out its children with flexbox. A view that sets no display stacks its children in a column; display: "flex" in a style makes a row unless flexDirection is set, exactly as in CSS and as Tailwind's flex class. Where nothing sets them, these defaults apply:
| Property | NSH default | Web default |
|---|---|---|
flexDirection |
"column" (a view that sets no display); "row" once display: "flex" is set |
row |
flexShrink |
0 |
1 |
alignContent |
"flex-start" (grids: "normal") |
normal |
alignItems |
"stretch" |
stretch |
minWidth / minHeight |
0 |
auto |
position |
"relative" |
static |
view()
.flex_row()
.items_center()
.gap_3()
.p_4()
.child(image(avatar).size(px(40.0)).rounded(px(20.0)))
.child(
view()
.flex_1()
.gap_0p5()
.child(text(name).font_weight(FontWeight::SEMIBOLD))
.child(text(message).text_color(rgb(0x6e7781)).number_of_lines(1)),
)
.child(text(time).text_color(rgb(0x6e7781)))<View style={{ flexDirection: "row", alignItems: "center", gap: 12, padding: 16 }}>
<Image src={avatar} style={{ width: 40, height: 40, borderRadius: 20 }} />
<View style={{ flex: 1, gap: 2 }}>
<Text style={{ fontWeight: "600" }}>{props.name}</Text>
<Text style={{ numberOfLines: 1, color: "#6e7781" }}>{props.message}</Text>
</View>
<Text style={{ color: "#6e7781" }}>{props.time}</Text>
</View>| Property | Values | Rust |
|---|---|---|
flexDirection |
"column", "row", "column-reverse", "row-reverse" |
.flex_col(), .flex_row(), .flex_col_reverse(), .flex_row_reverse() |
justifyContent |
Main axis: "flex-start", "flex-end", "start", "end", "center", "space-between", "space-around", "space-evenly", "stretch" |
.justify_start(), .justify_center(), .justify_end(), .justify_between(), .justify_around(), .justify_evenly() |
alignItems |
Cross axis: "stretch", "flex-start", "flex-end", "start", "end", "self-start", "self-end", "center", "baseline" |
.items_stretch(), .items_start(), .items_center(), .items_end(), .items_baseline() |
alignSelf |
One child's cross-axis alignment; "auto" follows the parent's alignItems |
.self_start(), .self_center(), .self_end(), .self_stretch() |
alignContent |
Lines of a wrapping container | |
flexWrap |
"nowrap", "wrap", "wrap-reverse" |
.flex_wrap() |
flex |
A single-number shorthand (below) | .flex_1(), .flex_auto(), .flex_none() |
flexGrow, flexShrink, flexBasis |
As in CSS | .flex_grow(n), .flex_shrink(n), .flex_shrink_0(), .basis(length) |
gap, rowGap, columnGap |
Space between children | .gap(length), .gap_y(length), .gap_x(length), and the steps .gap_2(), .gap_x_4(), ... |
flex-start and flex-end follow the flex direction; start and end follow the writing direction, as in CSS (right to left, start is the right side), and self-start / self-end the child's own.
flex is CSS's shorthand, as Tailwind's flex-* classes are: a number n means grow by n, shrink by 1 and start from a basis of 0, so flex: 1 siblings (.flex_1() in Rust, flex-1) share the space equally whatever their content; "none" is 0 0 auto (the view keeps its size), "auto" is 1 1 auto, "initial" is 0 1 auto, and grow, shrink and basis can be written out ("2 0 50%", "1 1 120px"). As minimum sizes default to 0, flex: 0 (0 1 0) shrinks a view to nothing unless minWidth: "auto" keeps its content's size.
view()
.flex_row()
.gap_2()
// One share.
.child(view().flex_1().h(px(36.0)).bg(rgb(0xc8e1ff)))
// Two shares.
.child(view().flex_grow(2.0).flex_shrink(1.0).basis(px(0.0)).h(px(36.0)).bg(rgb(0x79b8ff)))
// Fixed.
.child(view().size(px(36.0)).bg(rgb(0x0366d6)))<View style={{ flexDirection: "row", gap: 8 }}>
<View style={{ flex: 1, height: 36, backgroundColor: "#c8e1ff" }} /> {/* one share */}
<View style={{ flex: 2, height: 36, backgroundColor: "#79b8ff" }} /> {/* two shares */}
<View style={{ width: 36, height: 36, backgroundColor: "#0366d6" }} /> {/* fixed */}
</View>Because minimum sizes default to 0, a flex: 1 text in a row shrinks and wraps instead of pushing its siblings out. minWidth: "auto" / minHeight: "auto" (min-w-auto / min-h-auto, .min_w(auto()) in Rust) is CSS's automatic minimum: a shrinking item stops at its content's smallest size, the longest word of a text, as on the web.
Note
display: "flex" means a row, in a style, a class (flex) and Rust (.flex()) alike, unless a direction is set: { display: "flex", flexDirection: "column" }, flex flex-col, .flex().flex_col(). A view without display needs nothing to be a column.
| Property | Values | Rust |
|---|---|---|
width, height |
Points, a percentage of the parent ("50%"), or "auto" |
.w(px(120.0)), .h(relative(0.5)), .w_full(), .size(px(44.0)), the steps .w_10(), ... |
minWidth, maxWidth, minHeight, maxHeight |
Points or percentages; minWidth / minHeight also "auto", the content's smallest size |
.min_w(..), .max_w(..), .min_h(..), .max_h(..) |
aspectRatio |
Width divided by height: 1, 16 / 9 |
.aspect_ratio(16.0 / 9.0), .aspect_square() |
padding, paddingHorizontal, paddingVertical, paddingTop, ... |
Points or percentages | .p(..), .px(..), .py(..), .pt(..), ..., and the steps .p_4(), .px_2(), ... |
margin, marginHorizontal, marginVertical, marginTop, ... |
Points, percentages, or "auto" |
.m(..), .mx(..), .my(..), .mt(..), ..., .mx(auto()) |
In Rust a length is typed: px(16.0) is 16 points, relative(0.5) is 50% of the parent, auto() is auto. The step methods follow Tailwind's spacing scale of 4 points per step (.p_4() is 16 points, .gap_0p5() 2 points).
Without a size, a view is as big as its content (plus padding), stretched across its parent's cross axis by the default alignItems: "stretch". Percentages are relative to the parent's size (for padding and margin, as in CSS, to its width).
aspectRatio sizes the other dimension from the one that is known:
view().w_full().aspect_ratio(16.0 / 9.0).bg(black())<View style={{ width: "100%", aspectRatio: 16 / 9, backgroundColor: "#000000" }} />Images from the app's assets have a natural size and lay out at it when no size is given (Images and assets). Remote images have no size until they load: give them one.
display: "grid" (.grid() in Rust) lays out children in CSS grid tracks:
view()
.grid()
.grid_template_columns("repeat(3, 1fr)")
.gap_2()
.child(view().col_span(2).h(px(60.0)).bg(rgb(0xd8b9ff)))
.child(view().h(px(60.0)).bg(rgb(0xb083f0)))
.child(view().row_span(2).bg(rgb(0x8250df)))
.child(view().col_start(2).col_end(4).h(px(60.0)).bg(rgb(0x6639ba)))<View style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 8 }}>
<View style={{ gridColumn: "span 2", height: 60, backgroundColor: "#d8b9ff" }} />
<View style={{ height: 60, backgroundColor: "#b083f0" }} />
<View style={{ gridRow: "span 2", backgroundColor: "#8250df" }} />
<View style={{ gridColumn: "2 / 4", height: 60, backgroundColor: "#6639ba" }} />
</View>| Property | Values | Rust |
|---|---|---|
gridTemplateColumns, gridTemplateRows |
CSS track lists: "repeat(3, 1fr)", "200px 1fr", minmax(), auto-fill / auto-fit, line names |
.grid_cols(3), .grid_rows(2), .grid_template_columns("200px 1fr"), .grid_template_rows(..) |
gridAutoColumns, gridAutoRows |
Tracks of implicitly created columns and rows | |
gridAutoFlow |
"row", "column", "dense", "row dense", "column dense" |
.grid_flow_col(), .grid_flow_dense() |
gridColumn, gridRow |
"1 / 3", "span 2", a line number or name: "content-start / content-end", "2 col" (the second line named col), "span col" (up to the next line named col) |
.col_span(2), .row_span(2), .col_span_full() |
gridColumnStart, gridColumnEnd, gridRowStart, gridRowEnd |
Line numbers or names | .col_start(1), .col_end(3), .row_start(..), .row_end(..) (line numbers) |
justifyItems, justifySelf |
How items sit in their areas along the row: "start", "end", "center", "stretch", ... |
|
alignItems, alignSelf |
How items sit in their areas along the column | .items_*(), .self_*() |
justifyContent, alignContent |
How the tracks sit in the grid along the row and the column | .justify_*() |
Templates name their lines in brackets, one or several names per line, and items are placed by those names, as in CSS (in Rust, by the lines' numbers):
view()
.grid()
.grid_template_columns("[full-start] 16px [content-start] 1fr [content-end] 16px [full-end]")
// content-start / content-end: lines 2 to 3.
.child(view().col_start(2).col_end(3).h(px(60.0)).bg(rgb(0xb083f0)))
// full-start / full-end: lines 1 to 4.
.child(view().col_start(1).col_end(4).h(px(60.0)).bg(rgb(0x8250df)))<View style={{ display: "grid", gridTemplateColumns: "[full-start] 16px [content-start] 1fr [content-end] 16px [full-end]" }}>
<View style={{ gridColumn: "content-start / content-end", height: 60, backgroundColor: "#b083f0" }} />
<View style={{ gridColumn: "full-start / full-end", height: 60, backgroundColor: "#8250df" }} />
</View>In a grid, justifyContent and alignContent default to CSS's "normal": tracks sized auto stretch to fill the container, so a grid with a height and no row template shares that height among its rows. A value set explicitly ("start", "center", "space-between", ...) places the tracks as it does in CSS. The alignment classes set exactly what Tailwind CSS sets: content-start (align-content: flex-start) and place-content-start pack the tracks at the start, and content-normal and justify-normal stretch them, as in a browser.
display: "block" uses CSS block layout (children stacked, each as wide as the container), and display: "none" (.hidden()) removes the view from layout.
position: "absolute" (.absolute()) takes a view out of the flow and places it by top, right, bottom and left, relative to its parent:
view()
.h(px(200.0))
.child(image(cover).absolute().inset_0())
.child(
view()
.absolute()
.left_3()
.right_3()
.bottom_3()
.child(text("Caption").text_color(white()).font_weight(FontWeight::BOLD)),
)<View style={{ height: 200 }}>
<Image src={cover} style={{ position: "absolute", top: 0, right: 0, bottom: 0, left: 0 }} />
<View style={{ position: "absolute", left: 12, right: 12, bottom: 12 }}>
<Text style={{ color: "#ffffff", fontWeight: "700" }}>Caption</Text>
</View>
</View>Offsets accept points and percentages (.top(..), .right(..), .bottom(..), .left(..), .inset(..) in Rust). A view with left and right (or top and bottom) and no size stretches between them. Later siblings are drawn above earlier ones; zIndex (.z_index(n)) changes the order.
onLayout (.on_layout(..)) reports a view's frame, relative to its parent, after each layout in which it changed:
let width = signal(0.0_f32);
view()
.on_layout(move |frame: &Bounds<Points>| width.set(frame.size.width.0))
.child(text(move || format!("{} points wide", width.get())))const width = signal(0);
<View onLayout={({ bounds }) => width.set(bounds.size.width)}>
<Text>{width()} points wide</Text>
</View>The frame is in points: Bounds<Points> in Rust, { bounds } in TSX, a Bounds ({ origin: { x, y }, size: { width, height } }). For the whole window, read window() in Rust or windowSize() from nsh in TSX; both follow rotation and window resizing, and a style that reads them changes with the window:
view().style_when(|| window().width > px(600.0), |s| s.flex_row())import { View } from "nsh";
import { windowSize } from "nsh";
<View style={{ flexDirection: windowSize().width > 600 ? "row" : "column" }} />Tailwind's breakpoints (sm:, md:, ...) and portrait: / landscape: give the same result without code.
The root view covers the whole screen, under the status bar and the home indicator. Pad screens by the safe area insets with CSS's env() (paddingTop: "env(safe-area-inset-top)", Tailwind pt-[env(safe-area-inset-top)], .p_safe_area() in Rust; see Safe area), or read the insets with safe_area() in Rust or safeArea() / safeAreaInsets() in TSX and apply them yourself. Pages of a navigation stack already sit below their header.
The app's layout direction follows the system language, and a view's direction style ("ltr", "rtl" or "inherit"; .direction(LayoutDirection::Rtl) in Rust) sets the direction of its subtree. When the app lays out right to left:
- rows run from the right;
leftandrightkeep their sides, as in a browser, in style keys and classes alike:paddingLeft,marginLeft,left,borderLeftWidth,borderTopLeftRadius,textAlign: "left"andpl-4,ml-2,left-0,border-l,rounded-tl-lg,text-leftstay on the left;- the logical properties follow the direction, inline start being the right side:
marginStart,marginEnd,paddingStart,paddingEnd,start,end,borderStartWidth,borderEndColor,borderTopStartRadius, ...,textAlign: "start"/"end"(ps-4,ms-2,start-0,border-s,rounded-ss-lg,text-startas classes;.ms(..),.me(..),.ps(..),.pe(..),.border_s(..),.rounded_ss(..), ... in Rust);marginHorizontalandpaddingHorizontalset both sides; - text with
textAlign: "auto"aligns right, and native controls mirror.
view()
.flex_row()
.gap_2()
.child(view().ms_4().rounded_ss(px(12.0)).size(px(44.0)).bg(rgb(0x54aeff)))
let rtl = signal(false);
view()
.direction(move || if rtl.get() { LayoutDirection::Rtl } else { LayoutDirection::Ltr })
.on_press(move |_: &()| rtl.set(!rtl.get()))
.child(text(move || if rtl.get() { "Right to left" } else { "Left to right" }))import { signal, Text, View } from "nsh";
const rtl = signal(false);
<View style={{ direction: rtl() ? "rtl" : "ltr" }}>
<View style={{ flexDirection: "row", gap: 8 }}>
<View style={{ marginStart: 16, borderTopStartRadius: 12, width: 44, height: 44, backgroundColor: "#54aeff" }} />
</View>
<View onPress={() => rtl.set(!rtl())}>
<Text>{rtl() ? "Right to left" : "Left to right"}</Text>
</View>
</View>Changes to the direction, the language's or a view's, apply at once, without a restart. The direction style also keeps one view and its children in a direction of their own, for example a phone number left to right. On Android, apps that embed NSH need android:supportsRtl="true" in their manifest for native controls to mirror.
- Styling: how styles combine.
- Scrolling and Lists: content larger than the screen.
- Style reference: every layout property and its type.