Write HTML.
Not class soup.
mode.css styles semantic HTML out of the box, then lets you compose
everything with an exact family of µ-* attributes — no classes,
no build step, no runtime.
Introduction
One stylesheet turns plain, semantic markup into a polished, responsive,
themeable page. You never touch a class attribute.
Most CSS frameworks ask you to decorate every element with a string of utility classes. mode.css takes the opposite bet: your HTML should describe meaning, and the cascade should handle appearance. The entire library is built around a single principle — a strong attribute cascade.
| Instead of | You write |
|---|---|
<div class="grid grid-cols-3 gap-6"> |
<section µ-grid="3-col"> |
<div class="card border rounded shadow p-4"> |
<article> |
<h3 class="text-xl font-bold mb-2"> |
<h3> |
Installation
Add one <link>. There is no build step and nothing to configure.
From the CDN
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@microdom/mode.css@1/dist/mode.min.css">
From npm
npm install @microdom/mode.css
import "@microdom/mode.css" // flat, readable build
import "@microdom/mode.css/min" // minified build
Local file
<link rel="stylesheet" href="dist/mode.css">
Your first page
This is a complete, responsive page — sticky header with a no-JS hamburger menu, a centered reading column and a footer:
Hello, cascade
Native elements are styled automatically — no classes, no setup.
<body>
<input type="checkbox" id="menu-toggle" hidden>
<header>
<div>
<b> My Site</b>
<label for="menu-toggle"> <span> </span> </label>
<nav> <ul> <li> <a href="#"> Home</a> </li> </ul> </nav>
</div>
</header>
<main>
<section>
<h1> Hello, cascade</h1>
<p> Native elements are styled automatically.</p>
<button> A native button</button>
</section>
</main>
<footer> <div> <p> © 2026</p> </div> </footer>
</body>
The mental model
Everything you style lives in one of three attribute layers. Master these and you know the whole library.
| Layer | Role | Example |
|---|---|---|
| 1 · Structure | A bare attribute that establishes a layout. | <section µ-grid> |
| 2 · Modifier | Space-separated tokens that mutate the structure. | µ-grid="3-col cards" |
| 3 · Semantic | A domain attribute describing what the element is. | µ-pricing |
The one naming rule
Attribute values name the thing, never a CSS property. If a value reads like a Tailwind utility, it's wrong.
| Example | Why | |
|---|---|---|
| ✓ do | µ-grid="pricing" | Describes the block. |
| ✗ don't | µ-grid=" | That's a property, not a thing. |
| ✗ don't | | Utility class in disguise. |
The page skeleton
mode.css homologates three pillars — header, main and
footer — so content always lines up on the same lateral gutter.
Direct children of <body> share --screen-padding, and the
real content centers inside --max-width. A <section> spans
the full width, so its background can bleed edge-to-edge while its text stays aligned.
<body>
<header> <div> … brand + nav … </div> </header>
<main>
<section> … reading content … </section>
</main>
<footer> <div> … </div> </footer>
</body>
Grids
A bare µ-grid is auto-responsive. Add a column token to take control.
Auto grid (default)
With no value, the grid auto-fits as many 280px+ columns as will fit.
Auto
Fits to space.
Fit
No breakpoints to manage.
Flow
Wraps when it needs to.
<section µ-grid >
<article> <header> <h3> Auto</h3> </header> <p> Fits to space.</p> </article>
<article> … </article>
</section>
Explicit columns
Tokens 2-col through 6-col set a fixed track count that
collapses gracefully on smaller screens (1 → 2 → full).
One
3-col on desktop.
Two
2-col on tablet.
Three
1-col on mobile.
<section µ-grid="3-col" >
<article> … </article>
<article> … </article>
<article> … </article>
</section>
| Token | Desktop columns |
|---|---|
µ-grid | auto-fit (min 280px) |
2-col … 6-col | 2 – 6 fixed tracks |
sidebar-left | narrow rail + wide content |
sidebar-right | wide content + narrow rail |
Native elements
Headings, text, links, buttons and form fields are styled with zero attributes. These are the building blocks every layout inherits.
<h1> Heading 1</h1>
<h2> Heading 2</h2>
<p> Body text with a <a href="#"> link</a> .</p>
<button> Primary action</button>
Content elements
Quotes, code, keys, rules, figures and description lists — the prose furniture — are styled with zero attributes, in the same tokens as everything else.
“mode.css is an exact promise: semantic HTML looks finished. If an element ships with the browser, it ships styled.”
Inline code reads as code, and shortcuts like Ctrl + K read as keys.
<!-- One attribute, one mutation -->
<div µ-table="collapsed">
<table>…</table>
</div>
<blockquote>
<p> “mode.css is an exact promise: semantic HTML looks finished.”</p>
</blockquote>
<p> Inline <code> code</code> reads as code, and shortcuts like
<kbd> Ctrl</kbd> + <kbd> K</kbd> read as keys.</p>
<hr>
<pre> <code> …</code> </pre>
Description lists
- Tables
- Data tables by default,
µ-tablefor the irreducible. - ARIA states
- Truthful state attributes, visually rewarded.
- Content elements
- Quotes, code, keys, figures and lists.
<dl>
<dt> Tables</dt>
<dd> Data tables by default, µ-table for the irreducible.</dd>
<dt> ARIA states</dt>
<dd> Truthful state attributes, visually rewarded.</dd>
</dl>
Figures
figure with its figcaption, muted and small by default.<figure>
<img src="…" alt="Placeholder illustration">
<figcaption> A figure with its figcaption.</figcaption>
</figure>
Cards
Any <article> inside a µ-grid becomes a card — a
flex column with border, padding and radius. Use <header>,
<p> and <footer> for consistent rhythm.
Starter
Everything you need to ship a small site.
Pro
For teams that need more room to grow.
<section µ-grid="2-col" >
<article>
<header> <h3> Starter</h3> </header>
<p> Everything you need to ship a small site.</p>
<footer> <button> Choose</button> </footer>
</article>
<article> … </article>
</section>
<footer> button stretches to the card width automatically.Tables
A bare <table> is a styled data table. Wrap it in
µ-table to keep wide data scrollable — or collapsed into cards on phones.
Captions, header and footer rows, and row-hover feedback all come from the elements
themselves. Controls mutate through the cascade alone: a <button>
inside a table becomes dense to fit the row, and links pick up the accent color and
an underline.
| Invoice | Client | Status | Amount | Actions |
|---|---|---|---|---|
| #1042 | Aurora Labs | Paid | $12,400.00 | |
| #1039 | Helix & Co. | Pending | $8,150.00 | |
| #1036 | Nadir Studio | Paid | $3,975.50 | |
| #1031 | Vega Media | Overdue | $1,200.00 | |
| 4 invoices | $25,725.50 |
<div µ-table >
<table>
<caption> Invoices — Q3 2026, sorted by amount</caption>
<thead>
<tr> … <th aria-sort="descending" > <a href="#"> Amount</a> </th> … </tr>
</thead>
<tbody>
<tr> … <td> <button> View</button> </td> </tr>
<tr aria-selected="true" > … </tr>
</tbody>
<tfoot> <tr> <td> 4 invoices</td> … </tr> </tfoot>
</table>
</div>
aria-sort="descending" — truthful
because the data is rendered sorted — and one row carries
aria-selected="true".Scroll wrapper
The bare attribute keeps the semantics intact and adds horizontal scroll when the viewport is narrower than the data — zero markup cost on the table itself.
Collapsed variant
Same table language, one mutation: µ-table="collapsed". Below 640px each
row becomes a card and every cell shows its data-label — the declared
markup cost of this pattern. Resize to see it.
| Date | Event | Location | Seats |
|---|---|---|---|
| 2026-08-02 | Microdom meetup #4 | Buenos Aires | 120 |
| 2026-09-14 | Classless CSS workshop | Online | 500 |
| 2026-10-30 | Mode v3 launch | Córdoba | 80 |
<div µ-table="collapsed" >
<table>
<thead> … </thead>
<tbody>
<tr>
<td data-label="Date" > 2026-08-02</td>
<td data-label="Event" > Microdom meetup #4</td>
<td data-label="Location" > Buenos Aires</td>
<td data-label="Seats" > 120</td>
</tr>
<tr> … </tr>
</tbody>
</table>
</div>
<thead> is hidden visually but kept for assistive
technology.Tabs
HTML has no tabs element, so µ-tabs earns its attribute: radios +
labels, the same no-JS hack as the hamburger menu.
Pair each <input type="radio"> with a <label for>,
then add one <section> per panel — all direct children. The Nth radio
shows the Nth panel, up to six tabs (enumerated like the µ-grid column
spectrum). Bonus: radio groups already switch with ← → — try it
with the keyboard.
Each panel is a plain <section>. The Nth radio shows the Nth panel — up to six tabs, enumerated like the µ-grid column spectrum.
| Metric | Value |
|---|---|
| Signups | 1,284 |
| Churn | 2.1% |
<div µ-tabs >
<input type="radio" name="demo-tabs" id="tab-overview" checked>
<label for="tab-overview"> Overview</label>
<input type="radio" name="demo-tabs" id="tab-data">
<label for="tab-data"> Data</label>
<section> … first panel … </section>
<section> … second panel … </section>
</div>
Accordion
<details> is the browser's own accordion — the library only
dresses it. Exclusivity is the native name attribute: zero CSS, zero JS.
These three share name="faq", so they are exclusive: opening one closes
the others.
Why no JavaScript?
Same spirit as the hamburger menu: <details> is the browser’s own accordion. Zero runtime.
Why no classes?
Because the element already says what it is. Styling the element keeps the HTML the single source of truth.
Why the name attribute?
Exclusive groups are a native <details> feature — declare a shared name and the browser handles the rest.
<details name="faq" open>
<summary> Why no JavaScript?</summary>
<p> Same spirit as the hamburger menu. Zero runtime.</p>
</details>
<details name="faq" >
<summary> Why no classes?</summary>
<p> The element already says what it is.</p>
</details>
Forms
A <form> is a grid; wrap each field in a <label>
and everything aligns with shared focus styles.
<form>
<label> Full name <input type="text"> </label>
<label> Plan
<select> <option> Starter</option> </select>
</label>
<label> Message <textarea> </textarea> </label>
<button type="submit"> Send</button>
</form>
Fieldsets & options
Group related controls in a <fieldset>; the
<legend> acts as the group's label. When a
<label> wraps a checkbox or radio it flips from the column layout
to an aligned row, and the controls pick up the accent color — no extra markup on
any child.
<form>
<fieldset>
<legend> Notification settings</legend>
<label> <input type="checkbox" checked> Release announcements</label>
<label> <input type="radio" name="freq" checked> Weekly digest</label>
<label> <input type="radio" name="freq"> Real-time</label>
</fieldset>
<button type="submit"> Save preferences</button>
</form>
Invalid fields
A field that declares aria-invalid="true" gets a danger border. The
color reads from the optional --color-danger token and falls back to a
default red when undeclared.
<label>
Email
<input type="email" value="not-an-email" aria-invalid="true" >
</label>
ARIA states
Mode rewards accessible HTML: these looks come from truthful state attributes, never from classes.
State the truth in the markup and the styling follows. A sorted column declares
aria-sort (truthful even without JS when the server renders the order),
the current nav item declares aria-current="page" — replacing the classic
.active class — and a region waiting for data declares
aria-busy="true".
Busy region
This block declares aria-busy="true" while its data loads. It fades and ignores pointer events.
Disabled, unified
Native disabled and aria-disabled="true" look identical:
Invalid field
<div aria-busy="true" > … </div>
<button disabled > Native disabled</button>
<a role="button" aria-disabled="true" > ARIA disabled</a>
<input type="email" aria-invalid="true" >
<a href="/docs" aria-current="page" > Docs</a>
| Attribute | Visual reward |
|---|---|
aria-sort="ascending / descending" | Sort arrow on the <th>, pointer cursor |
aria-selected="true" | Accent-tinted table row |
aria-busy="true" | Region fades and ignores pointer events |
aria-disabled="true" / :disabled | Unified dimmed, not-allowed look |
aria-current="page" | Bold accent nav link |
aria-invalid="true" | Danger border (--color-danger) |
Tokens & theming
Every visual decision is a CSS custom property. Override them in :root —
no recompilation, no config file.
:root {
--color-bg : #ffffff;
--color-text : #1f2937;
--color-accent : #111827;
--spacing : 1.5rem; /* master rhythm unit */
--radius : 6px;
--max-width : 1200px; /* reading column */
--screen-padding : 1.5rem; /* shared gutter */
}
| Token | Controls |
|---|---|
--font-main | Base font stack |
--font-mono | Monospace stack (code, kbd, samp) |
--size-h1 … --size-small | Harmonic type scale |
--color-bg / text / muted / border / accent | Color system |
--color-danger | Invalid-field border (optional — has a built-in fallback) |
--spacing | Master spacing (scales up at breakpoints) |
--radius | Global corner radius |
--max-width | Reading-column width |
--screen-padding | Lateral gutter for the three pillars |
Dark mode
A checkbox and one CSS rule — no JavaScript. (This very page uses it; try the ◐ / ◑ toggle in the header.)
<!-- in your markup -->
<input type="checkbox" id="theme-toggle" hidden>
<label for="theme-toggle"> Toggle theme</label>
/* in your theme.css */
body:has(#theme-toggle:checked) {
--color-bg : #0b0d12;
--color-text : #c9d1d9;
--color-accent : #f3f6fb;
}
Because every component reads from the same tokens, re-painting the whole site is just a matter of swapping a handful of variables.
Migrating to mode
Refactoring class-heavy markup is mostly deletion. Flatten the divs, drop the classes, and let the cascade do the work.
Before — utility soup
<div class="grid grid-cols-3 gap-6 bg-gray-100 p-8 rounded-lg">
<div class="border p-4 rounded bg-white shadow">
<h3 class="text-xl font-bold text-blue-600 mb-2"> Client</h3>
<p class="text-sm text-gray-600"> Description…</p>
</div>
</div>
After — mode.css
<section µ-grid="3-col cards" >
<article>
<h3> Client</h3>
<p> Description…</p>
</article>
</section>
Aesthetics that the defaults don't cover (a brand color, a special card treatment) live in your theme layer, keyed off the Layer-3 attribute:
[µ-grid~="cards"] article {
box-shadow : var(--shadow-sm);
border-radius : var(--radius);
}