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
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.
Tokens
2 layersInkwell 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 | #0F1018 | Page background |
--paper | #FFFFFF | #181A24 | Card surface |
--slate | #13141B | #E8E8EE | Primary text |
--accent | #3B4A8C | #7A8AD1 | Links, focus, active |
--olive | #788C5D | #9CB07A | Success |
--rust | #B04A3F | #D27468 | Danger |
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.
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.
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 |
|---|---|---|
.btn | Stable | Buttons (primary / secondary / ghost / danger) |
.input · .textarea · .select | Stable | Form controls with .is-error + :disabled |
.field | New | Label + control + help/error wrapper |
.radio · .switch · .checkbox | Stable | Selection controls |
.alert | New | Flat-tinted system messages (4 severities) |
.code-block | New | Multi-line <pre><code> with copy slot |
.dialog | New | Native <dialog> styling |
kbd | New | Keyboard-shortcut chip |
.tabs · .tab | New | Underline tab nav (aria-selected or .is-active) |
.tooltip | New | CSS-only bubble via [data-tooltip] |
.breadcrumbs | New | <ol> with / separators & aria-current |
.pagination | New | Numbered page list with prev/next + ellipsis |
.skeleton | New | Shimmer placeholder, reduced-motion-safe |
.empty-state | New | Centered no-data panel with icon slot |
.card · .stat-card | Stable | Surfaces with optional .is-link hover lift |
.tbl · .tldr · .toc | Stable | Editorial primitives |
.timeline · .pill · .chip-dot | Stable | Status & sequence patterns |
Accessibility
WCAG-conscious--gray-700on--paper: 10.9:1 · primary body text--gray-500on--paper: 5.05:1 · captions, muted labels--accentfocus ring on--paper: 7.9:1 light · 5.2:1 dark--gray-300hairline 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.
Extend
tokens, never literalsBuild 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.
Made with hairlines and serifs by Vinny Carpenter. The 1.5px is on purpose.