Skip to content

Every element, in ink and paper

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.

01 — Buttons

Three variants, three sizes. The primary button is solid ink; hover inverts it. Disabled drops to gray — never half-transparent.

SHOW CODE
<button class="btn">Primary</button>
<button class="btn btn-secondary">Secondary</button>
<button class="btn btn-ghost">Ghost</button>

<button class="btn btn-sm">Small</button>
<button class="btn btn-lg">Large</button>

<button class="btn" disabled>Disabled</button>
<button class="btn btn-loading">Saving</button>

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.

Subscriptions
Theme
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.

Create account

Email is not valid.

Password is too short — 12 characters minimum.

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

The roster, by the numbers
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

Stacked on phones — resize to see

The roster, stacked variant
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.

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>

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.

applies live, saves to your browser

--paper

The page itself. Everything sits on it.

.panel

Cards, frames, tiers — one shade up (or one border out, in lines mode).

.panel-raised

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.

Everything else

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>