Flex grid
Wrap items into equal columns with flex-grid, and draw divider lines between them.
On this page
flex-grid is a wrapping flex container whose children size themselves as equal columns. Use it instead of CSS grid when you want divider lines between the items: each item draws its own divider, so the lines follow the items as they wrap.
Quick referenceLink to this section
| Class | Styles |
|---|---|
flex-grid | & { --tw-flex-grid-gap-x: 0px; --tw-flex-grid-gap-y: 0px; --tw-flex-grid-cols: 1; display: flex; flex-direction: row; flex-wrap: wrap; overflow: hidden; column-gap: calc(var(--tw-flex-grid-gap-x) - 1px); row-gap: calc(var(--tw-flex-grid-gap-y) - 1px); }.flex-grid { > * { width: round(down, calc((100% / var(--tw-flex-grid-cols)) - ((var(--tw-flex-grid-cols) - 1) * var(--tw-flex-grid-gap-x)) / var(--tw-flex-grid-cols)), 1px); } } |
flex-grid-col-span-1 | width: calc((100% / (var(--tw-flex-grid-cols) / 1)) - ((var(--tw-flex-grid-cols) / 1 - 1) * var(--tw-flex-grid-gap-x)) / (var(--tw-flex-grid-cols) / 1)); |
flex-grid-col-span-2 | width: calc((100% / (var(--tw-flex-grid-cols) / 2)) - ((var(--tw-flex-grid-cols) / 2 - 1) * var(--tw-flex-grid-gap-x)) / (var(--tw-flex-grid-cols) / 2)); |
flex-grid-col-span-3 | width: calc((100% / (var(--tw-flex-grid-cols) / 3)) - ((var(--tw-flex-grid-cols) / 3 - 1) * var(--tw-flex-grid-gap-x)) / (var(--tw-flex-grid-cols) / 3)); |
flex-grid-col-span-4 | width: calc((100% / (var(--tw-flex-grid-cols) / 4)) - ((var(--tw-flex-grid-cols) / 4 - 1) * var(--tw-flex-grid-gap-x)) / (var(--tw-flex-grid-cols) / 4)); |
flex-grid-col-span-5 | width: calc((100% / (var(--tw-flex-grid-cols) / 5)) - ((var(--tw-flex-grid-cols) / 5 - 1) * var(--tw-flex-grid-gap-x)) / (var(--tw-flex-grid-cols) / 5)); |
flex-grid-col-span-6 | width: calc((100% / (var(--tw-flex-grid-cols) / 6)) - ((var(--tw-flex-grid-cols) / 6 - 1) * var(--tw-flex-grid-gap-x)) / (var(--tw-flex-grid-cols) / 6)); |
flex-grid-cols-1 | --tw-flex-grid-cols: 1; |
flex-grid-cols-2 | --tw-flex-grid-cols: 2; |
flex-grid-cols-3 | --tw-flex-grid-cols: 3; |
flex-grid-cols-4 | --tw-flex-grid-cols: 4; |
flex-grid-cols-5 | --tw-flex-grid-cols: 5; |
flex-grid-cols-6 | --tw-flex-grid-cols: 6; |
flex-grid-divide-black | & > ::after { background-color: #000; } |
flex-grid-divide-transparent | & > ::after { background-color: transparent; } |
flex-grid-divide-white | & > ::after { background-color: #fff; } |
flex-grid-divide-x | & > * { position: relative; }.flex-grid-divide-x > * { &::after { content: var(--tw-content); position: absolute; top: 0; height: 100%; right: calc(-1 * (var(--tw-flex-grid-gap-x) / 2)); transform: translateX(50%); width: 1px; } }.flex-grid-divide-x > * { &:last-child::after { display: none; } } |
flex-grid-divide-x-0 | & > * { position: relative; }.flex-grid-divide-x-0 > * { &::after { content: var(--tw-content); position: absolute; top: 0; height: 100%; right: calc(-1 * (var(--tw-flex-grid-gap-x) / 2)); transform: translateX(50%); width: 0px; } }.flex-grid-divide-x-0 > * { &:last-child::after { display: none; } } |
flex-grid-divide-x-2 | & > * { position: relative; }.flex-grid-divide-x-2 > * { &::after { content: var(--tw-content); position: absolute; top: 0; height: 100%; right: calc(-1 * (var(--tw-flex-grid-gap-x) / 2)); transform: translateX(50%); width: 2px; } }.flex-grid-divide-x-2 > * { &:last-child::after { display: none; } } |
flex-grid-divide-x-4 | & > * { position: relative; }.flex-grid-divide-x-4 > * { &::after { content: var(--tw-content); position: absolute; top: 0; height: 100%; right: calc(-1 * (var(--tw-flex-grid-gap-x) / 2)); transform: translateX(50%); width: 4px; } }.flex-grid-divide-x-4 > * { &:last-child::after { display: none; } } |
flex-grid-divide-x-8 | & > * { position: relative; }.flex-grid-divide-x-8 > * { &::after { content: var(--tw-content); position: absolute; top: 0; height: 100%; right: calc(-1 * (var(--tw-flex-grid-gap-x) / 2)); transform: translateX(50%); width: 8px; } }.flex-grid-divide-x-8 > * { &:last-child::after { display: none; } } |
flex-grid-divide-y | & > * { position: relative; }.flex-grid-divide-y > * { &::after { content: var(--tw-content); position: absolute; left: 0; width: 100%; bottom: calc(-1 * (var(--tw-flex-grid-gap-y) / 2)); transform: translateY(50%); height: 1px; } }.flex-grid-divide-y > * { &:last-child::after { display: none; } } |
flex-grid-divide-y-0 | & > * { position: relative; }.flex-grid-divide-y-0 > * { &::after { content: var(--tw-content); position: absolute; left: 0; width: 100%; bottom: calc(-1 * (var(--tw-flex-grid-gap-y) / 2)); transform: translateY(50%); height: 0px; } }.flex-grid-divide-y-0 > * { &:last-child::after { display: none; } } |
flex-grid-divide-y-2 | & > * { position: relative; }.flex-grid-divide-y-2 > * { &::after { content: var(--tw-content); position: absolute; left: 0; width: 100%; bottom: calc(-1 * (var(--tw-flex-grid-gap-y) / 2)); transform: translateY(50%); height: 2px; } }.flex-grid-divide-y-2 > * { &:last-child::after { display: none; } } |
flex-grid-divide-y-4 | & > * { position: relative; }.flex-grid-divide-y-4 > * { &::after { content: var(--tw-content); position: absolute; left: 0; width: 100%; bottom: calc(-1 * (var(--tw-flex-grid-gap-y) / 2)); transform: translateY(50%); height: 4px; } }.flex-grid-divide-y-4 > * { &:last-child::after { display: none; } } |
flex-grid-divide-y-8 | & > * { position: relative; }.flex-grid-divide-y-8 > * { &::after { content: var(--tw-content); position: absolute; left: 0; width: 100%; bottom: calc(-1 * (var(--tw-flex-grid-gap-y) / 2)); transform: translateY(50%); height: 8px; } }.flex-grid-divide-y-8 > * { &:last-child::after { display: none; } } |
Basic usageLink to this section
Add flex-grid and a column count such as flex-grid-cols-3. Every direct child becomes one column wide; a partly filled last row keeps the same column width.
export default function Demo() {
return (
<div className="flex-grid flex-grid-cols-3 w-full max-w-[560px]">
{['01', '02', '03', '04', '05'].map((label, i) => (
<div
key={label}
className={`grid h-16 place-items-center font-mono text-sm text-white ${i % 2 ? 'bg-theme-700' : 'bg-theme-500'}`}
>
{label}
</div>
))}
</div>
)
}GapsLink to this section
Set the gap with an arbitrary value or a CSS variable: flex-grid-gap-[24px] or flex-grid-gap-(--gap-md). It sets the column and the row gap, and the column widths account for it. The hatched area is the gap.
const hatch =
'bg-[repeating-linear-gradient(315deg,var(--color-foundations-line-muted)_0_1px,transparent_0_50%)] bg-size-[10px_10px]'
export default function Demo() {
return (
<div className={`flex-grid flex-grid-cols-3 flex-grid-gap-(--gap-md) w-full max-w-[560px] ${hatch}`}>
{['01', '02', '03', '04', '05', '06'].map((label) => (
<div
key={label}
className="bg-theme-500 grid h-16 place-items-center rounded-lg font-mono text-sm text-white"
>
{label}
</div>
))}
</div>
)
}Spanning columnsLink to this section
flex-grid-col-span-{n} makes an item n columns wide, gaps included.
export default function Demo() {
return (
<div className="flex-grid flex-grid-cols-3 flex-grid-gap-[16px] w-full max-w-[560px]">
<div className="bg-theme-700 flex-grid-col-span-2 grid h-16 place-items-center rounded-lg font-mono text-sm text-white">
flex-grid-col-span-2
</div>
{['02', '03', '04'].map((label) => (
<div
key={label}
className="bg-theme-500 grid h-16 place-items-center rounded-lg font-mono text-sm text-white"
>
{label}
</div>
))}
</div>
)
}DividersLink to this section
flex-grid-divide-x draws a vertical line in the middle of the gap after each item, and flex-grid-divide-y a horizontal line under each item. Set the width with a suffix (-0, -2, -4, -8; the bare class is 1px) and the color with flex-grid-divide-{color}. The container clips its overflow, so the line after the last item of a row falls outside it and is not drawn, and the last item draws none at all.
The dividers are ::after elements whose content comes from Tailwind's --tw-content variable. Tailwind only defines that variable when a project uses a content-* utility somewhere, so add *:after:content-[''] to the container to be sure the lines render:
export default function Demo() {
return (
<div className="flex-grid flex-grid-cols-3 flex-grid-gap-[48px] flex-grid-divide-x flex-grid-divide-(--color-foundations-surface-border) w-full max-w-[600px] *:after:content-['']">
{[
['12k', 'Pages served'],
['98%', 'Lighthouse score'],
['4', 'Languages'],
].map(([value, label]) => (
<div key={label} className="flex flex-col gap-1">
<span className="text-h3 color-heading">{value}</span>
<span className="text-small color-body">{label}</span>
</div>
))}
</div>
)
}Tokens
Components
Payload blocks
export default function Demo() {
return (
<div className="flex-grid flex-grid-cols-1 flex-grid-gap-[24px] flex-grid-divide-y-2 flex-grid-divide-black/20 w-full max-w-[420px] *:after:content-['']">
{['Tokens', 'Components', 'Payload blocks'].map((label) => (
<p key={label} className="text-body color-body">
{label}
</p>
))}
</div>
)
}Each item has one ::after, so flex-grid-divide-x and flex-grid-divide-y cannot be combined on one container; the class that comes later in the CSS wins.
The colors that work are black, white and transparent, an arbitrary value (flex-grid-divide-[#d4d4d4]) or a CSS variable (flex-grid-divide-(--color-foundations-surface-border)), each with an optional opacity modifier (flex-grid-divide-black/20).
Responsive and state variantsLink to this section
Every flex-grid-* utility takes breakpoint prefixes. A common pattern is one column on phones and more from a breakpoint up:
export default function Demo() {
return (
<div className="flex-grid flex-grid-cols-1 flex-grid-gap-[16px] w-full md:flex-grid-cols-2 lg:flex-grid-cols-4">
{['01', '02', '03', '04'].map((label) => (
<div
key={label}
className="bg-theme-500 grid h-16 place-items-center rounded-lg font-mono text-sm text-white"
>
{label}
</div>
))}
</div>
)
}State variants work too, for example hover:flex-grid-divide-black to darken the lines while the pointer is over the grid.
CustomizingLink to this section
The table lists counts and spans from 1 to 6, but any whole number works (flex-grid-cols-12, flex-grid-col-span-8). The divider widths come from Tailwind's divide-width scale. The gap takes any length, so pass a token variable such as --gap-md or --grid-default-gap to stay on the token scale (see Gap).