Columns
Lay content out in one to four responsive columns with token gaps, alignment, spans, dividers and sticky columns.
On this page
Columns places its Column children side by side from the md breakpoint up and stacks them below it. Each Column is a rich-text box, so headings and paragraphs inside it keep the token rhythm. Use it for feature lists, card grids and text-and-media splits inside a Section.
import { Column, Columns, Heading, Paragraph } from '@systhemaui/next'
export default function Demo() {
return (
<div className="container py-12">
<Columns columns={3} gap="lg">
<Column>
<Heading.h4>Tokens first</Heading.h4>
<Paragraph>Every color, size and gap comes from the design tokens.</Paragraph>
</Column>
<Column>
<Heading.h4>Composable</Heading.h4>
<Paragraph>Nest columns in sections and cards; the spacing follows.</Paragraph>
</Column>
<Column>
<Heading.h4>Responsive</Heading.h4>
<Paragraph>Three columns on desktop, one on a phone, with no extra markup.</Paragraph>
</Column>
</Columns>
</div>
)
}ImportLink to this section
import { Column, Columns, Row } from '@systhemaui/next'In a React app without Next.js, import them from @systhemaui/react.
PartsLink to this section
Columns: the flex container. It sets the column count, the gaps, the alignment and the dividers.Column: one cell. It can span several columns and stick to the top of the viewport.Row: a full-width cell that breaks the flow, for a heading or a note across all columns.
ExamplesLink to this section
Column countLink to this section
columns takes 1 to 4 (the default is 1). The count applies from md up; below md every column is full width. Switch the preview to the phone viewport to see them stack.
import { Card, Column, Columns, Heading, Paragraph } from '@systhemaui/next'
export default function Demo() {
return (
<div className="container flex flex-col gap-10 py-12">
<Columns columns={2} gap="md">
<Column>
<Card variant="default">
<Heading.h4>Starter</Heading.h4>
<Paragraph>For a single site.</Paragraph>
</Card>
</Column>
<Column>
<Card variant="highlighted">
<Heading.h4>Studio</Heading.h4>
<Paragraph>For teams that run many sites.</Paragraph>
</Card>
</Column>
</Columns>
<Columns columns={4} gap="md">
{['Plan', 'Design', 'Build', 'Launch'].map((step, index) => (
<Column key={step}>
<Paragraph type="label">Step {index + 1}</Paragraph>
<Heading.h4>{step}</Heading.h4>
</Column>
))}
</Columns>
</div>
)
}Spanning columnsLink to this section
colSpan on a Column widens it to that many columns. A Row is always full width; set its colSpan to the column count so the stagger of the following columns stays in step (it renders hidden spacer elements for that).
import { Column, Columns, Heading, Paragraph, Row } from '@systhemaui/next'
export default function Demo() {
return (
<div className="container py-12">
<Columns columns={3} gap={{ x: 'lg', y: 'md' }}>
<Row colSpan={3}>
<Heading.h3>Quarterly report</Heading.h3>
</Row>
<Column colSpan={2}>
<Paragraph type="lead">
A wide column for the main text, spanning two of the three columns.
</Paragraph>
</Column>
<Column>
<Paragraph type="small">A narrow column for a side note or a figure caption.</Paragraph>
</Column>
</Columns>
</div>
)
}GapsLink to this section
gap takes a size from the token gap scale (none, xs, sm, md, lg, xl, 2xl, 3xl), or an object with separate x and y sizes. Without gap the columns touch.
import { Card, Column, Columns, Paragraph } from '@systhemaui/next'
const items = ['One', 'Two', 'Three', 'Four', 'Five', 'Six']
export default function Demo() {
return (
<div className="container py-12">
<Columns columns={3} gap={{ x: 'xl', y: 'xs' }}>
{items.map((item) => (
<Column key={item}>
<Card variant="default">
<Paragraph>{item}</Paragraph>
</Card>
</Column>
))}
</Columns>
</div>
)
}AlignmentLink to this section
alignY aligns columns of different heights (top, center, bottom, or the default stretch). alignX places a row that does not fill the count (left, center, right, justify, evenly). Both apply from md up.
import { Card, Column, Columns, Heading, Paragraph } from '@systhemaui/next'
export default function Demo() {
return (
<div className="container py-12">
<Columns columns={4} gap="md" alignX="center" alignY="center">
<Column>
<Card variant="default">
<Heading.h4>Short</Heading.h4>
</Card>
</Column>
<Column>
<Card variant="highlighted">
<Heading.h4>Taller card</Heading.h4>
<Paragraph>
Two cards in a four-column grid, centred horizontally and vertically.
</Paragraph>
</Card>
</Column>
</Columns>
</div>
)
}DividersLink to this section
divideX draws a hairline between columns (between stacked items on a phone), divideY one between rows. The line uses --separator-line-width and --color-separator-line.
import { Column, Columns, Heading, Paragraph } from '@systhemaui/next'
const stats = [
{ value: '120', label: 'Sites launched' },
{ value: '4.9', label: 'Average rating' },
{ value: '24 h', label: 'Support response' },
]
export default function Demo() {
return (
<div className="container py-12">
<Columns columns={3} gap="xl" divideX alignX="center">
{stats.map((stat) => (
<Column key={stat.label} className="text-center">
<Heading.h2>{stat.value}</Heading.h2>
<Paragraph type="label">{stat.label}</Paragraph>
</Column>
))}
</Columns>
</div>
)
}Sticky columnsLink to this section
sticky keeps a column at the top of the viewport while its neighbour scrolls past, offset by --header-height plus --section-padding-y. It applies from md up and needs a taller sibling to scroll against, which a preview frame cannot show:
import { Column, Columns, Heading, Paragraph } from '@systhemaui/next'
export function Chapters() {
return (
<Columns columns={3} gap="lg">
<Column sticky>
<Heading.h3>Contents</Heading.h3>
</Column>
<Column colSpan={2}>
<Paragraph>Long text that scrolls past the sticky column.</Paragraph>
</Column>
</Columns>
)
}StaggerLink to this section
With more than one column, each column reveals a little after the previous one (stagger-delay-150 plus a per-column step). Set disableStagger to reveal all columns at once. A Column or Row with its reveal animation on cancels the reveals of the elements inside it; set disableAnimation on the column to let its children animate on their own. See Animation.
PropsLink to this section
ColumnsLink to this section
columns takes 1 to 4. Any other value emits no count class, and the columns stack at every width.
| Prop | Type | Default | Description |
|---|---|---|---|
columns | number | 1 | |
gap | ColumnGap | - | |
alignX | 'left' | 'center' | 'right' | 'justify' | 'evenly' | 'left' | |
alignY | 'top' | 'center' | 'bottom' | 'stretch' | 'stretch' | |
divideX | boolean | false | |
divideY | boolean | false | |
disableStagger | boolean | false | |
…and all <div> attributes |
ColumnLink to this section
| Prop | Type | Default | Description |
|---|---|---|---|
colSpan | number | - | |
sticky | boolean | null | false | |
disableAnimation | boolean | false | |
…and all <div> attributes |
RowLink to this section
| Prop | Type | Default | Description |
|---|---|---|---|
colSpan (required) | number | - | |
disableAnimation | boolean | false | |
…and all <div> attributes |
HTML and CSSLink to this section
The same layout in plain HTML:
<div
class="columns columns-cols-3 columns-gap-lg columns-align-x-left columns-align-y-stretch stagger-delay-150 md:*:stagger-3"
>
<div class="column richtext column-span-2">…</div>
<div class="column richtext column-sticky">…</div>
</div>The number of columns-cols-* and column-span-* classes follows blocks.columns.count (4 by default); see Component CSS blocks.
| Class | Styles |
|---|---|
columns | &.footer-main-navigation { --columns-cols: 1; }@media (width >= 640px) { &.footer-main-navigation { --columns-cols: 2; } }@media (width >= 1192px) { &.footer-main-navigation { --columns-cols: 4; } }& { --columns-gap-x: 0px; --columns-gap-y: 0px; --columns-cols: 1; position: relative; display: flex; flex-direction: row; flex-wrap: wrap; overflow-x: visible; overflow-y: visible; column-gap: calc(var(--columns-gap-x) - 1px); row-gap: calc(var(--columns-gap-y) - 1px); }+ 3 more rules |
columns-cols-1 | @media (width >= 640px) { & { --columns-cols: 1; } } |
columns-cols-2 | @media (width >= 640px) { & { --columns-cols: 2; } } |
columns-cols-3 | @media (width >= 640px) { & { --columns-cols: 3; } } |
columns-cols-4 | @media (width >= 640px) { & { --columns-cols: 4; } } |
columns-gap-none | --columns-gap-x: var(--gap-none);--columns-gap-y: var(--gap-none); |
columns-gap-x-none | --columns-gap-x: var(--gap-none); |
columns-gap-y-none | --columns-gap-y: var(--gap-none); |
columns-gap-xs | --columns-gap-x: var(--gap-xs);--columns-gap-y: var(--gap-xs); |
columns-gap-x-xs | --columns-gap-x: var(--gap-xs); |
columns-gap-y-xs | --columns-gap-y: var(--gap-xs); |
columns-gap-sm | --columns-gap-x: var(--gap-sm);--columns-gap-y: var(--gap-sm); |
columns-gap-x-sm | --columns-gap-x: var(--gap-sm); |
columns-gap-y-sm | --columns-gap-y: var(--gap-sm); |
columns-gap-md | --columns-gap-x: var(--gap-md);--columns-gap-y: var(--gap-md); |
columns-gap-x-md | --columns-gap-x: var(--gap-md); |
columns-gap-y-md | --columns-gap-y: var(--gap-md); |
columns-gap-lg | --columns-gap-x: var(--gap-lg);--columns-gap-y: var(--gap-lg); |
columns-gap-x-lg | --columns-gap-x: var(--gap-lg); |
columns-gap-y-lg | --columns-gap-y: var(--gap-lg); |
columns-gap-xl | --columns-gap-x: var(--gap-xl);--columns-gap-y: var(--gap-xl); |
columns-gap-x-xl | --columns-gap-x: var(--gap-xl); |
columns-gap-y-xl | --columns-gap-y: var(--gap-xl); |
columns-gap-2xl | --columns-gap-x: var(--gap-2xl);--columns-gap-y: var(--gap-2xl); |
columns-gap-x-2xl | --columns-gap-x: var(--gap-2xl); |
columns-gap-y-2xl | --columns-gap-y: var(--gap-2xl); |
columns-gap-3xl | --columns-gap-x: var(--gap-3xl);--columns-gap-y: var(--gap-3xl); |
columns-gap-x-3xl | --columns-gap-x: var(--gap-3xl); |
columns-gap-y-3xl | --columns-gap-y: var(--gap-3xl); |
columns-divide-x | & { --columns-divider-thickness: var(--separator-line-width); --columns-divider-color: var(--color-separator-line); overflow-x: hidden; }& .column, .columns-divide-x .row { position: relative; }.columns-divide-x .column, .columns-divide-x .row { &::after { content: ""; position: absolute; background-color: var(--columns-divider-color); } }.columns-divide-x .column, .columns-divide-x .row { &:last-child::after { display: none; } } |
columns-divide-y | & { --columns-divider-thickness: var(--separator-line-width); --columns-divider-color: var(--color-separator-line); overflow-y: hidden; }& .column, .columns-divide-y .row { position: relative; }+ 3 more rules |
column | & { flex-grow: 0; width: round(down, calc((100% / var(--columns-cols)) - ((var(--columns-cols) - 1) * var(--columns-gap-x)) / var(--columns-cols)), 1px); }@media (width >= 640px) { &.column-sticky { position: sticky; align-self: flex-start; top: calc(var(--header-height, 64px) + var(--section-padding-y, 48px)); } } |
row | flex-grow: 0;width: 100%; |
columns-align-x-left | @media (width >= 640px) { & { justify-content: flex-start; } } |
columns-align-x-center | @media (width >= 640px) { & { justify-content: center; } } |
columns-align-x-right | @media (width >= 640px) { & { justify-content: flex-end; } } |
columns-align-x-justify | @media (width >= 640px) { & { justify-content: space-between; } } |
columns-align-x-evenly | @media (width >= 640px) { & { justify-content: space-evenly; } } |
columns-align-y-top | @media (width >= 640px) { & { align-items: flex-start; } } |
columns-align-y-center | @media (width >= 640px) { & { align-items: center; } } |
columns-align-y-bottom | @media (width >= 640px) { & { align-items: flex-end; } } |
columns-align-y-stretch | @media (width >= 640px) { & { align-items: stretch; } } |
column-span-1 | @media (width >= 640px) { & { width: calc((100% / (var(--columns-cols) / 1)) - ((var(--columns-cols) / 1 - 1) * var(--columns-gap-x)) / (var(--columns-cols) / 1)); } } |
column-span-2 | @media (width >= 640px) { & { width: calc((100% / (var(--columns-cols) / 2)) - ((var(--columns-cols) / 2 - 1) * var(--columns-gap-x)) / (var(--columns-cols) / 2)); } } |
column-span-3 | @media (width >= 640px) { & { width: calc((100% / (var(--columns-cols) / 3)) - ((var(--columns-cols) / 3 - 1) * var(--columns-gap-x)) / (var(--columns-cols) / 3)); } } |
column-span-4 | @media (width >= 640px) { & { width: calc((100% / (var(--columns-cols) / 4)) - ((var(--columns-cols) / 4 - 1) * var(--columns-gap-x)) / (var(--columns-cols) / 4)); } } |
Next.jsLink to this section
@systhemaui/next re-exports Columns, Column and Row from @systhemaui/react unchanged.
AccessibilityLink to this section
Columns reorder nothing: the reading order is the source order at every width, so put the column a screen-reader user should hear first first. Dividers are pseudo-elements and are not announced.