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="border border-ink 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

Heavy rule under the header, hairlines between rows. Add .table-striped for zebra rows or .table-dense for data-heavy views.

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
SHOW CODE
<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>

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>