Every framework I've used in fifteen years shipped a twelve-column grid, and on most of the
pages I built with them I used about four of those columns. You learn the incantation —
col-md-8 col-lg-6 — and after a while you stop noticing that you are describing a
layout in units that have nothing to do with the content.
So when it came to it, we left the grid system out. There are column classes, and I'll get to when they're the right call, but they are not how you are expected to lay out a page here.
Columns answer a question nobody asked
"How many twelfths of the container should this take at a medium viewport" is a strange thing to
know about a card. The useful question is what should happen when there isn't room — and a column
span can't express that. It can only say "become full width at a breakpoint", which is why every
twelve-column layout eventually grows a pile of col-sm-12 col-md-6 col-lg-4 and
nobody can picture the result without loading it.
What we ship instead is a handful of layouts that each answer the runs-out-of-room question differently. A cluster wraps item by item. A switcher goes from a row to a column all at once, so you never get two items on one line and a lonely third underneath. A sidebar stacks only when the main part would get too narrow to read. None of those need a breakpoint and none of them need you to count anything.
The grid that drops a column
Card grids are the one place a count genuinely matters, and even there we treat it as a ceiling
rather than a promise. grid-template-columns with a minmax() floor lets
the grid work out that three columns don't fit before it makes three columns that don't work:
grid-template-columns: repeat(auto-fit, minmax(max(
min(var(--pf-col-min, 17rem), 100%),
(100% - (var(--pf-cols, 1) - 1) * var(--pf-gap)) / var(--pf-cols, 1)
), 1fr));
It looks worse than it is. The inner division is "what each column would be at the requested
count"; the max() refuses to go below the readable minimum; auto-fit
then fits as many as it can. Ask for three, give it 940px and a generous gap, and you get two —
which is what you wanted, because three would have been 226px each and body text at 226px breaks
every three words. I know that number precisely because we shipped it.
When the columns are right
There is one case, and it's a real one: you are rebuilding an existing layout that was designed
on a twelve-column grid, with a design file that says so, and the fastest honest path is to
reproduce it. .pf-row and .pf-col-* are there for that, they behave the
way you expect, and they are not deprecated.
Watch the gutters, though. Twelve tracks means eleven gutters, and if you hand that row a generous page gap you can get eleven gutters wider than a phone — at which point every track computes to zero and your content sails off the side. Ask me how I know. The row keeps its own narrower gutter for exactly this reason.
The honest trade
You give up a vocabulary that transfers between projects. Someone who knows Bootstrap's grid can read a Bootstrap layout in any codebase, and that's worth something real. What you get back is markup where the class says what the layout does rather than how many twelfths it occupies, and a page that responds to its container instead of to the window it happens to be in.
What this post does not cover
- Subgrid, which solves a different problem and is not used here.
- Masonry layout, which is still moving in the specification.
Checked against twelve-column grid, readable minimum. Code blocks are reproduced from the PolyFishCSS source files, not reconstructed from description. Figures are measured from the build they describe.
What this doesn’t cover
If you are reproducing a design that was drawn on a twelve-column grid, none of this is an argument — you need the columns, and until 0.20 we shipped them. What replaced them cannot express an arbitrary span, and the escape hatch is a few lines of grid in your own stylesheet rather than a class.
Sources: MDN on minmax() and MDN on CSS grid layout. The grid expression is reproduced verbatim from PolyFishCSS-Core.css — not reconstructed from description.