Appearance
Grid (row + col)
A Bootstrap-style twelve-column grid expressed as two cooperating components: a row flex container and col children that opt into explicit widths via col-span-N. Responsive behavior is delegated to UnoCSS's variant prefixes (md:, lg:, etc.) instead of breakpoint-embedded class names.
Authoring
ts
// recipes/row.config.ts
import { defineComponent } from 'varia'
export default defineComponent('row', {
// The horizontal gutter is the `--row-gx` custom property. The row's
// negative margin pulls col paddings outside its content box; cols
// inherit `--row-gx` via the cascade and apply the matching internal
// padding. `gap-y-*` is safe because vertical gap doesn't interact with
// horizontal sibling widths.
base: 'flex flex-wrap mx-[calc(var(--row-gx,0)/-2)]',
variants: {
gx: {
0: '[--row-gx:0]',
1: '[--row-gx:0.25rem]',
2: '[--row-gx:0.5rem]',
3: '[--row-gx:1rem]',
4: '[--row-gx:1.5rem]',
5: '[--row-gx:3rem]',
},
gy: {
0: 'gap-y-0', 1: 'gap-y-1', 2: 'gap-y-2',
3: 'gap-y-4', 4: 'gap-y-6', 5: 'gap-y-12',
},
// Shorthand: sets both axes.
g: {
0: '[--row-gx:0] gap-y-0',
3: '[--row-gx:1rem] gap-y-4',
// ... 1, 2, 4, 5 follow the same shape
},
},
})ts
// recipes/col.config.ts
import { defineComponent } from 'varia'
export default defineComponent('col', {
// `flex-1` = Bootstrap's bare `.col` (equal-width flex sibling).
// `px-[calc(...)]` reads the gutter from the parent row.
base: 'flex-1 px-[calc(var(--row-gx,0)/2)]',
variants: {
span: {
auto: 'flex-none w-auto',
1: 'flex-none w-1/12',
2: 'flex-none w-2/12',
// ...
12: 'flex-none w-full',
},
offset: {
0: 'ml-0', 1: 'ml-1/12', /* ... up to 11 */
},
order: {
first: 'order-first', last: 'order-last',
0: 'order-0', 1: 'order-1', /* ... up to 5 */
},
},
})Three things to call out:
Gutters use padding + negative margin, not
gap.gapsits between siblings, so the total horizontal space consumed by N siblings isN × width + (N-1) × gap. With explicit fractional widths that sum to 100%, the gap pushes the last sibling onto the next row. Bootstrap's padding-on-col + negative-margin-on-row pattern avoids this: withbox-sizing: border-box, an explicitw-6/12includes the padding, so two siblings still total exactly 100%.The gutter flows through a CSS custom property. Setting
row-gx-3writes--row-gx: 1remon the row element; cols inherit the variable and use it for symmetric horizontal padding. One source of truth, no coordination required between the row's variant and the col's padding value.col's base isflex-1— the bare.colbehavior. When you addcol-span-N, that variant carriesflex-noneto cancel the base's flex-grow andw-N/12to set the explicit width.
The row > col > content pattern
The col element is structural: it owns the explicit width and the internal gutter padding. Don't put styling utilities (backgrounds, content padding, borders) on the col itself — they'll either stomp the gutter padding or visually consume it, making siblings appear flush. Put the styled content inside the col:
html
<div class="row row-g-3">
<div class="col col-span-6">
<div class="bg-blue-100 p-3 rounded">content here</div>
</div>
<div class="col col-span-6">
<div class="bg-blue-100 p-3 rounded">content here</div>
</div>
</div>This matches Bootstrap's convention. The col is the layout slot; the inner element is the visual cell.
Live preview
Three equal-width columns (bare .col):
A
B
C
Explicit widths (span-8 + span-4):
span 8
span 4
With offset (offset-2):
span 4, offset 2
span 4
Reordered (order-last on the first item):
first in source, last visually
B
C
Consumption
html
<div class="row row-g-3">
<div class="col col-span-12 md:col-span-6 lg:col-span-4">
<article class="bg-white p-4 rounded shadow-sm">A</article>
</div>
<div class="col col-span-12 md:col-span-6 lg:col-span-4">
<article class="bg-white p-4 rounded shadow-sm">B</article>
</div>
<div class="col col-span-12 md:col-span-6 lg:col-span-4">
<article class="bg-white p-4 rounded shadow-sm">C</article>
</div>
</div>The md: and lg: prefixes work because UnoCSS resolves them against the shortcut's underlying utilities. md:col-span-6 becomes md:w-1/2 md:flex-none at build time — same media-query mechanism Tailwind uses on its own utilities.
Generated class names
| Class | Purpose |
|---|---|
row | Flex container with flex-wrap |
row-g-{0..5} | Gutter (both axes) |
row-gx-{0..5} | Gutter (horizontal) |
row-gy-{0..5} | Gutter (vertical) |
col | Bare column (equal-width flex sibling) |
col-span-{auto, 1..12} | Explicit width |
col-offset-{0..11} | Left margin offset |
col-order-{first, last, 0..5} | Flex order |
Comparison with Bootstrap's class shape
Bootstrap's literal classes embed breakpoints (col-md-6, offset-lg-2); varia's naming convention is component-axis-value with no slot for a breakpoint segment. Two consequences:
- Class shape differs. Where Bootstrap writes
col-md-6, varia usesmd:col-span-6. The leadingmd:is UnoCSS's variant prefix, which it strips when emitting the media-queried CSS rule. Same outcome, different shape. - Offset / order classes are namespaced under
col. Bootstrap has.offset-2standing alone; varia emitscol-offset-2. The semantics are the same — both apply to a column element — but the class name reflects that.
If you want Bootstrap-literal class names (col-md-6, offset-2), you'd need one component per breakpoint and standalone offset/order components. That's roughly 250 shortcuts for full coverage; the variant-prefix approach above is roughly 30.
What's not included
- Container. Bootstrap's
.container(max-width with horizontal padding) is one base shortcut — fits adefineComponent('container', { ... })with size variants if you need it. - Responsive gutters.
md:row-g-3works (UnoCSS resolves the prefix against the shortcut), so no separate variant needed. row-cols-N(Bootstrap's "force N equal columns inside the row"). Achievable as acolsvariant onrowsettinggrid-cols-Nand switching base togrid, but that conflicts with the flex base. Easier to write explicitcol-span-*values per child.