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.

Stack

Vertical flex, no wrap. gap: maps to Tailwind's spacing scale; align: maps to items-*.

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