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.
Installation
bin/rails g shadcnrb:component layout
Stack
Vertical flex, no wrap. gap: maps to Tailwind's spacing scale;
align: maps to items-*.
Everything in this stack is centered horizontally.
<!-- gap: 2 --> <%= sui.layout.stack gap: 2 do %> <%= sui.badge "First" %> <%= sui.badge "Second" %> <%= sui.badge "Third" %> <% end %> <!-- gap: 4, align: :center --> <%= sui.layout.stack gap: 4, align: :center, class: "w-full" do %> <%= sui.button "Centered" %> <%= sui.muted { "Everything in this stack is centered horizontally." } %> <% end %>
Row
Horizontal flex. Defaults to items-center + flex-wrap.
Pass wrap: false to disable wrapping, justify: for
main-axis alignment.
Settings
<!-- gap: 2 --> <%= sui.layout.row gap: 2 do %> <%= sui.button "One" %> <%= sui.button "Two", variant: :outline %> <%= sui.button "Three", variant: :secondary %> <% end %> <!-- gap: 4, justify: :between --> <%= sui.layout.row gap: 4, justify: :between, class: "w-full" do %> <%= sui.h3 "Settings" %> <%= sui.button "Save" %> <% end %> <!-- wrap: false --> <%= 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 %>
Grid
CSS grid. cols: maps to grid-cols-N.
Cell 1
Cell 2
Cell 3
<%= sui.layout.grid cols: 3, gap: 4 do %> <% 3.times do |i| %> <%= sui.card(class: "p-4") { sui.muted { "Cell #{i + 1}" } } %> <% end %> <% end %>
Revenue
—
Users
—
Projects
—
Uptime
—
<%= 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.
<%= sui.layout.container size: :sm, class: "border rounded-md p-6 bg-muted" do %> <%= sui.muted { "sm container — max-w-3xl, centered." } %> <% 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
<%= 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. |