Designs

Tinta Color Studio: Turn One Brand Color Into an Accessible Palette

Efraim Ray Efraim Ray 0 Comments
Developer Tool Showcase

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.

Type Developer Tool
Stack Static · No server
Frontend Vanilla JS + OKLCH
Version v1.0.0
License Free · MIT

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.

efraimray.github.io/tinta-color-studio
Scale
Contrast checker
—
Live preview
Dividends are ready for pickup. Bring your passbook and one valid ID to the main office. Check my share



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:

10
Total JavaScript
KB gzipped
4.1
Stylesheet
KB gzipped
10
Unit Tests
node: 10/10 pass
0
Dependencies
only Google Fonts loads

Technology Stack & Architecture Layers

Color Science

OKLab & OKLCH

A perceptual color space, so step 700 of a yellow and step 700 of a blue have similar lightness and similar contrast.

Accessibility

WCAG 2.2 Contrast

Relative luminance and ratio checks for AA, AAA, large text and UI components, plus Machado color-vision simulation.

Front End

Vanilla JavaScript

One state object, string renderers and event delegation. A pure UMD engine runs in the browser and in Node tests.

Output

CSS · Tailwind · Tokens

Exports CSS custom properties, a tailwind.config.js, SCSS maps or W3C Design Tokens JSON.

Key Architectural Decisions

Pillar 01

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.

Pillar 02

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.

Pillar 03

Smallest Possible Fix

For a failing pair, the engine searches toward darker and lighter and suggests whichever passing color changes the least.

Pillar 04

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:

1

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.

2

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.

3

Planned: Share Links & Logo Import

Short links that reopen a palette, and picking brand colors straight from an uploaded logo.

4

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

Primary
50100200300400500600•700800900950
Accent
50100200300400•500600700800900950
Neutral
50100200300400500600700800900950
Scales

One Color In, Three Scales Out

Primary, accent and a tinted neutral, each marked with the text color that passes AA.

ORANGE #E0782B ON WHITE
3.04:1Below AA
AA FAILLARGE PASSUI PASS
Change text to #bc5b00 · 4.53:1 Apply
Checker

Nearest Passing Color, Same Hue

The fix keeps the brand’s hue and changes lightness only as much as needed.

// tailwind.config.js — generated by Tinta
module.exports = {
  theme: { extend: { colors: {
    'primary': {
      500: '#379780',
      600: '#0e7c66',
      700: '#056451'
    }
  } } }
};
Export

Paste Straight Into Your Project

CSS variables, Tailwind, SCSS or W3C design tokens with one click.

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.

Launch Tinta

GitHub Repository

Browse the source, run the tests, open an issue or suggest a feature. Free and open source under the MIT license.

View on GitHub

Tutorial & Docs

A beginner-friendly walkthrough of luminance, OKLCH and every function, plus three extension exercises.

Read the Tutorial
Engineering Takeaway

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.

Efraim Ray
Written by

Efraim Ray

Software Developer and AI Specialist. BS Information Technology graduate. I pride myself on being conscientious, attentive to detail, and always open to learning — because the best solutions come from listening, taking notes, and exploring what's next.

Comments