Short heading
A little body copy.
Jellyfish ships two layout engines that share one class API: .grid (CSS Grid — the default) and .row (flexbox — for flex-specific behaviour).
Both arrange .col children across 12 columns using the same .{breakpoint}-{n} and .order-{breakpoint}-{n} modifiers. Reach for .grid unless you specifically need flex behaviour (grow-to-fill columns, content-sized columns) — then use .row or .auto-grid.
| .grid | .row | |
|---|---|---|
| Engine | CSS Grid | Flexbox |
| Gutter | horizontal: column-gap (--jf-grid-gutter); vertical: row-gap = --jf-flow | horizontal: negative margin + .col padding (--jf-grid-gutter); vertical: row-gap = --jf-flow |
| Bare .col (no size class) | full width — add .{bp}-{n} or use .auto-grid | equal share of the row from sm up |
| .{bp}-auto (content-sized) / .{bp}-0 (fill remaining) | — (use .row) | ✓ |
| Zero gutter | .collapse / .collapse-{bp} / .nocollapse-{bp} | |
| Placement | .col-start / .col-end | — |
| Bleed a sized column (.bleed-start / .bleed-end on a .col) | ✓ | — (flexbox can't override a fixed column width with a bleed) |
| Align internal rows across items | .subgrid-rows + --jf-subgrid-rows | — |
Removed in v5: .offset-* and the push/pull classes. Use .grid with .col-start / .col-end, a margin utility, or an empty spacer .col. See the migration table.
A bare .col spans the full width. Add breakpoint classes for columns. Jellyfish is mobile-first, so a class applies at its breakpoint and up.
.grid spaces itself: column-gap (--jf-grid-gutter) between columns, row-gap (--jf-flow) between wrapped rows, and a trailing margin-block-end of --jf-flow so it flows in the page like a paragraph (zeroed automatically as the last child of a .section / .card / .callout / .modal / .col, and through a .container sitting directly inside a .section). Direct children lose their own margins — a .card or .callout dropped straight in won't add a trailing gap — and inside a .col only the last child is stripped, so a column of stacked paragraphs keeps its internal rhythm without pushing space past the grid. Override .grid.grid > * (doubled class = matching specificity) to space your own way.
Mix breakpoints on one column — each overrides the smaller one.
Use .xs-{n} to size columns below the sm (600px) breakpoint.
.collapse removes the gutter — the same class as .row.collapse. .collapse-{bp} / .nocollapse-{bp} gate it by breakpoint — see Collapsing the gutter below.
There's no .grid equivalent of .{bp}-auto (content-sized) or .{bp}-0 (grow-to-fill) — CSS Grid can't size a track to its content inside a fixed 12-track template. Use .row for those.
Equal columns when you don't know how many there'll be — they wrap automatically once each would drop below a minimum width (18rem by default; override with --jf-col-min on the grid).
The .col wrapper is optional here and purely semantic — .auto-grid sizes every cell equally, so size classes like .md-4 don't apply. Drop cards or callouts straight in if you'd rather; either way .auto-grid strips the cell's trailing margin and spaces with column-gap / row-gap (--jf-flow), and carries its own trailing --jf-flow margin.
Columns without a size class share the row equally and grow to fill it — the behaviour CSS Grid can't do without knowing the count. .{bp}-{n} and ordering work exactly as in .grid; .{bp}-auto (content-sized) and .{bp}-0 (fill remaining) are .row-only.
Like .grid, the row spaces itself: row-gap (--jf-flow) between wrapped rows, a trailing margin-block-end of --jf-flow below the row, and the last child of each .col stripped of its trailing margin so the column's content doesn't push past the row. (Block axis only — a nested .row keeps its gutter.) .collapse zeroes the gutter and the row-gap.
.{bp}-0 resets a sized column back to "fill remaining space" (flex-only) — demoed together with breakpoint-gated .collapse in Collapsing the gutter below.
Flex children don't stretch to match height by default. Add .equal-height to the .row for that (.grid children are equal-height already):
See Cards for the same pattern applied to real card markup.
.order-{bp}-{n} works under both engines (it sets CSS order). Columns with no order class keep their source position; reset with .order-{bp}-0.
.collapse zeroes the gutter under both engines. Gate it to one breakpoint with .collapse-{bp} and undo it again from a larger breakpoint with .nocollapse-{bp} — same classes, same behaviour, on .grid and .row.
Resize the window: gutter below md, collapsed from md, restored again from lg. Same classes on .row, here combined with .{bp}-0 (flex-only) so the third column visibly grows once it switches from a fixed sm-4 to filling whatever space is left from lg up:
.col-end pins a column to the last grid line — the "push it to the right" case offsets used to cover. .col-start forces a column onto a new row. Both keep the column's span, and both take a breakpoint suffix — .col-end-{bp} / .col-start-{bp} — to apply only from that breakpoint up, same as .bleed-*-{bp}.
The reflow a breakpoint suffix produces is easiest to see on a full-width resize rather than in this sidebar-constrained column — see case 10 on the container & bleed test page.
A .container is full width with the side space applied as padding, computed from 100% so it's exact — content is precisely --jf-container-max once the page is wide enough, --jf-container-gutter otherwise. The knobs are custom properties — override at :root or on a scope, no recompile:
See it live: the container & bleed test page is a standalone harness (no sidebar, so the viewport maths is honest) with viewport rulers and a live readout for every case below.
| Property | Default | What it does |
|---|---|---|
| --jf-container-max | 1140px (1280px from xl) | Content cap — the widest the content strip gets. |
| --jf-container-gutter | --jf-space-md (24px), --jf-space-lg (48px) from sm | Minimum side space before the cap kicks in. |
| --jf-page-max | unset (no cap) | Optional cap on the whole page shell (with .page). The container adapts automatically; bleeds then reach the shell edge, not the viewport edge. |
| --jf-bleed-space | computed | max(gutter, (100vw - max) / 2) — viewport-based (so a bleed works at any nesting depth). Re-resolved on every .container against its own --jf-container-max (so .is-wide widens the bleed to match), and re-capped inside .page. Read it (via .bleed*), don't set it. |
.container.is-wide lifts --jf-container-max to 1800px; the padding maths is otherwise identical.
By default there's no cap — a full bleed reaches the viewport edge at any width. To stop bleeds spanning a 3000px monitor, set --jf-page-max and wrap your content in .page (or put its two lines on your own <main>):
Inside .page the container's 100% maths measures the shell, so it adapts with no extra config; --jf-bleed-space is re-capped to min(100vw, --jf-page-max) so bleeds stop at the shell edge.
Any descendant of a .container can break out of it — the helpers apply a negative margin-inline of --jf-bleed-space:
| Class | Effect |
|---|---|
| .bleed | Both edges to the page edge. Only meaningful in a full-width context (a bare .col, or outside a row/grid). |
| .bleed-start / .bleed-end | One edge out, the other stays aligned — the multi-column case, e.g. .bleed-end on the last column of a two-column layout. |
| .bleed-md / .bleed-start-lg / .bleed-end-xl … | Same, but only from that breakpoint up (append any breakpoint key). |
| .bleed-none-md / .bleed-none-lg … | Cancel any bleed from that breakpoint up. Combine, e.g. .bleed-end-md .bleed-none-lg = bleed between md and lg only. |
On a full-width block inside a .container the negative margin is all it takes. To bleed a sized column it must be a .grid column — .grid pairs the bleed with a width that overrides the column's fixed size, which flexbox can't. Put the class on the .col:
.section is a full-width band with consistent vertical space above and below (--jf-section-space, fluid 2–6rem). Compose it with a surface modifier for colour and an inner .container to keep content on the grid. Stacked sections with the same background collapse their seam to a single gap; a background change keeps both sections' full padding, so the colour edge reads as a deliberate boundary. Add .is-tight / .is-loose, or set --jf-section-space yourself, to change the spacing on one section.
See it live: the container & bleed test page has both the seam-collapse behaviour and the .is-tight / .is-loose spacing side by side.
Grids and rows nest freely. A nested .container resets its own padding to zero. Default to a .grid / .row containing only .col elements — that's what the sizing, ordering, placement and bleed classes above all target. Non-.col children still lay out (they just get 1 track each on .grid, or share the row equally on .row) — see Aligning cards below for a case that relies on exactly that.
Footers pin to the bottom of an equal-height card with .card-footer (margin-top: auto) — see Cards for the equal-height setup on each engine.
To line up internal rows too — a heading that wraps to two lines in one item but not its neighbour — add .subgrid-rows to the grid and set --jf-subgrid-rows to each item's direct-child count. It makes every item a subgrid of shared row tracks, so those rows align across the whole set. Works for any repeating component with a consistent structure (cards, media objects, pricing tiles…). Simplest as items sitting directly in the grid (no .col wrapper) — .auto-grid then sizes them all equally with no extra classes. A .col wrapper works too (on .grid, .subgrid-rows > * targets whatever's directly inside, .col included) — it just needs an explicit breakpoint size class, since a bare .col defaults to full width. Either way, every item needs the same --jf-subgrid-rows structure. Compose with .grid or .auto-grid; override grid-auto-rows: minmax(min-content, 1fr) on it if you want short rows to stretch rather than sit content-height.
A little body copy.
This item carries more body copy so it's taller overall — yet its heading, body and footer rows still align with the items either side, because each is a subgrid of the same three tracks.
A little body copy.
Support: subgrid is Baseline; where it's missing the items just don't cross-align — still fully usable.
| v4 | v5 |
|---|---|
| .row as your only grid | Still works — or switch the class to .grid for the CSS Grid engine (add explicit .{bp}-{n} to any column that relied on flex auto-sizing). |
| .offset-sm-3 | .grid + .col-start / .col-end, a spacer .col, or a margin utility. |
| .collapse | Unchanged — same class zeroes the gutter on .grid and .row, with .collapse-{bp} / .nocollapse-{bp} variants. |
| $cols, $grid-has-order, $grid-has-offset | Gone. 12 columns and ordering are always generated; offsets are removed. |
| $base-col-name, $base-container-gutter, $base-col-gutter | Gone. The base column class is xs; base gutters are the --jf-grid-gutter / --jf-container-gutter defaults. |
| Per-breakpoint container-width / -max-width / -gutter / col-gutter in $breakpoints | Custom properties: --jf-container-max, --jf-container-gutter, --jf-grid-gutter. Override at :root / a scope in your own @media. |
| Per-breakpoint root-font-size / root-line-height | Removed. Root type is 16px / 1.5 everywhere. Reintroduce responsive root type by overriding --jf-root-font-size / --jf-root-line-height in your own @media. |
| Re-keying $breakpoints | The map is fixed at 600 / 900 / 1200 / 1800. The media mixins still take a raw px value (@include above(950px)), so one-off widths are fine — you just don't get generated column classes for them. Container queries cover the rest. |
All layout tuning is custom properties — set them at :root or on a scope (including inside your own @media).
| Property | Default | Notes |
|---|---|---|
| --jf-grid-gutter | space-sm → space-md at sm | Column gap on both engines. Vertical space between wrapped rows is row-gap = --jf-flow on both. |
| --jf-container-max | 1140px → 1280px at xl | Content cap. .container.is-wide lifts it to 1800px. |
| --jf-container-gutter | space-md → space-lg at sm | Minimum side padding. |
| --jf-page-max | unset | Optional .page shell cap. |
| --jf-col-min | 18rem | .auto-grid wrap threshold. |
| --jf-section-space | fluid(32px, 96px) | .section block padding. .is-tight (fluid(32px, 48px)) / .is-loose (fluid(48px, 128px)) repoint it. |
| --jf-bleed-space | computed | How far a .bleed* reaches. Computed from the container max — usually leave it. |
| --jf-col-span | set by the size classes | Internal. How many of the 12 tracks a .grid column spans — the .{bp}-{n} classes set it. Use the classes; this is documented only so an unfamiliar value in devtools is identifiable. |