Mark Design

Rows

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.

The vocabulary

_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.

ClassWhat 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.

Live specimens

Full page preview ↗
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


The children are pure

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.


Where the engine’s row still belongs

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.

WhereRowsWhy 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.


How it collapses

--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 widthColumnsEach
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.


Tuning it

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.

TokenDefaultWhat it sets
--mk-row-gap1.5rem The gutter between columns.
--mk-row-col-minvaries 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-aside30 The rail's share, unitless. The main column takes the rest.
--mk-row-weighted55 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:

CountDefault floorRow 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.


What still uses twelfths

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.