Containers answer how wide a thing is allowed to get. A row answers how that width is divided — said once, on the row, so a child is just the component.
_base.scss
There are two kinds of modifier here, and the split is the rule rather than a compromise: counts are numbered, proportions are named.
A count carries no meaning beyond its number. thirds and
3 say exactly the same thing, so the word is one more thing
to learn in exchange for nothing. A proportion does carry meaning
beyond its numbers: 4-8 had to be decoded into "narrow thing
first, then the main column" every time it was read, and
mk-row--aside-left just says it. That is why the twelfths
became words and the counts became digits — the test is whether naming it
tells you something the number does not.
| Class | What it says |
|---|---|
mk-row |
One full-width column — the same stack a bare Bootstrap row gives you, at a row gutter apart rather than touching. Division is named; nothing here divides by itself. |
mk-row--2up |
At most 2 per row. |
mk-row--3up |
At most 3 per row. |
mk-row--4up |
At most 4 per row. |
mk-row--5up |
At most 5 per row. |
mk-row--6up |
At most 6 per row. |
mk-row--aside-left |
70/30 — the rail on the left. |
mk-row--aside-right |
The same, rail on the right. |
mk-row--weighted-left |
55/45 — the mass on the left. Just off even: enough to say which side carries, not enough to make a sidebar. |
mk-row--weighted-right |
The same, mass on the right. |
mk-row--reverse |
Mirrors a pair — same weights, other way round. Add it to alternate rows for an editorial zigzag: the markup order never moves, so reading order, tab order and the collapsed phone view all stay uniform. Pairs only, and nothing below xl, where a pair is not a pair. |
The counted range used to stop at quarters, and the reason
given was that past four a row is a grid rather than a row. That was never
a design rule. English ran out — sixths is awkward,
fifths worse — and a limit of the language got written up as
a principle, which is the worst kind, because it looks like it is
protecting something. A six-across roster of artists is an ordinary row,
and the vocabulary had no way to say it. n-up is the printing
term for how many impressions land on a sheet, so it is borrowed rather
than invented, and it reads as "six across" — where 6x would
read as a multiplier, and 2x in CSS almost always means 2×
scale.
mk-row
mk-row--2up
mk-row--3up
mk-row--4up
mk-row--5up
mk-row--6up
mk-row--aside-left
mk-row--aside-right
mk-row--weighted-left
mk-row--weighted-right
mk-row--weighted-right mk-row--reverse
This column is 715px wide, so a 3up row above correctly
shows two columns — accurate, and misleading about what
the class does. The full page preview shows each division at three widths
at once, and the harness adds tablet and mobile, so the count can be
watched following the box and the viewport independently.
Open the stage ·
open in the preview harness
row > *
This is the reason the division lives on the parent. A child carries no
col-* wrapper, so a band's markup is its content and nothing
else — which matters most where the child is an authored block rather
than something a template wrote.
<div class="mk-row mk-row--3up">
<div>the component</div>
<div>the component</div>
<div>the component</div>
</div>
Note what is not beside it. These do not ride on Bootstrap's
.row — they replace it. A grid spaced with gap
puts no padding on its children, so .row's negative margins
would have nothing to offset and the band would hang 12px outside its
container on each side. The mk- prefix is honest for the same
reason the engine cannot do this: row-cols-* counts equal
columns, cannot express an aside, and keys off the viewport regardless.
the exceptions
This vocabulary replaced Bootstrap’s .row across the theme
— 55 sites down to 9 — and the 9 that stayed are not leftovers. Each
one needs something a counted row deliberately cannot say, and the list is
short enough to be worth knowing by heart.
| Where | Rows | Why it stays |
|---|---|---|
starters/one-page.html |
3 | Deliberately plain CDN Bootstrap. The file is the copy block a reader takes away, so it may not depend on anything this theme adds. |
templates/documentation |
2 | col and col-auto — intrinsic flex sizing, which the counted vocabulary has no word for — and the docs shell, whose sidenav is a responsive collapse rather than a division. |
chrome/navbar |
2 | The mega-menu. A panel that changes shape with the viewport rather than with its own width, which is the one case a breakpoint answers honestly. |
wraps/split |
1 | Its ratio is the public field. A caller passes 7-5, so the twelfths are the API rather than an implementation detail. |
utils/docs-render |
1 | A preview harness knob: it stages a component at any of 3, 4, 6, 8, 10 or 12 twelfths. Six values on a continuum, which no set of named rungs is meant to cover — and, like split, the number is the API. |
Two more used to be on this list. cta-band and
parallax hold caller-supplied content and want
to hold it to a measure — and a container rung was the wrong tool, because
it carries the section rhythm too, so a caller’s two paragraphs would have
sat half a section-space apart. They now use
mk-measure,
which is that width and nothing else. What is left above needs the
twelfths themselves, not a measure: in both cases the number is a
field a caller passes.
--mk-row-col-min
This is where a design system has to have a view, because the engine has
none: bare row-cols-4 means four abreast at 375px. But the
view is a width, not a table of breakpoints — a column is
at least --mk-row-col-min wide, and the count follows from that.
The count an author asks for is therefore a ceiling, reached only when there is room for it. Measured against the real rules with a 16rem floor and a 3up row:
| Row width | Columns | Each |
|---|---|---|
340px |
1 | 340px |
560px |
2 | 268px |
760px |
2 | 368px |
1100px |
3 | 351px |
2000px |
3 | 651px |
Two things to read off that. 2000px stays at three rather than becoming
six — a ceiling, not a multiplier. And 340px is one column, which is the
whole reason for the rewrite: the rules this replaced asked the VIEWPORT,
so a 3up row in a 340px box on a 1280px viewport went three across at
115px each. Any row inside an aside, a mk-container--prose or a
narrow CMS zone hit that.
Because % inside the track function resolves against the row,
the rule is correct wherever the row lands, with no breakpoints and no
container query. The uneven pairs work the same way with flex: the ratio
is the basis, each side's floor is a min-inline-size, and
wrapping does the collapsing.
custom properties
Four tokens, and each is a single number rather than a pair — the complement derives itself, so the two halves of a proportion can never disagree.
| Token | Default | What it sets |
|---|---|---|
--mk-row-gap | 1.5rem |
The gutter between columns. |
--mk-row-col-min | varies by count | The readable floor: how narrow a column may get. Raise it and a counted row drops to fewer, wider columns sooner — height only follows, when one fewer column means one more line to wrap onto. Its default falls as the count rises — see below. |
--mk-row-aside | 30 |
The rail's share, unitless. The main column takes the rest. |
--mk-row-weighted | 55 |
The lean, unitless. 60–70 reads as a lean; past that an aside is the honest word. |
The floor's default falls as the count rises, because the count is itself a claim about the items. Asking for six across says they are small — nobody puts six paragraphs abreast — so a single global floor would be pretending the count tells you nothing about the content:
| Count | Default floor | Row width it needs |
|---|---|---|
mk-row--2up |
16rem (256px) |
536px |
mk-row--3up |
15rem (240px) |
768px |
mk-row--4up |
13rem (208px) |
904px |
mk-row--5up |
10rem (160px) |
896px |
mk-row--6up |
8rem (128px) |
888px |
Each number sits just under a width this design has already shipped, so
none of them is a taste call: 4up under the 216px that
features gives at four across, 5up under stats'
156px, 6up under the roster's 136px — which is exactly what
col-lg-2 used to produce. Calibrated that way, every count
reaches its nominal value in a real 936px content column.
Where the defaults are known to be wrong. They are
calibrated against a column of prose or a card, and a good deal of real
content is neither. Three places in this theme lower the floor, and each
missed its count by a hair rather than by a mile: a contact form at 535px
against the 536 that 2up needs — one pixel — a docs grid at
757 against 768 for 3up, and a row of date and time controls
at 654 against the same 768. All three set --mk-row-col-min to
14rem or lower and get the count they asked for. If you write
a row whose count silently comes out one lower than you wrote, this is
almost certainly why, and the floor is the thing to change — not the count.
The alternative is worth naming, because it was shipped for an hour and was
wrong. With one 16rem prose floor for every count, 5up needs
1376px of row width before a fifth column can exist and 6up
needs 1656px — so inside an ordinary container both silently rendered
four. The classes were in the stylesheet and could not say
anything. A floor picked for being a round number is how 5up
comes to mean four.
They are custom properties, so they cascade — set one wherever it should apply, and everything inside inherits it:
:root { --mk-row-weighted: 65; } every weighted row on the site
.editorial { --mk-row-weighted: 68; } one section
<div class="mk-row mk-row--weighted-left" style="--mk-row-weighted:70"> one row
Stacked rows behave like a wrapped one. A row that follows
another takes the row's own gutter above itself, so six cells written as one
3up row that wraps and six written as two 3up rows
show the same distance between the lines — measured 24px either way. An author
choosing one row or two changes nothing a reader can see. It needs no marker
class the way containers do: a row already wears mk-row, so there
is nothing to forget.
One trap worth knowing, because it is the cost of these being custom properties:
--mk-row-col-min inherits. Setting it on a row to tune
that row also sets it for every row NESTED inside it, overriding the floor their
own class chose. That is exactly right when a page or a band is establishing a
rhythm, and a trap when one row wraps another — a 2up wrapper with a
raised floor quietly forced the 3up rows inside it down to one
column. Set it on the row you mean, or reset it on the inner one.
This is also why weighted is an adjective and
aside is a noun. An aside is a thing — a rail, at a
proportion narrow enough that the word stays true. A weighted row is a
state: it leans, by whatever you set. The names carry which one is
yours to move.
wraps/split
wraps/split and wraps/columns keep their
ratio field for now — they have exactly two named slots
whose wrappers they write themselves, so there are no author-supplied
children to keep pure, and nothing is gained by moving them.
That escape hatch is why the named half of the vocabulary can stay
small. It does not have to be complete — Bootstrap's col-lg-*
is still underneath for a proportion nothing here names. A language with a
way out can afford to name only the shapes that recur, instead of inventing
a word for every twelfth in case someone needs it.
Which is the second dividend of numbering the counts, and it is worth separating from the first. The counted range is complete, 2 through 6, and it costs nothing to be complete about, because the class name is generated from the number — one loop, no list of words to keep in step with it. The proportions are curated precisely because each one costs a word, and a word has to be argued for. Same vocabulary, two economies, and the digit-versus-name split is what tells you which one you are in.
The two uneven pairs are widths with names instead of
numbers, and they name different things on purpose.
aside names the support column — at 30% it is narrow
enough that "subordinate" is always true of it. weighted
names the row's lean, because at 55/45 the
wide column is as often a form or a table as an image, and the narrow one
is no longer subordinate. Naming either column there would be a claim that
is sometimes false; naming the lean is always true.
They are two words rather than one word with four directions because the
widths are opposites. The two suffixes therefore mean opposite
things — aside-left puts the support on the
left, weighted-left puts the mass on the
left — and that is workable because each reads correctly on its own at the
call site, which 4-8 never did.
There is one proportion per noun, and no size grades. An
aside-wide would turn noun-position into
noun-size-position, and -wide would compete with
-left for the same slot — reading aside-wide you
could not tell whether wide was a size or a side without
already knowing the vocabulary, which is the decode step this naming exists
to remove.
The proportions are round on purpose — twelfths were the engine's
constraint, not the design's, and 58.33% is its fingerprint. They are also
not interchangeable, and measuring at lg shows why. There the
container is 960px, so an aside's rail is 264px — about 27 characters a
line — while a weight row leaves its copy 360px, about 37. Both are the
same shape described loosely ("one wide column, one narrow"), and only one
of them can hold a paragraph.
Which points at a rule about content rather than about finding a
wider aside: no aside can hold comfortable prose at lg,
because reaching ~40 characters in the narrow column takes 40%, and 40% is
the weight ratio. A rail is for a list, a card, metadata — things that wrap
tightly without looking broken. A rail that must carry paragraphs wants the
pair held back to xl (~32ch) or xxl (~38ch),
which is a page's decision, not the vocabulary's. gallery stays out of
all of this: it is CSS masonry, a different mechanism that happens to take
a number.