Responsive
Continuous Max.X is the breakpoint — build screens that reflow, never CSS media queries.
lotusui apps are responsive by construction when you compose with the constraint-driven
primitives. There is no breakpoint enum and no @media twin: every widget lays out
from the Max.X (and Max.Y) it is given this frame. Narrow the window,
a Split pane, or a docs demo box — the same code path reflows.
Use this page as the map. Each mechanism has its own component page for props and edge cases; the live demos below are the real gallery states — resize the browser to see them change.
The doctrine
Pass the width you have; never invent a fake one. Gio hands every
layout a layout.Constraints. lotusui components honor it — buttons hug, grids
derive columns, Wrap starts a new line. The anti-pattern is clamping
gtx.Constraints.Max.X to a constant “for the demo”: that freezes reflow and lies
about how the screen behaves in a real window.
Desktop, mobile, and the WASM docs gallery share one model. A narrow Split detail pane is the same problem as a phone-sized window: a smaller Max.X. Design for constraints, not for device labels.
Readable page column
LayoutPage caps content at 920dp and
centers it with padding — the difference between a stretched OS window and a designed page.
Inside that column, children still see the column width, so Wrap and SimpleGrid keep
reflowing as the window grows past 920dp (the column stops growing) or shrinks below it.
lotusui.LayoutPage(gtx, func(gtx C) D {
return lotusui.VStack(th.Space.MD,
header,
lotusui.SimpleGrid(gtx, cards, 180, 4, th.Space.SM, cardCell),
lotusui.Wrap(th.Space.SM, layout.Middle, chips...),
)(gtx)
})
Wrap — flowing chips and labels
Wrap is CSS flex-wrap for Gio: each child
is measured at intrinsic width, then packed left-to-right; when the next child would exceed
Max.X, a new line starts. Same gap between items and between lines. Prefer it for badges,
filter chips, and any row that must not squeeze labels into one-character columns.
HStack never wraps — under a narrow Max.X its Rigid
children are squeezed. Use HStack for a single non-wrapping unit (switch + label); use Wrap
for a flowing set.
lotusui.Wrap(th.Space.SM, layout.Middle,
chip("Design"), chip("Engineering"), chip("Product"),
chip("Marketing"), chip("Sales"), chip("Support"),
// …)
SimpleGrid — columns from width
SimpleGrid derives
column count = min(maxCols, floor(available / minChildWidth)) (never below 1).
No breakpoint table: continuous width in, column count out. Rows share the tallest cell's
height. Reach for Grid only when you need explicit spans.
lotusui.SimpleGrid(gtx, items, 140, 4, th.Space.SM,
func(gtx C, it Item) D { return cell(it)(gtx) })
Tabs and AnnotatedText
Horizontal Tabs lay the strip with Wrap, so whole
tabs flow to the next line at intrinsic width — never a 1-character squeezed label in a
narrow pane. The default well grows with wrapped height. Vertical stays a
column Flex.
AnnotatedText (glossary segments) uses the same Wrap composition (gap 0) so inline terms wrap instead of compressing.
tabs := lotusui.Tabs{Options: lotusui.TabOpts(
"Changes", "Staging", "Production", "Reviews", "Approvals", "History",
)}
// under a narrow Max.X the strip wraps to two lines
Panes, columns, and scrolling
Split gives each visible pane its own Max.X from
the carousel layout — detail panes are narrower than solo full-width. Put Wrap / SimpleGrid /
Tabs inside a pane and they reflow to that width; do not clamp Max.X further “for
safety.” Use SplitColumnScroll / SplitBoxScroll /
SplitBoxFillScroll when content is taller than the viewport; scrolling is
vertical overflow, not a substitute for horizontal reflow.
For the main screen body, Scrollable (mixed
content) and ListView (virtualized rows) honor the width they are given the same
way.
s.Layout(gtx, th.Space.MD, depth,
lotusui.SplitBox(th, listPane),
lotusui.SplitBox(th, func(gtx C) D {
return lotusui.Wrap(th.Space.SM, layout.Middle, filters...)(gtx)
}),
)
Choosing the right primitive
| Need | Use | Avoid |
|---|---|---|
| Readable app column | LayoutPage | Full-bleed Flexed content edge-to-edge |
| Flowing chips / tags | Wrap | HStack (squeezes) |
| Equal tiles, N columns | SimpleGrid | Hard-coded column Flex |
| Explicit spans | Grid | Nested HStacks |
| One non-wrapping row | HStack | Wrap (may break the unit) |
| Tab strip under narrow Max.X | horizontal Tabs (Wrap) | Hand-rolled Flex+Rigid labels |
| Multi-pane screen | Split + reflow inside panes | Fixed dp columns that ignore Max.X |
What lotusui does not ship
No named breakpoints (sm/md/lg), no
show/hide-at-width helpers, no separate “mobile” component set. Those are web-era answers to
CSS layout. In Gio the continuous constraint is the responsive API — if a screen must
change structure (not just reflow), the app owns that policy with ordinary Go
(if gtx.Constraints.Max.X < gtx.Dp(480) { … }), not a library mode flag.