Layout
Four primitives for the 95% of layouts: stack (vertical), row (horizontal), grid, container. Separate stack + row, not a direction param — defaults diverge enough that parameterizing it just pushes complexity to every call site.
Stack
Vertical flex, no wrap. gap: maps to Tailwind's spacing scale;
align: maps to items-*.
live_example do sui.layout.stack gap: 2 do sui.badge "First" sui.badge "Second" sui.badge "Third" end end
Everything in this stack is centered horizontally.
live_example do sui.layout.stack gap: 4, align: :center, class: "w-full" do sui.button "Centered" sui.muted { "Everything in this stack is centered horizontally." } end end
Row
Horizontal flex. Defaults to items-center + flex-wrap.
Pass wrap: false to disable wrapping, justify: for
main-axis alignment.
live_example do sui.layout.row gap: 2 do sui.button "One" sui.button "Two", variant: :outline sui.button "Three", variant: :secondary end end
Settings
live_example do sui.layout.row gap: 4, justify: :between, class: "w-full" do sui.h3 "Settings" sui.button "Save" end end
live_example do sui.layout.row gap: 2, wrap: false, class: "overflow-x-auto w-full" do 12.times do |i| sui.badge "Tag #{i + 1}", variant: :secondary end end end
Grid
CSS grid. cols: maps to grid-cols-N.
Cell 1
Cell 2
Cell 3
live_example do sui.layout.grid cols: 3, gap: 4 do 3.times do |i| sui.card(class: "p-4") { sui.muted { "Cell #{i + 1}" } } end end end
Revenue
—
Users
—
Projects
—
Uptime
—
live_example do sui.layout.grid cols: 2, gap: 4, class: "md:grid-cols-4" do %w[Revenue Users Projects Uptime].each do |label| sui.card(class: "p-4") do sui.layout.stack gap: 1 do sui.muted { label } sui.h3 "—" end end end end
Container
Max-width wrapper with horizontal padding and auto margins. Sizes:
:sm (3xl), :md (5xl, default),
:lg (7xl), :xl, :full.
sm container — max-w-3xl, centered.
live_example do sui.layout.container size: :sm, class: "border rounded-md p-6 bg-muted" do sui.muted { "sm container — max-w-3xl, centered." } end end
Composing primitives
A realistic layout: container wrapping a stack, rows inside the stack, a grid inside.
Dashboard
Revenue
1,234
Active
1,234
Orders
1,234
live_example do sui.layout.container size: :md, class: "border rounded-md p-6" do sui.layout.stack gap: 6 do sui.layout.row justify: :between do sui.h2 "Dashboard" sui.layout.row gap: 2 do sui.button "Export", variant: :outline sui.button "New" end end sui.layout.grid cols: 3, gap: 4 do %w[Revenue Active Orders].each do |label| sui.card(class: "p-4") do sui.layout.stack gap: 1 do sui.muted { label } sui.h3 "1,234" end end end end end end
Escape hatches
Every primitive accepts class: for one-offs and forwards
remaining kwargs as HTML attrs — pass Stimulus data:,
id:, role:, etc. For responsive axis-flipping
(e.g. stack on mobile, row on desktop), use class: with
responsive prefixes rather than adding a direction prop.
Title
Description
sui.layout.stack gap: 4, class: "md:flex-row md:items-center" do sui.h2 "Title" sui.muted { "Description" } end
API
| Method | Kwargs | Default | Notes |
|---|---|---|---|
| stack | gap:, align: | — | Vertical flex, no wrap. |
| row | gap:, align:, justify:, wrap: | align: :center, wrap: true | Horizontal flex, wraps + centers by default. |
| grid | cols:, gap: | — | CSS grid. |
| container | size: | :md | Max-width wrapper, centered, horizontal padding. |