Components
Auto Grid
A responsive, intrinsically-wrapping grid of equal cells with no breakpoints. Children flow into as many columns as fit at a configurable minimum item width, then grow to share the leftover space — the card-grid recipe, built on CSS Grid's repeat(auto-fit, minmax()) (the "RAM" pattern). Pure CSS, zero JavaScript.
Installation
1. Install via the shadcn CLI
npx shadcn@latest add http://localhost/r/auto-grid.json2. Use it
import { AutoGrid } from "@/components/ui/auto-grid"
// Card grid — as many 16rem columns as fit, no breakpoints.
<AutoGrid>
<div>…</div>
<div>…</div>
</AutoGrid>
// Wider items, larger gap, semantic list.
<AutoGrid as="ul" min="20rem" gap={6}>
<li>…</li>
</AutoGrid>
// Keep empty trailing tracks (auto-fill) instead of stretching.
<AutoGrid fill min="12rem">
<div>…</div>
</AutoGrid>Or copy the source manually
/** @jsxImportSource hono/jsx */
import type { PropsWithChildren } from "hono/jsx"
import { cn, type ClassValue } from "@/registry/lib/cn"
// Auto Grid — shadcn-htmx, htmx v4 + Tailwind v4.
//
// A responsive, intrinsically-wrapping grid of equal cells with NO media
// queries: children flow into as many columns as fit at a configurable
// minimum item width, then grow to share the leftover space. This is the
// "RAM" pattern (Repeat, Auto, Minmax) — the card-grid recipe most people
// reach for. Pure CSS Grid; zero JavaScript.
//
// Built on (read before editing):
// repos/web.dev/src/site/content/en/patterns/layout/repeat-auto-minmax/index.md
// — the canonical recipe: `grid-template-columns: repeat(auto-fit,
// minmax(150px, 1fr))`. auto-fit collapses empty tracks so filled
// tracks grow; auto-fill keeps empty tracks (their width reserved).
// repos/web.dev/src/site/content/en/learn/css/grid/index.md:295-389
// — `minmax(0, 1fr)` forces equal share minus gaps; the
// `auto-fill`/`auto-fit` keywords create "as many tracks as will fit"
// with no media queries; the subtle auto-fill vs auto-fit difference.
// repos/mdn/files/en-us/web/css/minmax/index.md (minmax function)
// repos/mdn/files/en-us/web/css/min/index.md
// — `min(var(--auto-grid-min), 100%)` guards the lower bound so a single
// wide item can never overflow a container narrower than the min.
//
// shadcn/ui has no Auto Grid (React libraries leave layout to the consumer),
// so there is no class string to mirror — this is a layout primitive.
//
// API shape:
// - `min` : the per-item minimum width (any CSS length). Drives how many
// columns fit. Default "16rem".
// - `gap` : the Tailwind gap step (number → gap-<n>) or a class. Default 4.
// - `fill` : false (default) uses auto-fit — empty tracks collapse and
// real items stretch to fill the row. true uses auto-fill —
// empty tracks are kept, so a half-empty last row stays aligned
// to the column rhythm rather than stretching.
// - `as` : the element/role. Default <div>. Use "ul"/"ol" for a list of
// cards (each child should then be an <li>), or "section".
//
// We publish `--auto-grid-min` on the root and read it in an arbitrary
// `grid-template-columns` utility, exactly like RangeSlider publishes
// `--range-min`/`--range-max`. No runtime; the browser does the layout.
export type AutoGridAs = "div" | "ul" | "ol" | "section"
// repeat(auto-fit|auto-fill, minmax(min(var(--auto-grid-min), 100%), 1fr)).
// The min() guard means: never demand more than the container width, so one
// item in a too-narrow container shrinks instead of forcing a scrollbar.
const FIT_CLASS =
"grid [grid-template-columns:repeat(auto-fit,minmax(min(var(--auto-grid-min,16rem),100%),1fr))]"
const FILL_CLASS =
"grid [grid-template-columns:repeat(auto-fill,minmax(min(var(--auto-grid-min,16rem),100%),1fr))]"
// Gap presets so callers can pass a plain number; any other value (string)
// is treated as an explicit class and appended verbatim.
const GAP_CLASS: Record<number, string> = {
0: "gap-0",
1: "gap-1",
2: "gap-2",
3: "gap-3",
4: "gap-4",
5: "gap-5",
6: "gap-6",
8: "gap-8",
10: "gap-10",
12: "gap-12",
}
type AutoGridProps = PropsWithChildren<{
// Minimum per-item width. Any CSS length ("16rem", "200px", "20ch").
min?: string
// Gap between cells: a number maps to gap-<n>; a string is used verbatim.
gap?: number | string
// auto-fill (keep empty tracks) instead of auto-fit (collapse them).
fill?: boolean
// Semantic element / role. "ul"/"ol" for a card list.
as?: AutoGridAs
ariaLabel?: string
ariaLabelledby?: string
class?: ClassValue
id?: string
[key: `hx-${string}`]: any
[key: `data-${string}`]: any
[key: `aria-${string}`]: any
}>
export function AutoGrid(props: AutoGridProps) {
const {
min = "16rem",
gap = 4,
fill = false,
as = "div",
ariaLabel,
ariaLabelledby,
class: className,
children,
...rest
} = props as any
const Tag: any = as
const gapClass = typeof gap === "number" ? (GAP_CLASS[gap] ?? "gap-4") : gap
return (
<Tag
data-slot="auto-grid"
data-fill={fill ? "true" : undefined}
// Publish the per-item minimum; the grid-template-columns utility reads
// it. Keeping it a custom property means callers tune density without
// touching the (uncompilable-at-runtime) arbitrary class.
style={`--auto-grid-min:${min}`}
aria-label={ariaLabel}
aria-labelledby={ariaLabelledby}
class={cn(fill ? FILL_CLASS : FIT_CLASS, gapClass, className)}
{...rest}
>
{children}
</Tag>
)
}
1. Save the file
Copy auto-grid.html into templates/components/.
2. Use it
{% from "components/auto-grid.html" import auto_grid %}
{% call auto_grid() %}
<div>…</div>
{% endcall %}
{% call auto_grid(min="20rem", gap="gap-6", tag="ul") %}
<li>…</li>
{% endcall %}View source
{# Auto Grid macro — shadcn-htmx, htmx v4 + Tailwind v4.
Mirrors registry/ui/auto-grid.tsx.
A responsive, intrinsically-wrapping grid of equal cells with no media
queries (the RAM pattern: repeat, auto-fit/auto-fill, minmax). Pure CSS.
Usage:
{% from "components/auto-grid.html" import auto_grid %}
{% call auto_grid() %} {# default: 16rem min, gap-4 #}
<div>…card…</div>
{% endcall %}
{% call auto_grid(min="20rem", gap="gap-6", fill=true, tag="ul") %}
<li>…</li>
{% endcall %}
Args:
min per-item minimum width (any CSS length). Default "16rem".
gap a gap-* class (string). Default "gap-4".
fill true → auto-fill (keep empty tracks); false → auto-fit. Default false.
tag div | ul | ol | section. Default "div".
attrs dict of extra attributes (hx-*, data-*, aria-*). #}
{% macro auto_grid(
min="16rem",
gap="gap-4",
fill=false,
tag="div",
aria_label=none,
aria_labelledby=none,
extra_class="",
attrs={}
) %}
{%- set tracks -%}
{% if fill %}repeat(auto-fill,minmax(min(var(--auto-grid-min,16rem),100%),1fr)){% else %}repeat(auto-fit,minmax(min(var(--auto-grid-min,16rem),100%),1fr)){% endif %}
{%- endset -%}
<{{ tag }}
data-slot="auto-grid"
{%- if fill %} data-fill="true"{% endif %}
style="--auto-grid-min:{{ min }}"
{%- if aria_label %} aria-label="{{ aria_label }}"{% endif %}
{%- if aria_labelledby %} aria-labelledby="{{ aria_labelledby }}"{% endif %}
{%- for k, v in attrs.items() %} {{ k|replace('_','-') }}="{{ v }}"{% endfor %}
class="grid [grid-template-columns:{{ tracks }}] {{ gap }} {{ extra_class }}">
{{ caller() }}
</{{ tag }}>
{% endmacro %}
1. Save the file
Add auto-grid.tmpl alongside your templates.
2. Use it
{{template "auto-grid" (dict "Body" (htmlSafe $cards))}}
{{template "auto-grid" (dict "Min" "20rem" "Gap" "gap-6" "Tag" "ul" "Body" (htmlSafe $items))}}
{{template "auto-grid" (dict "Fill" true "Min" "12rem" "Body" (htmlSafe $cells))}}View source
{{/*
Auto Grid template — shadcn-htmx, htmx v4 + Tailwind v4.
Mirrors registry/ui/auto-grid.tsx.
A responsive, intrinsically-wrapping grid of equal cells with no media
queries (the RAM pattern: repeat, auto-fit/auto-fill, minmax). Pure CSS.
type AutoGridArgs struct {
Min string // per-item minimum width (any CSS length); default "16rem"
Gap string // a gap-* class; default "gap-4"
Fill bool // true = auto-fill (keep empty tracks); false = auto-fit
Tag string // div | ul | ol | section; default "div"
Class string // extra classes appended to the root
Body string // inner HTML (use htmlSafe)
}
Usage:
{{template "auto-grid" (dict "Body" (htmlSafe $cards))}}
{{template "auto-grid" (dict "Min" "20rem" "Gap" "gap-6" "Fill" true "Tag" "ul" "Body" (htmlSafe $items))}}
*/}}
{{define "auto-grid"}}
{{- $min := or .Min "16rem" -}}
{{- $gap := or .Gap "gap-4" -}}
{{- $tag := or .Tag "div" -}}
{{- $tracks := "repeat(auto-fit,minmax(min(var(--auto-grid-min,16rem),100%),1fr))" -}}
{{- if .Fill}}{{- $tracks = "repeat(auto-fill,minmax(min(var(--auto-grid-min,16rem),100%),1fr))"}}{{end -}}
<{{$tag}} data-slot="auto-grid"{{if .Fill}} data-fill="true"{{end}} style="--auto-grid-min:{{$min}}"{{if .AriaLabel}} aria-label="{{.AriaLabel}}"{{end}}{{if .AriaLabelledby}} aria-labelledby="{{.AriaLabelledby}}"{{end}} class="grid [grid-template-columns:{{$tracks}}] {{$gap}}{{if .Class}} {{.Class}}{{end}}">{{htmlSafe .Body}}</{{$tag}}>
{{end}}
1. Save the file
Drop auto_grid.ex into lib/my_app_web/components/.
2. Use it
<.auto_grid>
<div :for={item <- @items}>…</div>
</.auto_grid>
<.auto_grid min="20rem" gap="gap-6" tag="ul">
<li :for={item <- @items}>…</li>
</.auto_grid>
<.auto_grid fill min="12rem">
<div :for={item <- @items}>…</div>
</.auto_grid>View source
defmodule ShadcnHtmx.Components.AutoGrid do
@moduledoc """
Auto Grid — shadcn-htmx, htmx v4 + Tailwind v4 for Phoenix.
Mirrors registry/ui/auto-grid.tsx.
A responsive, intrinsically-wrapping grid of equal cells with no media
queries — children flow into as many columns as fit at a configurable
minimum item width, then grow to share the leftover space. This is the
"RAM" pattern (Repeat, Auto, Minmax). Pure CSS Grid; zero JavaScript.
- `min` — per-item minimum width (any CSS length). Default "16rem".
- `gap` — a gap-* class. Default "gap-4".
- `fill` — true → auto-fill (keep empty tracks); false → auto-fit
(collapse them so real items stretch). Default false.
- `tag` — div | ul | ol | section. Default "div".
## Examples
<.auto_grid>
<div :for={item <- @items}>…</.auto_grid>
<.auto_grid min="20rem" gap="gap-6" fill tag="ul">
<li :for={item <- @items}>…</li>
</.auto_grid>
"""
use Phoenix.Component
attr :min, :string, default: "16rem"
attr :gap, :string, default: "gap-4"
attr :fill, :boolean, default: false
attr :tag, :string, default: "div", values: ~w(div ul ol section)
attr :class, :string, default: nil
attr :rest, :global, include: ~w(aria-label aria-labelledby)
slot :inner_block, required: true
def auto_grid(assigns) do
tracks =
if assigns.fill do
"repeat(auto-fill,minmax(min(var(--auto-grid-min,16rem),100%),1fr))"
else
"repeat(auto-fit,minmax(min(var(--auto-grid-min,16rem),100%),1fr))"
end
assigns = assign(assigns, :tracks, tracks)
~H"""
<.dynamic_tag
tag_name={@tag}
data-slot="auto-grid"
data-fill={if @fill, do: "true"}
style={"--auto-grid-min:#{@min}"}
class={["grid", "[grid-template-columns:#{@tracks}]", @gap, @class]}
{@rest}
>
{render_slot(@inner_block)}
</.dynamic_tag>
"""
end
end
1. Save the file
Paste the markup; relies only on theme tokens.
2. Use it
<div data-slot="auto-grid"
style="--auto-grid-min:16rem"
class="grid [grid-template-columns:repeat(auto-fit,minmax(min(var(--auto-grid-min,16rem),100%),1fr))] gap-4">
<div>…</div>
</div>View source
<!--
shadcn-htmx — raw HTML Auto Grid snippet.
Mirrors registry/ui/auto-grid.tsx.
A responsive, intrinsically-wrapping grid of equal cells with NO media
queries (the RAM pattern: repeat, auto-fit/auto-fill, minmax). Pure CSS;
no script. Relies only on theme tokens.
Tune density with the --auto-grid-min custom property in the inline style.
The grid-template-columns utility reads it:
repeat(auto-fit, minmax(min(var(--auto-grid-min), 100%), 1fr)) → collapse empty tracks
repeat(auto-fill, minmax(min(var(--auto-grid-min), 100%), 1fr)) → keep empty tracks
-->
<!-- Default: 16rem minimum per item, gap-4, auto-fit (items stretch) -->
<div data-slot="auto-grid"
style="--auto-grid-min:16rem"
class="grid [grid-template-columns:repeat(auto-fit,minmax(min(var(--auto-grid-min,16rem),100%),1fr))] gap-4">
<div class="rounded-lg border bg-card p-4 text-card-foreground">Item 1</div>
<div class="rounded-lg border bg-card p-4 text-card-foreground">Item 2</div>
<div class="rounded-lg border bg-card p-4 text-card-foreground">Item 3</div>
<div class="rounded-lg border bg-card p-4 text-card-foreground">Item 4</div>
</div>
<!-- As a list of cards: <ul> root, <li> children, wider min, larger gap -->
<ul data-slot="auto-grid"
style="--auto-grid-min:20rem"
class="grid [grid-template-columns:repeat(auto-fit,minmax(min(var(--auto-grid-min,16rem),100%),1fr))] gap-6">
<li class="rounded-lg border bg-card p-4 text-card-foreground">Card A</li>
<li class="rounded-lg border bg-card p-4 text-card-foreground">Card B</li>
<li class="rounded-lg border bg-card p-4 text-card-foreground">Card C</li>
</ul>
<!-- auto-fill: empty trailing tracks are kept so a half-empty row stays
aligned to the column rhythm instead of stretching -->
<div data-slot="auto-grid" data-fill="true"
style="--auto-grid-min:12rem"
class="grid [grid-template-columns:repeat(auto-fill,minmax(min(var(--auto-grid-min,16rem),100%),1fr))] gap-4">
<div class="rounded-lg border bg-card p-4 text-card-foreground">One</div>
<div class="rounded-lg border bg-card p-4 text-card-foreground">Two</div>
</div>
Examples
Card grid — no media queries
Children flow into as many columns as fit at the min item width, then stretch to fill the row. Resize the window and watch the column count change with zero breakpoints.
The whole layout is one line of CSS: grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)). web.dev calls this the RAM pattern — Repeat, Auto-fit, Minmax. Each track is at least 16rem and at most 1fr, so on a narrow screen items take the full width and as the container grows they snap onto the same row. We wrap the lower bound in min(16rem, 100%) so a single item can never overflow a container narrower than the minimum.
<AutoGrid>
<div>One</div>
<div>Two</div>
<div>Three</div>
<div>Four</div>
<div>Five</div>
<div>Six</div>
</AutoGrid>{% call auto_grid() %}
<div>One</div>
<div>Two</div>
<div>Three</div>
{% endcall %}{{template "auto-grid" (dict "Body" (htmlSafe $cards))}}<.auto_grid>
<div :for={item <- @items}>{item}</div>
</.auto_grid><div data-slot="auto-grid" style="--auto-grid-min:16rem" class="grid [grid-template-columns:repeat(auto-fit,minmax(min(var(--auto-grid-min,16rem),100%),1fr))] gap-4 w-full" data-test="basic">
<div class="flex min-h-20 items-center justify-center rounded-lg border bg-card p-4 text-sm font-medium text-card-foreground">One</div>
<div class="flex min-h-20 items-center justify-center rounded-lg border bg-card p-4 text-sm font-medium text-card-foreground">Two</div>
<div class="flex min-h-20 items-center justify-center rounded-lg border bg-card p-4 text-sm font-medium text-card-foreground">Three</div>
<div class="flex min-h-20 items-center justify-center rounded-lg border bg-card p-4 text-sm font-medium text-card-foreground">Four</div>
<div class="flex min-h-20 items-center justify-center rounded-lg border bg-card p-4 text-sm font-medium text-card-foreground">Five</div>
<div class="flex min-h-20 items-center justify-center rounded-lg border bg-card p-4 text-sm font-medium text-card-foreground">Six</div>
</div>Further reading
auto-fill vs auto-fit
With only two items: auto-fit (default) collapses empty tracks so the two cells stretch across the row; auto-fill keeps the empty tracks so the cells stay at their minimum width, aligned to the column rhythm.
The only difference is what happens to empty tracks. Per the web.dev "Learn CSS — Grid" course: auto-fit collapses unused tracks to 0 so the filled tracks grow to consume the space; auto-fill leaves the empty tracks at their reserved width. Reach for fill when a half-empty last row should keep the same item size as the full rows above it.
auto-fit (default) — items stretch
auto-fill — empty tracks kept
// auto-fit (default): the two items stretch to fill the row
<AutoGrid min="10rem">
<div>A</div>
<div>B</div>
</AutoGrid>
// auto-fill: empty tracks are reserved, items stay at min width
<AutoGrid fill min="10rem">
<div>A</div>
<div>B</div>
</AutoGrid>{% call auto_grid(min="10rem") %}…{% endcall %}
{% call auto_grid(min="10rem", fill=true) %}…{% endcall %}{{template "auto-grid" (dict "Min" "10rem" "Body" (htmlSafe $b))}}
{{template "auto-grid" (dict "Min" "10rem" "Fill" true "Body" (htmlSafe $b))}}<.auto_grid min="10rem">…</.auto_grid>
<.auto_grid min="10rem" fill>…</.auto_grid><div class="w-full space-y-4">
<div class="space-y-1.5">
<p class="text-xs font-medium text-muted-foreground">auto-fit (default) — items stretch</p>
<div data-slot="auto-grid" style="--auto-grid-min:10rem" class="grid [grid-template-columns:repeat(auto-fit,minmax(min(var(--auto-grid-min,16rem),100%),1fr))] gap-4" data-test="fit">
<div class="flex min-h-20 items-center justify-center rounded-lg border bg-card p-4 text-sm font-medium text-card-foreground">A</div>
<div class="flex min-h-20 items-center justify-center rounded-lg border bg-card p-4 text-sm font-medium text-card-foreground">B</div>
</div>
</div>
<div class="space-y-1.5">
<p class="text-xs font-medium text-muted-foreground">auto-fill — empty tracks kept</p>
<div data-slot="auto-grid" data-fill="true" style="--auto-grid-min:10rem" class="grid [grid-template-columns:repeat(auto-fill,minmax(min(var(--auto-grid-min,16rem),100%),1fr))] gap-4" data-test="fill">
<div class="flex min-h-20 items-center justify-center rounded-lg border bg-card p-4 text-sm font-medium text-card-foreground">A</div>
<div class="flex min-h-20 items-center justify-center rounded-lg border bg-card p-4 text-sm font-medium text-card-foreground">B</div>
</div>
</div>
</div>Further reading
Density — the min width drives the column count
A smaller min item width packs more columns into the same container; a larger one yields fewer, wider columns. Set it with the min prop (any CSS length).
There are no per-breakpoint column counts to maintain — you describe the smallest acceptable item and the browser derives the columns. Use a list element (as="ul") when the cells are a genuine list so assistive tech announces the count.
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
<AutoGrid as="ul" min="7rem" gap={3} aria-label="Swatches">
<li>1</li>
<li>2</li>
<li>3</li>
{/* … */}
</AutoGrid>{% call auto_grid(min="7rem", gap="gap-3", tag="ul", aria_label="Swatches") %}
<li>1</li>
{% endcall %}{{template "auto-grid" (dict "Min" "7rem" "Gap" "gap-3" "Tag" "ul" "AriaLabel" "Swatches" "Body" (htmlSafe $items))}}<.auto_grid tag="ul" min="7rem" gap="gap-3" aria-label="Swatches">
<li :for={n <- 1..8}>{n}</li>
</.auto_grid><ul data-slot="auto-grid" style="--auto-grid-min:7rem" aria-label="Swatches" class="grid [grid-template-columns:repeat(auto-fit,minmax(min(var(--auto-grid-min,16rem),100%),1fr))] gap-3 w-full" data-test="density">
<li class="flex min-h-20 items-center justify-center rounded-lg border bg-card p-4 text-sm font-medium text-card-foreground">1</li>
<li class="flex min-h-20 items-center justify-center rounded-lg border bg-card p-4 text-sm font-medium text-card-foreground">2</li>
<li class="flex min-h-20 items-center justify-center rounded-lg border bg-card p-4 text-sm font-medium text-card-foreground">3</li>
<li class="flex min-h-20 items-center justify-center rounded-lg border bg-card p-4 text-sm font-medium text-card-foreground">4</li>
<li class="flex min-h-20 items-center justify-center rounded-lg border bg-card p-4 text-sm font-medium text-card-foreground">5</li>
<li class="flex min-h-20 items-center justify-center rounded-lg border bg-card p-4 text-sm font-medium text-card-foreground">6</li>
<li class="flex min-h-20 items-center justify-center rounded-lg border bg-card p-4 text-sm font-medium text-card-foreground">7</li>
<li class="flex min-h-20 items-center justify-center rounded-lg border bg-card p-4 text-sm font-medium text-card-foreground">8</li>
</ul>Further reading
API Reference
<AutoGrid>
| Prop | Type | Default | Description |
|---|---|---|---|
min | string | "16rem" | Minimum per-item width (any CSS length, e.g. "16rem", "200px", "20ch"). Drives how many columns fit. Published as the --auto-grid-min custom property and read inside minmax().MDNminmax() |
gap | number|string | 4 | Gap between cells. A number maps to gap-<n> (0,1,2,3,4,5,6,8,10,12); a string is appended verbatim as a class. |
fill | boolean | false | Use auto-fill (keep empty trailing tracks) instead of auto-fit (collapse them so real items stretch to fill the row).MDNrepeat() |
as | "div"|"ul"|"ol"|"section" | "div" | Semantic element / role for the grid container. Use ul/ol when the cells are a genuine list (children should be <li>). |
ariaLabel | string | — | Accessible name when no visible <label>.MDNaria-label |
ariaLabelledby | string | — | Id of a visible element providing the accessible name.MDNaria-labelledby |
class | string | — | Extra Tailwind classes appended to the root element. |
hx-* | any | — | Any htmx attribute. Forwarded onto the underlying element.htmxAttribute reference |