Blocks

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-*.

First Second Third

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

Tag 1 Tag 2 Tag 3 Tag 4 Tag 5 Tag 6 Tag 7 Tag 8 Tag 9 Tag 10 Tag 11 Tag 12
<!-- 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

MethodKwargsDefaultNotes
stackgap:, align:Vertical flex, no wrap.
rowgap:, align:, justify:, wrap:align: :center, wrap: trueHorizontal flex, wraps + centers by default.
gridcols:, gap:CSS grid.
containersize::mdMax-width wrapper, centered, horizontal padding.