Grid

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
EngineCSS GridFlexbox
Gutterhorizontal: column-gap (--jf-grid-gutter); vertical: row-gap = --jf-flowhorizontal: 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-gridequal 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.

.grid — the default

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.

md-6
md-6
md-4
md-4
md-4

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

sm-6 md-3 lg-8
sm-6 md-9 lg-4

Use .xs-{n} to size columns below the sm (600px) breakpoint.

xs-6
xs-6

.collapse removes the gutter — the same class as .row.collapse. .collapse-{bp} / .nocollapse-{bp} gate it by breakpoint — see Collapsing the gutter below.

md-3
md-9

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.

.auto-grid

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

one
two
three
four

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.

.row — the flex engine

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.

auto
sm-6
auto

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):

Short.
A longer paragraph that pushes this column taller than its neighbour, so without .equal-height they'd mismatch.

See Cards for the same pattern applied to real card markup.

Ordering

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

order-md-3
order-md-2
order-md-1

Collapsing the gutter

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

md-4
md-4
md-4

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:

md-3
md-3
sm-4 lg-0

Placement (grid only)

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

md-3
md-3 col-end
md-4 col-end

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.

Containers

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.

PropertyDefaultWhat it does
--jf-container-max1140px (1280px from xl)Content cap — the widest the content strip gets.
--jf-container-gutter--jf-space-md (24px), --jf-space-lg (48px) from smMinimum side space before the cap kicks in.
--jf-page-maxunset (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-spacecomputedmax(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.

The container must be centred in the viewport

The container sizes correctly wherever it sits, but --jf-bleed-space — and therefore every .bleed* — is measured against 100vw on the assumption that the gap from the container's content edge to each viewport edge is the same on both sides. Push the container off-centre and bleeds break: they overshoot one edge and stop short of the other.

Anything that offsets it does this — asymmetric padding / margin on an ancestor (e.g. padding-right: 300px on a wrapper), a fixed sidebar, a scrollbar on only one side. The fix is to not offset it: put the sidebar inside the container as a grid column (this docs site does exactly that), rather than beside it. There's no pure-CSS way to bleed to the viewport edge from an element that doesn't know how far off-centre it is.

Page cap (very wide screens)

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.

Full-bleed helpers

Any descendant of a .container can break out of it — the helpers apply a negative margin-inline of --jf-bleed-space:

ClassEffect
.bleedBoth edges to the page edge. Only meaningful in a full-width context (a bare .col, or outside a row/grid).
.bleed-start / .bleed-endOne 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-xlSame, but only from that breakpoint up (append any breakpoint key).
.bleed-none-md / .bleed-none-lgCancel 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:

Classic-scrollbar caveat

Bleeds are 100vw-based (a nested element can't measure the viewport otherwise), and 100vw includes the scrollbar. With an overlay scrollbar that's exact; with a classic scrollbar a full bleed's far edge sits under it. Put overflow-x: clip on <body> (not hiddenclip keeps position: sticky working) if a sliver of horizontal scroll shows. For pixel accuracy, a script setting --jf-bleed-space from innerWidth - documentElement.clientWidth is the only fully-reliable route.

Sections

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

Nesting

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.

inner md-6
inner md-6
outer md-4

Aligning cards

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.

Short heading

A little body copy.

A longer heading that wraps onto two lines

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.

Short heading

A little body copy.

Support: subgrid is Baseline; where it's missing the items just don't cross-align — still fully usable.

v4 → v5 migration

v4v5
.row as your only gridStill 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.
.collapseUnchanged — same class zeroes the gutter on .grid and .row, with .collapse-{bp} / .nocollapse-{bp} variants.
$cols, $grid-has-order, $grid-has-offsetGone. 12 columns and ordering are always generated; offsets are removed.
$base-col-name, $base-container-gutter, $base-col-gutterGone. 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 $breakpointsCustom 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-heightRemoved. 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 $breakpointsThe 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.

Overriding

All layout tuning is custom properties — set them at :root or on a scope (including inside your own @media).

Grid and container custom properties
PropertyDefaultNotes
--jf-grid-gutterspace-smspace-md at smColumn gap on both engines. Vertical space between wrapped rows is row-gap = --jf-flow on both.
--jf-container-max1140px1280px at xlContent cap. .container.is-wide lifts it to 1800px.
--jf-container-gutterspace-mdspace-lg at smMinimum side padding.
--jf-page-maxunsetOptional .page shell cap.
--jf-col-min18rem.auto-grid wrap threshold.
--jf-section-spacefluid(32px, 96px).section block padding. .is-tight (fluid(32px, 48px)) / .is-loose (fluid(48px, 128px)) repoint it.
--jf-bleed-spacecomputedHow far a .bleed* reaches. Computed from the container max — usually leave it.
--jf-col-spanset by the size classesInternal. 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.