Docs

This page isn't translated yet

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.

Two and four columns
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.

PropTypeDefaultDescription
columnsnumber1
gapColumnGap-
alignX'left' | 'center' | 'right' | 'justify' | 'evenly''left'
alignY'top' | 'center' | 'bottom' | 'stretch''stretch'
divideXbooleanfalse
divideYbooleanfalse
disableStaggerbooleanfalse
…and all <div> attributes

ColumnLink to this section

PropTypeDefaultDescription
colSpannumber-
stickyboolean | nullfalse
disableAnimationbooleanfalse
…and all <div> attributes

RowLink to this section

PropTypeDefaultDescription
colSpan (required)number-
disableAnimationbooleanfalse
…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.

ClassStyles
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)); } }
rowflex-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.