Grids
feature-grid, bento-grid, card-carousel and pricing-table — laying several things out at once.
feature-grid
Equal-weight cards. The workhorse.
Everything you need
Included in the engine, not bolted on.
type: feature-grid
heading: Everything you need
subhead: Included in the engine, not bolted on.
columns: 3
items:
- icon: file-text
title: Content model
body: YAML for structure, MDX for prose. No database.
href: /guide/content-model
- icon: layout-grid
title: Nineteen sections
body: Openers, grids, narrative, proof and media.
href: /sections
- icon: palette
title: Fourteen themes
body: Palette, type, geometry and shadow, switched together.
href: /guide/themesicon is a lucide name in kebab-case. Icons resolve on
the server, so none of the icon library reaches the browser, and an unknown
name fails the build naming the export it looked for.
bento-grid
Asymmetric tiles. Use it where a few things genuinely matter more than the
others — that is the difference from feature-grid, which weights everything
equally.
One engine, not five tools
A folder is the whole input
site.yaml, a menu, some sections, a guide folder. That is the build.

Themes
Fourteen of them, switchable at runtime.
Search
Built at compile time, loaded on first keystroke.
Motion
Three presets, and reduced-motion always wins.
type: bento-grid
heading: One engine, not five tools
items:
- title: A folder is the whole input
body: site.yaml, a menu, some sections, a guide folder. That is the build.
image: assets/bento-model.png
span: large
href: /guide/content-model
- title: Themes
body: Fourteen of them, switchable at runtime.
icon: palette
span: normal
background: tinted
- title: Search
body: Built at compile time, loaded on first keystroke.
icon: search
span: normal
- title: Motion
body: Three presets, and reduced-motion always wins.
icon: sparkles
span: wideSpans are cells on a four-column grid: normal is 1×1, wide 2×1, tall 1×2,
large 2×2. On a tablet the grid drops to two columns and large becomes
wide; on a phone everything is one column.
A tile with an image puts it at the foot and lets it fill the remaining
height, which is what makes a large tile read as one composition rather than
a card with a picture stuck under it.
card-carousel
A horizontally scrolling row, snapped, with the next card peeking past the container edge so the row reads as scrollable before anything moves.
Why teams pick Zellij
Everything below ships in the engine.
Content
Write YAML, not components.
A section is a file with a type and some fields.

Sections
Nineteen ways to say it.
Each one is documented with its source and its output.

Themes
Fourteen looks, no redesign.
Swap a single line in site.yaml.

Guides
MDX with the parts you actually want.
Callouts, steps, tabs, code groups and a table of contents.

CLI
Check before you ship.
One command validates the whole content folder.

type: card-carousel
heading: Why teams pick Zellij
subhead: Everything below ships in the engine.
link: { label: Browse the guide, href: /guide }
cardWidth: medium
items:
- eyebrow: Content
title: Write YAML, not components.
body: A section is a file with a type and some fields.
image: assets/card-content.png
href: /guide/content-model
- eyebrow: Sections
title: Nineteen ways to say it.
body: Each one is documented with its source and its output.
image: assets/card-sections.png
href: /sections
- eyebrow: Themes
title: Fourteen looks, no redesign.
body: Swap a single line in site.yaml.
image: assets/card-themes.png
background: tinted
- eyebrow: Guides
title: MDX with the parts you actually want.
body: Callouts, steps, tabs, code groups and a table of contents.
image: assets/card-guides.png
href: /guide
- eyebrow: CLI
title: Check before you ship.
body: One command validates the whole content folder.
image: assets/card-cli.png
href: /cliThe track is focusable so it can be scrolled by keyboard even when the cards inside are not links.
promo-grid
Large linked tiles, each carrying its own artwork and its own destination. This is the landing-page format a consumer product site uses: the tiles are the navigation, and the page has almost no prose of its own.

The engine
A folder of content is a website.
Point it at a directory and it builds the marketing pages and the docs.

