Most product UIs don't fail at taste. They fail at consistency.
Hand a designer the same brief on Monday and on Friday. You'll get two pages that are both fine, and that don't quite agree with each other. Buttons drift. A heading picks up a new line height. One card wears a 2px border, the next wears 1px. Each page is acceptable on its own. Together they read as two different products built by two different people.
That does not mean designers are optional. It means the best design work has to survive the next page, the next component, the next developer, the next coding agent, and the next late-night change.
That realization became Inkwell, a pure-CSS design system I reach for whenever I start a new project. This post is a field report on how I built it, the discipline it costs, and the decisions you can lift directly into your own work.
Consistency is a system, not a vibe
Stunning UIs aren't created by one beautiful page. They're enforced across all the pages that come after it.
The work of design is not only making one screen look good. Plenty of people can do that. The harder work is making 15 pages look like the same hand drew them on the same day. There are two ways to get there.
The first is talent so absolute that you redraw every decision the same way every time without thinking. That scales to one person. The second is to write the decisions down once, name them, and stop remaking them. That scales to a team, an LLM, and future you at 11 p.m.
Inkwell is the second path, made concrete. At the core: four CSS files. Add one optional Tailwind v4 theme file if your project already lives there. No build step. No dependencies. The basic install is one line in your <head>:
<link rel="stylesheet" href="inkwell.css">
What you get back is a tokenized color system, three type families with assigned jobs, a 1.5px hairline running through everything, and dark mode that lifts the accents instead of inverting them.
The hard work isn't writing the system. The hard work is making yourself follow it.
Name your taste before you write any components
The most common failure mode in a young design system is reaching for components first. "I need a card. I need a button. I need an alert."
So you write a .card with border: 1px solid #ddd; border-radius: 8px; and ship it. A sprint later, you add a .panel with border: 2px solid #ccc; border-radius: 12px; because, you know, it's a panel, not a card.
Six components in, you have six versions of "thin rectangle with a border," and nobody can tell you which one is canonical.
The fix is boring, which is usually a sign it might work. Decide the atoms before you build the molecules.
Inkwell defines one border token:
:root {
--border: 1.5px solid var(--gray-300);
}
Every panel-shaped component points at that token. The day I decide hairlines should be 1.25px, I change one line and the whole system breathes in at once.
Colors follow the same rule. Inkwell ships 12 hues mapped through about 25 semantic tokens. --ivory is for page background. --paper is for surfaces. --slate is for primary text. --accent is for the one, yes, one, saturated brand color.
Components never see hex codes. They see semantic tokens. The hex values only live in :root.
This sounds obvious until you check your own discipline. In Inkwell v1.3.1, I shipped a patch release whose entire job was sweeping six raw rgba() literals out of inkwell-components.css. Past me had typed colors inline instead of defining them as tokens. Very rude of him, frankly.
Promoting one of those values to a paired light/dark token looked like this:
:root {
--olive-strong-border: rgba(120, 140, 93, 0.45);
}
@media (prefers-color-scheme: dark) {
:root {
--olive-strong-border: rgba(156, 176, 122, 0.6);
}
}
Five of the six literals I swept were also dark-mode bugs the system had been silently carrying. The values were locked to the light-mode hue, so on a dark surface the border kept its old color while the background flipped.
Tokens would have prevented every one of them. The lesson costs you exactly one refactor, and you'll probably do it on yourself before you do it on anyone else.
Assign jobs, not preferences
The thing that gives Inkwell its editorial feeling is not the colors. It's three font families doing very specific work.
:root {
--serif: ui-serif, Georgia, "Times New Roman", Times, serif;
--sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--mono: ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace;
}
Serif handles headings, stat numbers, and italic emphasis. It carries the editorial gravitas.
Mono handles eyebrows, file names, hex codes, and table headers. It signals technical metadata.
Sans handles everything else. It's the default.
You don't choose a font in Inkwell. You choose a job. A button is sans because a button is interactive, not editorial. An eyebrow tag above a section is mono because it signals metadata. A page hero is serif because it's making a statement.
People resist this principle the most when I describe the system, and it pays back the most once they try it. Stop thinking, "What font feels right here?" Start thinking, "What role does this text play?" Every page in your project starts to harmonize whether you're paying attention or not.
One bonus: I use platform fonts only. ui-serif, system-ui, and ui-monospace. No webfont download, no flash of unstyled text, and no licensing. Some readers will hate this. It's fine. The system is meant to look like a thoughtful default, not a logo.
Pick a signature and ride it everywhere
Inkwell has a signature, and it's silly: borders are 1.5px, never 1px, never 2px.
A 1px border can read as wireframe. A 2px border can read as playful. A 1.5px border reads as a hairline. Once you see a page of 1.5px borders against cool-putty neutrals, you can't unsee it.
It's the single most identifiable thing about the system, and it cost nothing to decide.
The 1.5px is retina-first by design. On lower-density displays, browsers may snap border widths to align with device pixels, so the computed value can differ from the authored value. That is not a bug in Inkwell. It is the browser doing what browsers do: trying to keep borders crisp.
If you want to verify the signature visually, do it on a 2x display.
The point isn't the 1.5px specifically. The point is that every design system needs one technical choice nobody else makes, applied with embarrassing consistency. Maybe it's your radius scale. Maybe it's your shadow profile. Maybe it's the way you handle accent tints.
Find your 1.5px and ride it everywhere. That's the thing people will remember.
Define dark mode at the same time as light mode
Most systems retrofit dark mode. Inkwell defines both modes for every saturated token in the same file, on the same screen.
The rule: every saturated color gets a lifted dark counterpart. Same hue, more luminance.
:root {
--accent: #3B4A8C; /* deep indigo on cool stone */
}
@media (prefers-color-scheme: dark) {
:root {
--accent: #7A8AD1; /* periwinkle on near-black */
}
}
#3B4A8C against a near-black background reads as a hole punched in the page, not a highlight. #7A8AD1 restores the sense of "this thing matters" by keeping the hue but gaining luminance.
Apply that rule to every colored token and dark mode stops looking like an afterthought.
A token defined only in :root will look wrong on a dark surface. That's the trap most systems fall into. They flip the background and call dark mode done. The accent has to move with the room, or the system fails it.
Name your anti-patterns out loud
Every design system document I've read describes what the system is. Almost none describe what the system isn't. That asymmetry is where drift comes from.
Inkwell's spec has a §4 titled "Anti-patterns." It reads like a list of grievances I have with my past self:
- No pure white backgrounds. Use
--ivory. White on white kills depth. - No pure black text. Use
--slate(#13141B). Pure black is too cold. - No warm grays. The neutrals here are cool putty. Warm beige makes the indigo accent feel orphaned.
- No multiple accent colors. One indigo, period. If you need a second hue for data visualization, reach for
--oliveor--sky. - No sans-serif headings. Serifs do the editorial work. Sans headings collapse the personality.
- No emoji icons. Inline SVG strokes only.
- No hex literals in component CSS. Always reference tokens. Always.
The anti-pattern list also has to include accessibility failures. No invisible focus states. No status conveyed by color alone. No tiny tap targets because the page looks better in a screenshot.
A design system that only enforces aesthetics is incomplete. The system should protect the user from my taste when my taste gets too clever.
Writing these rules down makes you realize what consistency really is: the absence of specific small mistakes. The interesting design decisions aren't infinite. There are maybe a dozen places where you can take a wrong turn.
Name them, and you're 80 percent of the way to a coherent system.
Write the system for people and agents
The other audience for Inkwell is not just me. It is the coding assistant I am going to hand the next screen to.
That changes how the system has to be written.
A vague design system tells a human designer what direction to aim. A useful design system tells an agent what not to invent. Use this token. Do not create a second accent. Use serif headings. Do not reach for emoji icons. Keep borders at 1.5px.
The more explicit the system is, the less creative the agent gets in the places where creativity becomes drift.
This is where design systems and AI-assisted development start to rhyme. Agents are excellent at producing plausible work. That is both the superpower and the risk. Without constraints, they will invent a good-looking local answer that quietly weakens the global system.
The answer is not to make the agent less capable. The answer is to give it better rails.
Inkwell's documentation has to serve two readers at once: the person making judgment calls and the agent applying the rules. The person needs the why. The agent needs the boundaries. Both need the same source of truth.
That is why the anti-patterns matter. That is why semantic tokens matter. That is why "use --accent" is better than "make this feel branded." The first instruction can be followed. The second one invites a tiny act of unauthorized creativity. Enough of those and your product starts wearing mismatched socks.
Be willing to refactor your own naming
The first cut of Inkwell was four palette variants: clay, sage, burgundy, and indigo. All shared one base file. The accent token was named --clay regardless of the actual hue because clay was first. The indigo variant still referenced var(--clay) for its accent.
It worked, technically. It also made obvious that --clay was a brand-layer name pretending to be a semantic token.
The fix was to rebuild the canonical layer with --accent as the semantic name. I demoted the original four variants to a variants/ folder, kept them as references, and marked them as explicitly not the place new work happens.
The repo now contains two universes. --accent is for new work. --clay is for the legacy palette set. The README warns you, in bold, not to mix them.
That kind of refactor is the most painful move to make and the most clarifying. You can't build a repeatable system by being precious about your earlier decisions.
The version of you that named the token six months ago is the version you most often need to disagree with. Writing the disagreement down, where it can be pointed at, beats silent reinvention every time.
Where this approach does not fit
Inkwell is not for every project.
If you're building a brand-led marketing site where the hero color is the brand and changes by campaign, a tokenized system biased toward editorial restraint will fight you. If you're shipping a white-label SaaS where each tenant gets a different accent and font, you need a theming layer Inkwell doesn't ship.
If you treat every page as a unique composition rather than an instance of a system, the constraints will feel like a cage.
The system works best for what I built it for: product UI, dashboards, technical documentation, and editorial layouts where the brand is "thoughtful default" rather than "vivid signature."
Use the right tool for the job. Even the best hammer gets weird when you start aiming it at soup.
What repeatable actually buys you
The honest payoff is easy to undersell.
Sit down in front of a blank page in an Inkwell project. The questions you actually have to answer are: What is this page for? What's the hierarchy? What's the data shape?
You don't think about button colors. You don't think about border widths. You don't think about heading font choice. Those decisions are gone. Made once, named, tokenized, and they don't come back as long as the system holds. That's the unlock.
A design system isn't a UI library. It's a memory aid. It's the place where you put down the decisions you don't want to keep paying for. The repeatable process isn't about being faster at making the same decisions. It's about not making them at all.
The work that's left, once the system carries its weight, is the work that actually deserves your attention: figuring out what to put on the page, in what order, with what emphasis.
That is the part where taste matters. Everything below that line should be on rails.
If you take one thing from this, take this. Before you write your next component, write the token it points at. Then grep your existing component CSS for # and rgba(. Every literal you find outside :root is a future drift bug hiding in plain sight. Sweep them now. The discipline pays back faster than you expect.
Inkwell is open source under MIT. See it in use at inkwell.vinny.dev. Open any page and view source: everything is in the CSS files.
Made with hairlines and serifs. The 1.5px is on purpose.
