Accessible text color suggester
Fix text that fails contrast without losing its hue: gray #888888 on white reaches only 3.54 to 1, so it is darkened to #767676 for 4.54 to 1, just past the 4.5 target. Only lightness moves. Free in your browser.
- Free
- Runs in your browser
- No sign-up
- Your data stays on your device
Run — free
Worked example
- Background
- #ffffff
- Text
- #888888
- Target ratio
- 4.5
- Original ratio
- 3.54
- Suggested ratio
- 4.54
- Direction
- darken
- Lightness before
- 53.33
This accessible text color suggester takes a background hex color, a starting text hex color, and a target WCAG contrast ratio, then returns a suggested text color that meets that ratio by shifting only HSL lightness. Hue and saturation stay fixed, so a brand blue stays blue while it becomes dark enough or light enough to pass AA or AAA against the given surface. If the starting color already meets the target, it is returned unchanged and marked as not adjusted. Designers, design-token authors, and CI jobs use it when a contrast checker is not enough: they need a deterministic replacement hex, not only a pass or fail flag. There are no network calls, random seeds, or clock reads.
How a lightness search meets a WCAG contrast target
Contrast on the web is not a taste call. The Web Content Accessibility Guidelines define a single contrast ratio from relative luminance so that body copy, buttons, and labels stay readable for people with low vision. This suggester starts from your background hex and a starting text hex, then changes only the lightness of that text color in HSL until the WCAG 2.x ratio against the background reaches the target you asked for.
Hue and saturation stay put, so a brand navy remains navy and a brand red remains red; only how light or dark the ink is will move. The engine linearizes each sRGB channel with the official piecewise curve, builds luminance with the 0.2126, 0.7152, and 0.0722 weights, and computes the lighter luminance plus 0.05 divided by the darker luminance plus 0.05.
It then walks HSL lightness in deterministic tenths of a percent toward black and toward white, converts each step back to 8-bit RGB, and keeps the first quantized hex in each direction that meets the unrounded target. The candidate with the smaller lightness change wins; a tie prefers darkening, which is the usual fix for gray type on a light card. If the starting color already meets the target, it is returned unchanged so tokens are not rewritten for no reason.
There is no random jitter, no network lookup, and no date-dependent shortcut: the same three inputs always produce the same suggested hex and the same validation flags.
Where an accessible text color suggester earns its keep
Product teams meet this page when a brand gray looks elegant on a marketing slide but fails body text on a white card, when a dark-mode theme is generated from light tokens, or when legal stakeholders ask for a replacement hex rather than a screenshot opinion. Designers paste a surface and a starting ink from Figma or a token file and take the suggested hex back into the palette instead of nudging a lightness slider by eye.
QA and accessibility specialists attach the original ratio, the suggested ratio, and the suggested hex to tickets so developers fix the exact pair. Content and marketing teams verify that campaign colors still have a legal text companion when placed on light or dark site backgrounds. If you only need to measure a pair you already chose, use a contrast checker landing; this page owns the suggest-a-passing-text-color intent.
What you can do with it
Fix failing body-text tokens
Given a brand gray that misses AA on a white card, get the closest darker hex that passes 4.5:1 without changing hue.
Dark-mode and light-mode ink
From one starting text color, derive a passing companion for a light surface and a passing companion for a dark surface.
FAQ
How does the accessible text color suggester choose a hex?
It keeps the starting hue and saturation, walks HSL lightness toward black and toward white, and returns the closest quantized hex whose WCAG 2.x contrast meets the target. A tie prefers darkening.
Which target ratios can I request?
Any finite number from 1 to 21 inclusive. Defaults to 4.5 (normal-text AA). Use 3 for large-text AA and 7 for normal-text AAA.
What does the accessible text color suggester cost?
The browser tool is free and runs locally.
What if no shade of my color can reach the target?
You still get the better extreme, black or white, clearly marked as not meeting the target. That happens, for example, when asking for AAA 7 to 1 on a mid-gray card.
Will my brand color change its hue?
No. Hue and saturation stay fixed and only lightness changes, so a brand navy stays navy and a brand red stays red, just darker or lighter until the contrast target is met.
Related tools
For developers — API access
Everything on this page is available programmatically. This section is for teams who want to wire it into their own systems; everyone else can just use the tool above.
API endpoint
Prefer to automate it? One authenticated POST creates the task; the result comes back by webhook or a signed link. The same capability also runs here on the web, by email and from Telegram — and soon from our app too.
Call it from your stack
curl -X POST https://api.kit.forhosting.com/color/suggest-accessible-text \
-H "Authorization: Bearer $KIT_KEY" \
-H "Content-Type: application/json" \
-d '{"background":"#ffffff","text":"#888888"}'const res = await fetch("https://api.kit.forhosting.com/color/suggest-accessible-text", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KIT_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"background": "#ffffff",
"text": "#888888"
})
});
const { task_id } = await res.json();import os, requests
res = requests.post(
"https://api.kit.forhosting.com/color/suggest-accessible-text",
headers={"Authorization": f"Bearer {os.environ['KIT_KEY']}"},
json={
"background": "#ffffff",
"text": "#888888"
},
)
task_id = res.json()["task_id"]<?php
$res = file_get_contents("https://api.kit.forhosting.com/color/suggest-accessible-text", false, stream_context_create([
"http" => [
"method" => "POST",
"header" => "Authorization: Bearer " . getenv("KIT_KEY") . "\r\nContent-Type: application/json",
"content" => '{"background":"#ffffff","text":"#888888"}',
],
]));
$task = json_decode($res, true);body := bytes.NewBufferString(`{"background":"#ffffff","text":"#888888"}`)
req, _ := http.NewRequest("POST", "https://api.kit.forhosting.com/color/suggest-accessible-text", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("KIT_KEY"))
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)Example request
{
"background": "#ffffff",
"text": "#888888"
}Example response
{
"task_id": "tsk_a1b2c3d4e5f6a1b2c3d4e5f6",
"type": "color.suggest_accessible_text",
"status": "queued",
"_links": {
"result": "/tasks/tsk_…/result"
}
}The API is asynchronous: the call returns a task_id immediately and the result arrives by webhook. Polling is capped at 1 req/s per task.
Pricing
Published price — no tokens, no invented credits. A failed task is never charged.
Limits
max_color_chars | 32 |
Errors
| HTTP | Code | Meaning |
|---|---|---|
401 | unauthorized | Missing or invalid API key. |
402 | insufficient_balance | Your balance doesn't cover the task price. |
404 | unknown_type | That task type doesn't exist. |
429 | rate_limited | Too many requests. Use the webhook instead of polling. |
Technical notes
If no lightness on that hue can reach the target — for example AAA 7:1 on a mid-gray card — the engine still returns the better extreme, black or white, and sets meets_target to false so a pipeline can fail closed. Successful API calls are billed at $0.002 per item; invalid input is not charged.
What you send, what you get back, and how validation works
Send a background hex and a starting text hex. Common targets are 3 for large-text AA, 4.5 for normal AA, and 7 for normal AAA; any finite number from 1 to 21 inclusive is accepted because that is the WCAG range from identical colors to black on white. A short formula string documents the rule. An optional target_ratio defaults to 4.5, the normal-text AA threshold. Background also accepts the alias bg; text accepts foreground, fg, and color; target_ratio accepts ratio and target. Missing fields, non-hex characters, alpha forms, wrong lengths, NaN, Infinity, and a target outside the closed interval fail with invalid_input and are not billed. On success the response echoes normalized six-digit background, text, and suggested swatches with printable rgb strings. original_ratio and suggested_ratio are rounded to two decimals for display, while meets_target compares the unrounded suggested ratio to the target so a printed 4.50 cannot hide a true 4.499. adjusted tells you whether the hex actually moved; direction is none, darken, or lighten. lightness_before and lightness_after make the HSL step auditable. The wcag object reports normal_aa, normal_aaa, large_aa, and large_aaa for the suggested pair so a reviewer can see the full matrix, not only the one target they asked for. Pair the free browser form with the identical API at $0.002 per successful item when a design-token pipeline needs the same hex in continuous integration.
Where an accessible text color suggester earns its keep
Engineers wire the same contract into pull-request checks that rewrite or reject a token when normal-text AA would drop below 4.5, and they read meets_target so an impossible AAA request on mid-gray does not silently publish black as if it passed. Because this capability is a pure parse engine, API pricing is the published base of $0.002 per successful item and failed validation does not consume balance.
Integration use cases
CI gates that need a replacement hex
Call the API from pull requests to rewrite or reject tokens when the suggested pair still cannot meet the requested WCAG target.
Technical questions
What if no text color on this hue can meet the target?
The engine returns the better of black or white and sets meets_target to false. That happens for some mid-gray backgrounds when you ask for 7:1.
Which color formats are accepted?
Hex only: #RGB or #RRGGBB, with or without the leading hash, case-insensitive. rgb(), hsl(), named colors, and alpha hex are rejected with invalid_input.