COMPONENTS
Buttons, forms, tables, and the dynamic parts — tabs, dialogs,
dropdowns, toasts. Each one accessible, keyboard-friendly, and
ready to copy. The classes live in
css/mono.css;
the behaviors in
js/mono.js.
02 — Text input & textarea
Label, field, hint — in that order. Error states use a dashed border
and a message wired up with aria-describedby; never
color alone.
As it should appear in print.
That doesn't look like an email address.
Available.
Disabled fields go gray, not ghostly.
SHOW CODE
<div class="field">
<label class="label" for="name">Name</label>
<input class="input" type="text" id="name" placeholder="Ada Lovelace" />
<p class="hint">As it should appear in print.</p>
</div>
<!-- Error state -->
<div class="field">
<label class="label" for="email">Email</label>
<input class="input" type="email" id="email"
aria-invalid="true" aria-describedby="email-error" />
<p class="error-text" id="email-error">That doesn't look like an email address.</p>
</div>
<!-- Textarea -->
<div class="field">
<label class="label" for="bio">Bio</label>
<textarea class="input" id="bio" rows="3"></textarea>
</div>
03 — Select
A native <select> with the chrome stripped and a
CSS triangle for a marker — it inverts with the theme because it's
drawn in --ink.
SHOW CODE
<div class="field">
<label class="label" for="font">Typeface</label>
<div class="select">
<select id="font">
<option>Space Mono</option>
<option>JetBrains Mono</option>
</select>
</div>
</div>
04 — Checkbox & radio
Square check, round radio — geometry tells them apart before the
cursor does. Both are real inputs under
appearance: none.
SHOW CODE
<label class="choice">
<input type="checkbox" class="check" checked /> Release notes
</label>
<label class="choice">
<input type="radio" name="theme" class="radio" checked /> Ink on paper
</label>
05 — Toggle switch
A checkbox in a track. The thumb slides; with
prefers-reduced-motion it simply jumps.
SHOW CODE
<label class="choice">
<input type="checkbox" class="switch" checked /> Autosave
</label>
06 — Range
A hairline track and a square thumb. Add
data-mono-output and the value renders live into any
element.
SHOW CODE
<label class="label" for="contrast">Contrast — <output id="contrast-out"></output></label>
<input type="range" class="range" id="contrast" min="0" max="100" value="80"
data-mono-output="contrast-out" data-unit="%" />
07 — File input
The native input, with its button restyled through
::file-selector-button. No hidden-input tricks, so
keyboard and screen reader behavior stay stock.
Black and white images preferred, obviously.
SHOW CODE
<div class="field">
<label class="label" for="attachment">Attachment</label>
<input type="file" class="file" id="attachment" />
</div>
08 — Fieldset & validation
The full pattern: a <fieldset> with a legend, an
error summary at the top, and per-field messages tied in with
aria-invalid and aria-describedby.
SHOW CODE
<div class="panel-raised p-4" role="alert">
<p class="text-step--1 font-bold">2 fields need attention</p>
<ul class="text-step--1 mt-2 list-disc list-inside">
<li><a href="#email">Email is not valid</a></li>
</ul>
</div>
<fieldset>
<legend class="label">Create account</legend>
<div class="field">
<label class="label" for="email">Email</label>
<input class="input" type="email" id="email"
aria-invalid="true" aria-describedby="email-err" />
<p class="error-text" id="email-err">Email is not valid.</p>
</div>
</fieldset>
09 — Table
Structure from the header rule, row shading, and breathing room.
Add .table-striped for zebra rows or
.table-dense for data-heavy views. Wrap wide tables
in .table-scroll so phones scroll instead of
crushing columns — cell padding grows on small screens, it never
shrinks. For phone-first data, .table-stack turns
each row into a labelled card under 640px (give every cell a
data-label).
| Typeface | Designer | Weights | Year |
|---|---|---|---|
| Space Mono | Colophon Foundry | 400, 700 | 2016 |
| JetBrains Mono | JetBrains | 100–800 | 2020 |
| IBM Plex Mono | Bold Monday | 400, 700 | 2017 |
| Fragment Mono | Wei Huang | 400 | 2022 |
| Typeface | Designer | Year |
|---|---|---|
| Space Mono | Colophon Foundry | 2016 |
| Courier Prime | Alan Dague-Greene | 2013 |
SHOW CODE
<!-- Wide data: wrap for horizontal scroll on phones -->
<div class="table-scroll">
<table class="table table-striped">
<caption>The roster, by the numbers</caption>
<thead>
<tr><th scope="col">Typeface</th><th scope="col">Year</th></tr>
</thead>
<tbody>
<tr><td>Space Mono</td><td>2016</td></tr>
</tbody>
</table>
</div>
<!-- Phone-first data: rows become labelled cards under 640px -->
<table class="table table-stack">
<tbody>
<tr>
<td data-label="Typeface">Space Mono</td>
<td data-label="Year">2016</td>
</tr>
</tbody>
</table>
10 — Tabs
Proper role="tablist" semantics with arrow-key
navigation and roving tabindex — wired automatically by
js/mono.js.
Strip away the non-essential. Color, decoration, complexity — gone.
Perfect what remains: typography, spacing, and proportion.
Create patterns and relationships with limited elements.
SHOW CODE
<div class="tabs" role="tablist" aria-label="Sections">
<button class="tab" role="tab" id="tab-1" aria-controls="panel-1"
aria-selected="true">Reduce</button>
<button class="tab" role="tab" id="tab-2" aria-controls="panel-2"
aria-selected="false" tabindex="-1">Refine</button>
</div>
<div class="tabpanel" id="panel-1" role="tabpanel" aria-labelledby="tab-1">…</div>
<div class="tabpanel" id="panel-2" role="tabpanel" aria-labelledby="tab-2" hidden>…</div>
11 — Accordion
Native <details> — zero JavaScript, fully
keyboard accessible, and the browser remembers how it works.
Does it work without JavaScript?
Yes. This accordion is a native
<details> element; the only thing CSS adds is the plus-minus marker.
Can more than one be open?
By default, yes. Add a shared
name attribute to make them exclusive in modern browsers.
SHOW CODE
<details class="accordion">
<summary>Does it work without JavaScript?</summary>
<p>Yes. It's a native element.</p>
</details>
12 — Modal
Native <dialog>: focus trapping, Escape, and the
backdrop come free from the browser. Clicking outside closes it.
SHOW CODE
<button class="btn" data-mono-modal-open="confirm">Open modal</button>
<dialog id="confirm" class="modal" aria-labelledby="confirm-title">
<h3 id="confirm-title" class="text-step-2 font-bold">Delete everything?</h3>
<p>…</p>
<button class="btn btn-secondary" data-mono-modal-close>Cancel</button>
<button class="btn" data-mono-modal-close>Proceed</button>
</dialog>
13 — Dropdown menu
A button with aria-expanded and a panel. Escape or an
outside click closes it.
SHOW CODE
<div class="dropdown" data-mono-dropdown>
<button class="btn btn-secondary" aria-expanded="false"
aria-haspopup="true">Actions ▾</button>
<ul class="dropdown-menu" hidden>
<li><button>Duplicate</button></li>
<li><button>Delete</button></li>
</ul>
</div>
14 — Tooltip
Pure CSS via data-tooltip; it appears on hover and on
keyboard focus. Keep it to a few words — tooltips are captions, not
paragraphs.
SHOW CODE
<button class="btn btn-secondary"
data-tooltip="Saves to local storage">Hover or focus me</button>
15 — Toast
Call mono.toast('message') from anywhere, or put the
message in a data-mono-toast attribute. Toasts land in
a role="status" region and dismiss themselves.
SHOW CODE
<!-- Declarative -->
<button class="btn" data-mono-toast="Changes saved.">Save changes</button>
<!-- Programmatic -->
<script>
mono.toast("Changes saved.");
</script>
16 — Surfaces & structure
Structure is a token, not a habit. .panel reads the
structure axis: soft (the default) builds from
shades and whitespace, lines boxes everything in
ink, min dissolves into negative space. Switch it
here — or in the TUNE panel — and the whole site follows. In dark
mode the tiers become layered grays, never flat black.
The page itself. Everything sits on it.
Cards, frames, tiers — one shade up (or one border out, in lines mode).
A second step for emphasis and nested surfaces.
SHOW CODE
<!-- The structure axis lives on <html> -->
<html data-structure="soft"> <!-- outlined | soft | minimal -->
<div class="panel p-6">A card.</div>
<div class="panel-raised p-6">A raised card.</div>
<div class="panel-inverse p-6">Solid ink.</div>
<script>
monoTheme.set({ structure: "minimal" }); // applies + persists
</script>
17 — Tab bar & sheet
Phone navigation that behaves like an app: a fixed bottom bar with
icons and thumb-sized targets, and a bottom sheet
(<dialog class="sheet">) as the mega menu for
everything beyond four destinations. This site uses it on every
page — shrink the window and look down. The demo below is the
same component, inline.
Open MORE — the sheet slides up on phones and centers on desktop. Escape, backdrop, and ✕ all close it.
SHOW CODE
<!-- Fixed bottom bar (hide on desktop with lg:hidden).
Add class="has-tabbar" to <body> so content clears it. -->
<nav class="tabbar lg:hidden" aria-label="Primary">
<a href="/" class="tabbar-item" aria-current="page">
<i class="icon" data-icon="home" aria-hidden="true"></i><span>HOME</span>
</a>
<button class="tabbar-item" data-mono-sheet-open="menu" aria-expanded="false">
<i class="icon" data-icon="more" aria-hidden="true"></i><span>MORE</span>
</button>
</nav>
<!-- The mega menu: a native dialog -->
<dialog class="sheet" id="menu" aria-label="Menu">
<div class="sheet-handle" aria-hidden="true"></div>
<a href="/settings" class="sheet-link">
<i class="icon" data-icon="settings" aria-hidden="true"></i>
<span><span class="sheet-link-title block">SETTINGS</span>
<span class="sheet-link-desc block">Description.</span></span>
</a>
</dialog>
18 — Icons
One vocabulary, four libraries. Write
<i class="icon" data-icon="settings"></i>
once; Lucide, Tabler, Phosphor, or Material Symbols renders it —
whichever is active. Switch sets here or in the TUNE panel; the
choice persists and the whole site re-draws.
SHOW CODE
<!-- One canonical name, any library -->
<i class="icon" data-icon="settings" aria-hidden="true"></i>
<i class="icon icon-sm" data-icon="search" aria-hidden="true"></i>
<i class="icon icon-lg" data-icon="home" aria-hidden="true"></i>
<script>
monoTheme.set({ icons: "phosphor" }); // lucide | tabler | phosphor | material
</script>