type: promo-grid
gap: tight
items:
- eyebrow: The engine
title: A folder of content is a website.
body: Point it at a directory and it builds the marketing pages and the docs.
image: assets/example/promo-wide.png
alt: ""
width: full
height: short
align: top
tone: dark
scrim: 0.5
links:
- { label: Read the guide, href: /guide }
- { label: Browse sections, href: /sections }
- eyebrow: Themes
title: Fourteen of them.
image: assets/example/promo-1.png
alt: ""
width: half
height: short
align: top
tone: dark
scrim: 0.45
links:
- { label: Learn more, href: /guide/themes }
- eyebrow: CLI
title: Check before you ship.
image: assets/example/promo-2.png
alt: ""
width: half
height: short
align: top
tone: light
scrim: 0
href: /cliWidths are full or half, and two half tiles pair up on a wide viewport and
stack below it. height picks short, tall or full — full is a tile that
fills the viewport, which is what makes a page of them read as a sequence of
screens rather than a list.
A tile links in one of two ways, and they are exclusive: give it links for up
to two labelled links laid over the artwork, or give it href to make the whole
tile one big target. A link inside a link is not a thing.
pricing-table
One to four tiers.
Simple pricing
Per seat, per month.
Most popular
Team
$12per user / month
For a team shipping regularly.
Start a trial- Unlimited users
- SSO
- Priority support
type: pricing-table
heading: Simple pricing
subhead: Per seat, per month.
tiers:
- name: Free
price: "$0"
period: forever
description: For trying it out.
features: [Up to 3 users, Community support]
cta: { label: Start free, href: /guide }
- name: Team
price: "$12"
period: per user / month
description: For a team shipping regularly.
features: [Unlimited users, SSO, Priority support]
cta: { label: Start a trial, href: /guide }
highlighted: true
- name: Enterprise
price: Contact us
description: For everything else.
features: [Audit log, Custom retention, A person to call]
cta: { label: Talk to us, href: /guide }Exactly one tier may be highlighted. A single "Contact us" tier renders
gracefully — the layout does not assume three.
price is free text, so $12, From €9 and Contact us all belong. A bare
number is accepted too and stringified, since YAML would otherwise hand it over
as a number.
code-cards
A row of cards whose snippets are the point. For comparing options that differ mainly in what you have to write down — CI modes, deployment targets, auth strategies.
Deployment
Three ways to put it somewhere
Each one is the same content folder. What differs is the file you commit to make it happen.
Static
A directory of files
Any host that serves files. Netlify, Cloudflare Pages, S3, a web server.
.github/workflows/docs.yml
- run: zel build docs --out dist
- uses: nwtgck/actions-netlify@v3
with:
publish-dir: distContainer
An image that serves itself
For a platform that runs containers rather than hosting files.
.github/workflows/image.yml
- run: zel build docs --target image \
--tag ghcr.io/acme/docs:$GITHUB_SHA
- run: docker push ghcr.io/acme/docs:$GITHUB_SHAWrapper
A Next.js app you own
When you want your own routes beside the generated ones.
package.json
{
"scripts": {
"build": "zellij prepare && next build"
}
}zel ci init writes whichever of these you pick, so the snippets are what you end up with rather than what you have to type.
type: code-cards
eyebrow: Deployment
heading: Three ways to put it somewhere
subhead: >-
Each one is the same content folder. What differs is the file you commit to
make it happen.
cards:
- eyebrow: Static
title: A directory of files
body: Any host that serves files. Netlify, Cloudflare Pages, S3, a web server.
code:
title: .github/workflows/docs.yml
body: |
- run: zel build docs --out dist
- uses: nwtgck/actions-netlify@v3
with:
publish-dir: dist
- eyebrow: Container
title: An image that serves itself
featured: true
body: For a platform that runs containers rather than hosting files.
code:
title: .github/workflows/image.yml
body: |
- run: zel build docs --target image \
--tag ghcr.io/acme/docs:$GITHUB_SHA
- run: docker push ghcr.io/acme/docs:$GITHUB_SHA
- eyebrow: Wrapper
title: A Next.js app you own
body: When you want your own routes beside the generated ones.
code:
title: package.json
body: |
{
"scripts": {
"build": "zellij prepare && next build"
}
}
footnote: >-
`zel ci init` writes whichever of these you pick, so the snippets are what you
end up with rather than what you have to type.Two to four cards. One is not a comparison, and a fifth pushes every card below the width at which its snippet still makes sense wrapped.
featured: true marks the one you recommend, with the accent rather than a
badge — a badge needs a word, and the word is already in the card.
The snippets are <pre> elements, so they can be selected, copied and read
aloud. They are not syntax-highlighted: Shiki runs at build time over MDX,
and reaching it from a YAML section would mean either shipping a highlighter to
the browser or threading a build step through the content loader — for snippets
that are mostly keys and paths.
matrix
Two axes crossed, with a sentence in every cell. For the page whose real claim is that the grid is full — that every combination of the two choices is legal.
Every target, every build mode
The two choices are independent, which is the point a list of six bullets cannot make.
| Target ↓Runs on → | Local | CI | Container |
|---|---|---|---|
| staticA directory of files. | zel build docs --out dist | Committed workflow runs the same command. | Served by any web server image. |
| imageA container that serves itself. | Needs a runtime — zel doctor says which. | Built and pushed by the pipeline. | The output is the container. |
Both rows read the same content folder. Nothing about the content changes when the target does.
type: matrix
heading: Every target, every build mode
subhead: >-
The two choices are independent, which is the point a list of six bullets
cannot make.
rowAxis: Target
columnAxis: Runs on
columns:
- { label: Local }
- { label: CI }
- { label: Container }
rows:
- label: static
description: A directory of files.
cells:
- "`zel build docs --out dist`"
- Committed workflow runs the same command.
- Served by any web server image.
- label: image
description: A container that serves itself.
cells:
- "Needs a runtime — `zel doctor` says which."
- Built and pushed by the pipeline.
- "The output *is* the container."
footnote: >-
Both rows read the same content folder. Nothing about the content changes
when the target does.A list of the six combinations makes the same claim, and a reader has to count to check it. A grid makes it visible at a glance, which is the whole reason to spend the width.
It renders a real <table>: column names are <th scope="col">, each row's
name is <th scope="row">. That is not markup pedantry — it is what makes a
screen reader announce "passphrase, portal-key" before reading a cell, and the
cell means nothing without it. A grid of divs looks identical and says, to
anyone not looking at it, eight unrelated sentences.
Below 60rem the same table restacks into one block per row, each cell labelled by its column. Only the CSS changes; the markup and the header associations do not, so the reading order survives the layout.

