@useavalon/flora
v0.1.0
Published
Framework-agnostic, responsive modular grid system. CSS-first, token-driven, and trivially overridable — great DX with zero runtime dependencies.
Maintainers
Readme
Flora
A framework-agnostic, responsive modular grid — CSS-first, token-driven, and trivially overridable. Zero runtime dependencies.
Flora gives you a mathematically-grounded modular grid (the 8-point system, sensible 4 / 8 / 12 responsive columns, baseline rhythm) that drops into any stack — plain HTML, React, Preact, Vue, Svelte, Solid, or a design system. Its whole job is to get out of your way: everything is a --flora-* custom property inside a low-priority @layer flora, so you customize by setting a variable and override any rule with an ordinary selector. No specificity fights, no !important, no quirks.
- CSS-first. Import one stylesheet and add a class. No build step, no runtime.
- Framework-agnostic. It's just CSS + custom properties. Use it anywhere, or nowhere.
- Effortless override. All rules live in
@layer flora; any unlayered rule you write wins automatically. Retune the grid by redefining a custom property. - Responsive by default. Mobile-first 4 / 8 / 12 columns with token-driven gutters and margins across the standard breakpoints.
- Declarative spans.
class="flora-col-6 flora-col-lg-4"orstyle="--flora-col-span: 6". - Optional
<flora-grid>element. Zero-dependency web component for plain-HTML ergonomics. - Grid maths, exposed. Column widths, span widths, modular type scales, and divisibility helpers as pure functions.
Status: early (
0.1.x). The API may change.
Install
npm install @useavalon/floraQuick start (CSS)
import "@useavalon/flora/flora.css";<div class="flora-grid">
<div class="flora-col-4 flora-col-lg-8">main</div>
<div class="flora-col-4 flora-col-lg-4">aside</div>
</div>That's it — a responsive modular grid: 4 columns on mobile, 8 on tablet, 12 on desktop, with gutters and margins that scale per breakpoint.
Two ways to place items
<!-- utility classes, responsive per breakpoint -->
<div class="flora-col-6 flora-start-2 flora-col-lg-4">…</div>
<!-- or custom properties, inline / from your own CSS -->
<div style="--flora-col-span: 6; --flora-col-start: 2">…</div>Overriding — the whole point
Flora ships inside @layer flora, the lowest-priority layer. Anything you write beats it, so you never fight the cascade:
/* 1. Retune a token — no rebuild, cascades normally. */
:root {
--flora-gutter: 12px;
--flora-columns: 16;
}
/* 2. Override a rule — an ordinary selector wins over the layer. No !important. */
.flora-grid {
gap: 4rem;
}Scope Flora to part of a page by generating it against your own selector (below), so there's no global .flora-grid at all if you don't want one.
Optional: <flora-grid> web component
import "@useavalon/flora/element"; // self-registers <flora-grid><flora-grid columns="12" gutter="24" margin="40" max-width="1200">
<div class="flora-col-8">main</div>
<aside style="--flora-col-span: 4">aside</aside>
</flora-grid>The element only reflects its container attributes (columns, gutter, margin, row-gap, baseline, max-width) onto --flora-* properties. Children are placed with the same API as everywhere else — the flora-col-* classes or the --flora-col-span / --flora-col-start properties — so there's one mental model, not a separate attribute grammar. Its structural rules are injected into @layer flora too, so they stay overridable.
Generate a bespoke stylesheet
When you want a different column count, breakpoint set, or selector baked in:
import { generateGridCss } from "@useavalon/flora";
const css = generateGridCss({
selector: "[data-grid]",
columns: { xs: 2, md: 6, xl: 16 },
gutter: { xs: 16, lg: 32 },
maxWidth: 1440,
layer: "flora", // or null to emit unlayered CSS
});The generator is the single source of truth: the shipped flora.css is produced from the default config, so CSS and JS can't drift.
Layouts & recipes
All included in flora.css, all in @layer flora, all overridable.
Grid modes — retune gutters by adding a class:
<div class="flora-grid flora-condensed">…</div> <!-- 1px gutters -->
<div class="flora-grid flora-wide">…</div> <!-- 32px gutters -->
<!-- also: flora-narrow (16px), flora-flush (0) -->Native subgrid — nested items align to the outer grid's columns:
<div class="flora-grid">
<section class="flora-subgrid flora-col-8">
<h2 class="flora-col-6">…</h2> <!-- aligns to the parent grid lines -->
<aside class="flora-col-2">…</aside>
</section>
</div>Aspect-ratio boxes (media auto-covers):
<div class="flora-ratio-16x9"><img src="…" /></div>
<!-- 1x1, 2x1, 3x2, 4x3, 16x9, 2x3, 3x4 -->Auto grid — responsive cards with no media queries:
<div class="flora-auto" style="--flora-auto-min: 16rem">
<article>…</article> <!-- tracks fit automatically -->
</div>Bento grid — dense, editorial tiles:
<div class="flora-bento">
<div class="flora-tile-big">2×2</div>
<div class="flora-tile-wide">2×1</div>
<div class="flora-tile-tall">1×2</div>
<div>1×1</div>
</div>Masonry — variable-height items packed tightly with no fixed row lines:
<div class="flora-masonry" style="--flora-masonry-columns: 3; --flora-masonry-min: 16rem">
<figure>…</figure> <!-- items keep their natural heights -->
<figure>…</figure>
</div>Native CSS masonry isn't broadly available yet (still experimental and behind flags, with the syntax being finalized), and Flora won't pull in a JS layout library — that would break the zero-dependency, CSS-first promise. So .flora-masonry is a progressive enhancement: today it uses CSS multi-column, which packs variable heights in every browser. The one caveat is that multi-column flows top-to-bottom within each column, so the visual order differs from source order — keep that in mind for reading order. The moment a browser ships native grid masonry, an @supports block automatically upgrades .flora-masonry to true grid masonry (which packs in source order and aligns to real column tracks) — no code change on your part, still no JS. Until then, .flora-bento remains a great source-order-preserving alternative.
16-column preset — a wider, powers-of-two grid (4 / 8 / 16) for dense, desktop-heavy layouts:
import { generateGridCss, POWERS_OF_TWO_COLUMNS } from "@useavalon/flora";
const css = generateGridCss({
columns: POWERS_OF_TWO_COLUMNS, // 4 / 8 / 16
maxSpan: 16,
});Vertical rhythm
The counterpart to the column grid: keep vertical spacing on a baseline so text and blocks line up like ruled paper. Flora's baseline is --flora-baseline (defaults to the base unit, 8px).
<!-- consistent, baseline-multiple spacing between stacked children -->
<article class="flora-flow" style="--flora-flow-step: 3">
<h1>Title</h1>
<p>…</p>
<figure>…</figure>
</article>.flora-flow spaces adjacent children by --flora-baseline * --flora-flow-step (default 3 → 24px). Add .flora-baseline-grid to any element to paint a debug overlay every baseline unit and eyeball alignment.
Snap sizes to the grid in JS:
import { snapToBaseline, baselineLineHeight } from "@useavalon/flora";
snapToBaseline(20, 8); // 24 (nearest 8px multiple)
baselineLineHeight(20, 8); // 32 (line-height on the rhythm)Or skip the manual math: a typeScale hands you font sizes and baseline-snapped line-heights together, so you can generate a whole scale that's already on the rhythm:
import { typeScale } from "@useavalon/flora";
const t = typeScale(16, "perfectFifth", { baseline: 8 });
for (const step of [-1, 0, 1, 2]) {
console.log(`--fs-${step}: ${t.at(step)}px; --lh-${step}: ${t.lineHeightAt(step)}px;`);
}
// every line-height is a whole number of 8px baseline unitsContainer queries — respond to the slot, not the screen
Every responsive stylesheet ships in a @media (viewport) flavour and a @container flavour, so a grid or template composes based on its own container's width — ideal for a component reused in a wide main column and a narrow sidebar.
import "@useavalon/flora/flora.container.css"; // instead of flora.css
// and/or
import "@useavalon/flora/templates.container.css";<aside class="flora-container">
<!-- these columns react to <aside>'s width, regardless of the viewport -->
<div class="flora-grid"> … </div>
</aside>Add class="flora-container" (or container: flora / inline-size) to any ancestor; the container-flavoured grid/templates query it by name. Generate a custom one with generateGridCss({ query: "container" }) / generateTemplatesCss({ query: "container" }).
Print — layouts that survive paper & PDF export
Flora targets editorial layouts, the kind people actually print or export. Add the opt-in print stylesheet and your grids render sensibly on paper instead of overflowing:
import "@useavalon/flora/flora.css";
import "@useavalon/flora/flora.print.css"; // @media print rulesIt only takes effect under @media print, and (like everything) lives in @layer flora. Out of the box it:
- keeps cards, tiles,
figures, and template slots from splitting across pages (break-inside: avoid); - collapses the responsive grid to full page width and drops the screen margins / max-width cap;
- fits aspect-ratio media to the page instead of overflowing it;
- turns the baseline debug overlay off and sets
print-color-adjust: exactso backgrounds, borders, and tints survive the printer.
Two utilities and a few tokens let you steer it:
<!-- force a page break before/after any block (e.g. a template section) -->
<section class="flora-tmpl-cover flora-break-after">…</section>
<section class="flora-tmpl-editorial flora-break-before">…</section>
<!-- keep an arbitrary block whole; hide screen-only chrome on paper -->
<div class="flora-break-avoid">…</div>
<nav class="flora-screen-only">…</nav>/* retune the print collapse without touching screen styles */
@media print {
:root {
--flora-print-margin: 12mm; /* default 0 */
--flora-print-max-width: none; /* default 100% */
--flora-print-gutter: 8px; /* default: the screen gutter */
}
}Generate a customized one (e.g. against your own grid selector) with generatePrintCss({ selector, layer }).
Templates — drop your components into ready-made layouts
Opt-in, editorial compositions built on grid-template-areas. Import the extra stylesheet, add a flora-tmpl-* class, and place your own components (any framework, or plain HTML) into named slots. They stack to one column on mobile and compose at md+.
import "@useavalon/flora/flora.css";
import "@useavalon/flora/templates.css";<section class="flora-tmpl-cover">
<p class="flora-area-eyebrow">Issue 01</p>
<h1 class="flora-area-title">RETRO</h1>
<div class="flora-area-meta">Spring 2025</div>
<img class="flora-area-media" src="…" alt="" />
</section>
<!-- swap sides with flora-flip -->
<section class="flora-tmpl-split flora-flip">
<MyGallery class="flora-area-media" />
<div class="flora-area-content">…</div>
</section>Included templates and their slots:
| Template | Slots | For |
| --- | --- | --- |
| flora-tmpl-cover | eyebrow title meta media | Brand / magazine covers |
| flora-tmpl-split | media content (+flora-flip) | Heroes, about sections |
| flora-tmpl-editorial | lede body figure pull | Article / newsletter spreads |
| flora-tmpl-showcase | hero detail thumbs | Portfolio / project pages |
| flora-tmpl-feature | head items | Strategy / feature sections |
| flora-tmpl-toc | head list media | Table of contents / index |
| flora-tmpl-team | head members | Team / people grids |
| flora-tmpl-gallery | head gallery | Mixed-size image galleries |
Slots are just grid-areas, so you can also place a child with style="grid-area: title". And since it's all in @layer flora, retune any template with an ordinary rule — e.g. .flora-tmpl-cover { grid-template-columns: 2fr 1fr; }. Generate a customized set with generateTemplatesCss({ breakpoint, layer }).
Grid maths
Pure, framework-agnostic helpers:
import { columnWidth, spanWidth, typeScale, flexibilityScore } from "@useavalon/flora";
columnWidth({ container: 1200, columns: 12, gutter: 32, margin: 40 }); // 64
spanWidth({ container: 1200, columns: 12, gutter: 32, margin: 40, span: 6 }); // 544
const t = typeScale(16, "perfectFifth", { baseline: 8 }); // 3:2
t.at(1); // 24 — font size
t.at(-1); // 10.67
t.lineHeightAt(1); // 40 — line-height, snapped to the 8px baseline
flexibilityScore(12); // 4 — {2,3,4,6}; a prime like 13 → 0Tokens
BASE_UNIT, SPACING_STEPS, BREAKPOINTS, SCALE_RATIOS, ASPECT_RATIOS, DEFAULT_COLUMNS/GUTTER/MARGIN, MIN_TOUCH_TARGET — exported as plain data to read in JS or feed to the generator.
Development
Flora is written in dependency-free TypeScript and ships its source. The examples double as the test suite (each asserts and prints ALL CHECKS PASSED):
bun install
bun run build # tsc → dist, then generate dist/styles/flora.css
bun run demo # generator
bun run demo:math # grid maths
bun run demo:tokens # default tokens
bun run demo:override # override storyOpen examples/index.html (after bun run build) for a live visual demo.
License
MIT © useAvalon
