Tinta Color Studio: Turn One Brand Color Into an Accessible Palette
Most clients hand me one thing when we start a website: a logo color. Turning that single hex into hover states, backgrounds, borders and readable text usually happens by eye, and that is how sites end up with grey text on green buttons that nobody can read in the sun. I built Tinta Color Studio, a free browser tool that turns one brand color into a full 50–950 palette, checks every pair against WCAG 2.2, and tells you exactly which color to use instead when a pair fails.
Tinta is Filipino for ink. It builds scales in OKLCH so every step looks evenly spaced, keeps the client’s exact hex on the closest step, previews the palette for color-blind visitors, and exports ready-to-paste CSS variables, a Tailwind config, SCSS maps or design tokens.
Try It: Fix a Failing Color Pair
This is a trimmed-down version of the real tool. Pick a brand color or a sample brand, then click swatches to set the text or background. When a pair fails, press Apply on the suggested color.
Build Footprint & Quality Checks
A color tool should load instantly and work offline, so there is no framework and no build step. These are the measured numbers from the v1.0.0 files:
Technology Stack & Architecture Layers
OKLab & OKLCH
A perceptual color space, so step 700 of a yellow and step 700 of a blue have similar lightness and similar contrast.
WCAG 2.2 Contrast
Relative luminance and ratio checks for AA, AAA, large text and UI components, plus Machado color-vision simulation.
Vanilla JavaScript
One state object, string renderers and event delegation. A pure UMD engine runs in the browser and in Node tests.
CSS · Tailwind · Tokens
Exports CSS custom properties, a tailwind.config.js, SCSS maps or W3C Design Tokens JSON.
Key Architectural Decisions
Pin the Brand Color
Each step has a target lightness. The step closest to the client’s color keeps their exact hex, so the logo color is always in the palette.
Reduce Chroma, Never Clip
Colors outside the sRGB gamut are pulled in by lowering chroma with a binary search. Hue and lightness stay put, so contrast stays predictable.
Smallest Possible Fix
For a failing pair, the engine searches toward darker and lighter and suggests whichever passing color changes the least.
Pure, Tested Core
All math sits in color-core.js with no DOM access. Ten Node tests check reference ratios such as #767676 on white = 4.54:1.
Contrast Math, Scale Builder & Usage
Here are three real excerpts: the WCAG ratio and the contrast fixer, the scale builder that pins the brand color, and how to use the engine in your own project.
// js/color-core.js — WCAG 2.2 ratio + nearest passing color
/** WCAG 2.x contrast ratio between two hex colors, 1–21. */
function contrast(a, b) {
var la = luminance(a);
var lb = luminance(b);
var hi = Math.max(la, lb);
var lo = Math.min(la, lb);
return (hi + 0.05) / (lo + 0.05);
}
/**
* Smallest lightness change to `fg` (keeping hue and chroma) that reaches
* `target` contrast against `bg`. Tries the darker and lighter directions and
* returns the closer passing color, or null if neither can pass.
*/
function fixContrast(fg, bg, target) {
target = target || WCAG.AA;
if (contrast(fg, bg) >= target) return normalizeHex(fg);
var o = hexToOklch(fg);
if (!o) return null;
function search(toward) {
var end = toward === 'dark' ? 0 : 1;
if (contrast(oklchToHex(end, o.c, o.h), bg) < target) return null;
var lo = o.l;
var hi = end;
for (var i = 0; i < 30; i++) {
var mid = (lo + hi) / 2;
if (contrast(oklchToHex(mid, o.c, o.h), bg) >= target) hi = mid; else lo = mid;
}
var out = oklchToHex(hi, o.c, o.h);
return contrast(out, bg) >= target ? { hex: out, shift: Math.abs(hi - o.l) } : null;
}
var dark = search('dark');
var light = search('light');
if (dark && light) return dark.shift <= light.shift ? dark.hex : light.hex;
if (dark) return dark.hex;
if (light) return light.hex;
return null;
}
// js/color-core.js — 11-step OKLCH scale with the brand hex pinned
/**
* Build an 11-step tonal scale from one brand color.
* The step whose lightness is closest to the brand keeps the exact brand hex,
* so "primary-600" (or whichever) is precisely the color the client gave you.
*
* options.neutral → low-chroma grey tinted toward the brand hue
* options.anchor → false to skip pinning the exact brand color
* Returns [{ step, hex, anchor }].
*/
function buildScale(hex, options) {
options = options || {};
var base = hexToOklch(hex);
if (!base) throw new Error('Invalid color: ' + hex);
var peak = options.neutral ? Math.min(0.022, base.c * 0.18 + 0.006) : Math.max(base.c, 0.02);
var anchorIndex = -1;
if (!options.neutral && options.anchor !== false) {
var best = Infinity;
LIGHTNESS.forEach(function (L, i) {
var d = Math.abs(L - base.l);
if (d < best) { best = d; anchorIndex = i; }
});
}
return STEPS.map(function (step, i) {
if (i === anchorIndex) return { step: step, hex: normalizeHex(hex), anchor: true };
// Slight hue drift toward warmer darks / cooler lights keeps scales from looking flat.
var hue = options.neutral ? base.h : (base.h + (i - 5) * -0.6 + 360) % 360;
return { step: step, hex: oklchToHex(LIGHTNESS[i], peak * CHROMA_CURVE[i], hue), anchor: false };
});
}
// Browser: <script src="js/color-core.js"></script> → window.TintaCore
// Node: const T = require('./js/color-core.js');
const T = window.TintaCore;
const primary = T.buildScale('#0e7c66'); // [{ step: 50, hex: '#eff9f6' }, …]
const neutral = T.buildScale('#0e7c66', { neutral: true });
T.contrast('#ffffff', '#0e7c66'); // 5.13 → passes AA
T.fixContrast('#e0782b', '#ffffff', 4.5); // '#bc5b00' (4.53:1, same hue)
T.simulate('#e0782b', 'deuteranopia'); // how it looks without green cones
const css = T.exportPalettes(
[{ name: 'Primary', scale: primary }, { name: 'Neutral', scale: neutral }],
'css' // or 'tailwind' | 'scss' | 'json'
);
// :root { --primary-50: #eff9f6; … --neutral-950: #171d1b; }
Development Roadmap & Milestones
Version 1.0.0 is released. The next milestones come straight from the project’s roadmap:
v1.0.0: Core Studio (Released)
OKLCH scales with a pinned brand color, tinted neutrals, contrast checker with fixes, pair finder grid, color-vision simulation and four export formats.
Next: APCA Contrast
Show the APCA Lc value from the WCAG 3 draft next to the WCAG 2.2 ratio, so designers can compare both.
Planned: Share Links & Logo Import
Short links that reopen a palette, and picking brand colors straight from an uploaded logo.
Planned: theme.json & Semantic Tokens
Export a WordPress theme.json palette and Figma variables, with light and dark semantic tokens for surface, text, border and focus.
User Interface & Feature Showcase
Project Links, Code & Documentation
Open the Live Tool
Use the full studio in your browser: five brand colors, pair finder grid, color-vision preview and all four export formats.
GitHub Repository
Browse the source, run the tests, open an issue or suggest a feature. Free and open source under the MIT license.
Tutorial & Docs
A beginner-friendly walkthrough of luminance, OKLCH and every function, plus three extension exercises.
Readable Sites Start With the Palette
Accessibility problems are cheapest to fix before the first line of CSS. Tinta turns a logo color into a palette you can defend, with the numbers to prove every text color passes. Want a brand color audit for your website, or an accessible redesign for your school, cooperative or office?
The brand names and colors in the demo are fictional samples. Contrast ratios follow WCAG 2.2; the color-vision preview is a simulation and not a substitute for testing with real users.

Comments
Post a Comment