Documentation guide

Getting started with Inkwell

A pure-CSS design system for product UI, dashboards, and editorial interfaces. Two files, no build step, light and dark out of the box. This page walks through installing it, understanding the token model, and building a custom component without breaking the system.

Last updated · 2026-05-09 · v1.1

01

Install

2 files

Copy tokens.css and inkwell.css into your project. Link inkwell.css from your <head> — it re-exports the tokens file, so one link is all you need.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="inkwell.css">
  <title>My new app</title>
</head>
<body>
  <div class="wrap">
    <h1 class="t-h1">Hello, Inkwell</h1>
  </div>
</body>
</html>

No bundler, no preprocessing

Inkwell ships unminified by design. Open tokens.css in your editor — it's the source of truth and the spec, in the same file.

02

Tokens

2 layers

Inkwell separates structure (borders, type scale, spacing, motion, components) from brand (colors). The structural layer is identical across palettes; only the brand-layer tokens change. This means a future palette swap is a ~30-line file, not a fork.

Token Light Dark Role
--ivory#F4F4F0#0F1018Page background
--paper#FFFFFF#181A24Card surface
--slate#13141B#E8E8EEPrimary text
--accent#3B4A8C#7A8AD1Links, focus, active
--olive#788C5D#9CB07ASuccess
--rust#B04A3F#D27468Danger

Notice every saturated token is lifted in dark mode — same hue, more luminance. #3B4A8C against near-black would read as a hole in the page; #7A8AD1 reads as an accent. Apply this rule to any new colored token.

03

Theming

3 states

Dark mode applies automatically via prefers-color-scheme. Users can override with a data-theme attribute on <html>. The toggle in the top-right of this page is a working three-state widget — try it with Tab and Enter.

// Persist user choice; default to OS preference.
const KEY = 'inkwell-theme';
const saved = localStorage.getItem(KEY);
if (saved === 'light' || saved === 'dark') {
  document.documentElement.setAttribute('data-theme', saved);
}

function setTheme(choice) {
  if (choice === 'auto') {
    document.documentElement.removeAttribute('data-theme');
    localStorage.removeItem(KEY);
  } else {
    document.documentElement.setAttribute('data-theme', choice);
    localStorage.setItem(KEY, choice);
  }
}

Avoid the flash

Read localStorage and set the attribute before paint — inline this script in <head>, not in a deferred bundle. The index.html example shows the pattern.

04

Components

26 classes

Each component is a single class plus optional modifiers. None of them require JavaScript except <dialog>, which uses the native showModal() API for focus trap and ::backdrop.

Class Status Purpose
.btnStableButtons (primary / secondary / ghost / danger)
.input · .textarea · .selectStableForm controls with .is-error + :disabled
.fieldNewLabel + control + help/error wrapper
.radio · .switch · .checkboxStableSelection controls
.alertNewFlat-tinted system messages (4 severities)
.code-blockNewMulti-line <pre><code> with copy slot
.dialogNewNative <dialog> styling
kbdNewKeyboard-shortcut chip
.tabs · .tabNewUnderline tab nav (aria-selected or .is-active)
.tooltipNewCSS-only bubble via [data-tooltip]
.breadcrumbsNew<ol> with / separators & aria-current
.paginationNewNumbered page list with prev/next + ellipsis
.skeletonNewShimmer placeholder, reduced-motion-safe
.empty-stateNewCentered no-data panel with icon slot
.card · .stat-cardStableSurfaces with optional .is-link hover lift
.tbl · .tldr · .tocStableEditorial primitives
.timeline · .pill · .chip-dotStableStatus & sequence patterns
05

Accessibility

WCAG-conscious
TL;DR
Body text clears WCAG AA (4.5:1). Focus rings clear AA non-text (3:1) by a margin. The 1.5px hairline border is deliberately below 3:1 — visual separation comes from border + surface tone shift + shadow combined.
  • --gray-700 on --paper: 10.9:1 · primary body text
  • --gray-500 on --paper: 5.05:1 · captions, muted labels
  • --accent focus ring on --paper: 7.9:1 light · 5.2:1 dark
  • --gray-300 hairline on --paper: 1.57:1 light · 1.45:1 dark — intentional, see above

Reduced-motion is honored

All transitions and animations collapse to ~0ms under prefers-reduced-motion: reduce. The card-hover lift, dialog pop, and switch-knob slide all degrade gracefully.

06

Extend

tokens, never literals

Build new components by composing existing tokens. Never hardcode hex values in component CSS — that's the rule that keeps the system theme-able. Here's a notification card that respects the entire token system, including dark mode, with no extra work:

.notice {
  background: var(--paper);
  border: var(--border);
  border-left: 4px solid var(--accent);
  border-radius: var(--r-md);
  padding: var(--sp-4) var(--sp-5);
  box-shadow: var(--shadow-sm);
  font: var(--t-body)/1.55 var(--sans);
  color: var(--slate);
}
.notice strong {
  font-family: var(--serif);
  font-weight: 500;
}

Every value is a token. Switch to dark mode and this component shifts surfaces, text, accent, and shadow in lockstep — no @media (prefers-color-scheme: dark) needed in your component file.

When you need a second hue

For data viz with multiple series, reach for --olive or --sky. Do not introduce a second saturated brand color — it dilutes the accent's job.

Source · tokens.css (the spec) · inkwell.css (the alias) · DESIGN_SYSTEM.md (the canonical reference). Every example on this site is built from the same two CSS files you'd link from your own project.

Made with hairlines and serifs by Vinny Carpenter. The 1.5px is on purpose.