{"id":15728,"date":"2026-09-06T11:15:28","date_gmt":"2026-09-06T09:15:28","guid":{"rendered":"https:\/\/www.lukaswojcik.com\/blog\/toolbox\/colour-contrast-palette-auditor\/"},"modified":"2026-09-06T11:20:14","modified_gmt":"2026-09-06T09:20:14","slug":"colour-contrast-palette-auditor","status":"publish","type":"page","link":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/colour-contrast-palette-auditor\/","title":{"rendered":"Colour Contrast and Palette Auditor: two themes, one set of names"},"content":{"rendered":"<div class=\"gtm-analyser-container\" style=\"background: var(--bg-panel, #1e1e24); padding: 25px; border-radius: 8px; border: 1px solid var(--border, #2a2a35);\">\n<p style=\"color: var(--text-secondary, #a0a0b0); margin-bottom: 20px;\">A palette kept in two themes has two ways to go wrong. One is a pair whose contrast is too low. The other is quieter: a variable declared in one theme and forgotten in the other, so that in that theme the colour falls back to whatever the default is. Both are visible once the two blocks stand side by side.<\/p>\n<div style=\"display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 14px; margin-bottom: 14px;\">\n<div>\n            <label for=\"ko-hell\" style=\"color: var(--text-secondary, #a0a0b0); display: block; font-size: 0.85rem; margin-bottom: 5px;\">Light theme, CSS custom properties<\/label><br \/>\n            <textarea id=\"ko-hell\" rows=\"10\" class=\"form-control\" style=\"width: 100%; padding: 10px; background: var(--bg-body, #14141a); border: 1px solid var(--border, #2a2a35); color: var(--text-primary, #e8e8ee); border-radius: 6px; box-sizing: border-box; font-family: monospace; font-size: 0.82rem;\">:root {<br \/>\n  &#8211;bg-body: #f4f6f9;<br \/>\n  &#8211;bg-panel: #ffffff;<br \/>\n  &#8211;text-primary: #1a1f2b;<br \/>\n  &#8211;text-secondary: #5b6472;<br \/>\n  &#8211;border: #d7dde5;<br \/>\n  &#8211;accent: #0369a1;<br \/>\n  &#8211;on-accent: #ffffff;<br \/>\n}<\/textarea>\n        <\/div>\n<div>\n            <label for=\"ko-dunkel\" style=\"color: var(--text-secondary, #a0a0b0); display: block; font-size: 0.85rem; margin-bottom: 5px;\">Dark theme, CSS custom properties<\/label><br \/>\n            <textarea id=\"ko-dunkel\" rows=\"10\" class=\"form-control\" style=\"width: 100%; padding: 10px; background: var(--bg-body, #14141a); border: 1px solid var(--border, #2a2a35); color: var(--text-primary, #e8e8ee); border-radius: 6px; box-sizing: border-box; font-family: monospace; font-size: 0.82rem;\">:root {<br \/>\n  &#8211;bg-body: #14141a;<br \/>\n  &#8211;bg-panel: #1e1e24;<br \/>\n  &#8211;text-primary: #e8e8ee;<br \/>\n  &#8211;text-secondary: #a0a0b0;<br \/>\n  &#8211;border: #2a2a35;<br \/>\n  &#8211;accent: #7ee787;<br \/>\n  &#8211;on-accent: #0b1114;<br \/>\n}<\/textarea>\n        <\/div>\n<\/p><\/div>\n<p style=\"color: var(--text-secondary, #a0a0b0); font-size: 0.82rem; margin-bottom: 16px;\">Any CSS block works \u2014 the declarations are read out of it. Hex, rgb() and hsl() are understood; a colour with alpha is measured on its own value and marked, because the result depends on what lies beneath it.<\/p>\n<p>    <button id=\"ko-btn\" class=\"button\" style=\"background: var(--accent, #7ee787); color: var(--on-accent, #0b1114); border: none; padding: 12px 24px; border-radius: 6px; font-weight: 700; cursor: pointer;\">Measure contrast<\/button><\/p>\n<div id=\"ko-ausgabe\" style=\"margin-top: 22px;\"><\/div>\n<p style=\"color: var(--text-secondary, #a0a0b0); font-size: 0.8rem; margin: 24px 0 0;\">Everything is computed in the browser. No value leaves this page.<\/p>\n<\/div>\n<p><script>\n(function () {\n    'use strict';<\/p>\n<p>    const T = {\"einleitung\":\"A palette kept in two themes has two ways to go wrong. One is a pair whose contrast is too low. The other is quieter: a variable declared in one theme and forgotten in the other, so that in that theme the colour falls back to whatever the default is. Both are visible once the two blocks stand side by side.\",\"label_hell\":\"Light theme, CSS custom properties\",\"label_dunkel\":\"Dark theme, CSS custom properties\",\"hinweis\":\"Any CSS block works \u2014 the declarations are read out of it. Hex, rgb() and hsl() are understood; a colour with alpha is measured on its own value and marked, because the result depends on what lies beneath it.\",\"knopf\":\"Measure contrast\",\"grenze\":\"Everything is computed in the browser. No value leaves this page.\",\"urteil_gut\":\"Nothing stands in the way\",\"urteil_warnung\":\"Worth a look\",\"urteil_kritisch\":\"Needs attention\",\"grenzen_erklaerung\":\"WCAG 2.2 asks 4.5:1 for normal text, 3:1 for large text from 24 px or bold from 19 px, and 3:1 for borders and controls that carry meaning. AAA raises normal text to 7:1.\",\"h_thema_hell\":\"Light theme\",\"h_thema_dunkel\":\"Dark theme\",\"thema_kopf\":\"{farben} colours, {paare} pairs, weakest first.\",\"spalte_vorne\":\"Foreground\",\"spalte_hinten\":\"Background\",\"spalte_wert\":\"Ratio\",\"spalte_normal\":\"Normal text\",\"spalte_gross\":\"Large text\",\"spalte_hell\":\"Light\",\"spalte_dunkel\":\"Dark\",\"stufe_aa\":\"AA\",\"stufe_aaa\":\"AAA\",\"stufe_durchgefallen\":\"fails\",\"nur_erste\":\"The first {gezeigt} of {gesamt} pairs are listed.\",\"h_abweichung\":\"Pairs the two themes disagree on\",\"h_fehlend\":\"Names present in only one theme\",\"fehlt_in_hell\":\"missing in the light theme\",\"fehlt_in_dunkel\":\"missing in the dark theme\",\"h_befunde\":\"Findings\",\"keine_befunde\":\"No findings.\",\"stufe_hoch\":\"High\",\"stufe_mittel\":\"Medium\",\"stufe_niedrig\":\"Low\",\"stufe_info\":\"Note\",\"stufe_gut\":\"Good\",\"befund_keine_farben\":\"No colour declarations found || Neither block contained a custom property with a colour value.\",\"befund_namen_ungleich\":\"{anzahl} names exist in only one theme || For instance {namen}. In the theme where the declaration is missing, the colour falls back to whatever default the rule carries \u2014 and that default was chosen for the other theme.\",\"befund_unter_drei_hell\":\"{anzahl} pairs below 3:1 in the light theme || The weakest is {schlechtestes} at {wert}:1. Below 3:1 not even a border or a control is distinguishable.\",\"befund_unter_drei_dunkel\":\"{anzahl} pairs below 3:1 in the dark theme || The weakest is {schlechtestes} at {wert}:1. Below 3:1 not even a border or a control is distinguishable.\",\"befund_unter_aa_hell\":\"{anzahl} pairs below 4.5:1 in the light theme || The weakest is {schlechtestes} at {wert}:1. Enough for large text, not enough for body copy.\",\"befund_unter_aa_dunkel\":\"{anzahl} pairs below 4.5:1 in the dark theme || The weakest is {schlechtestes} at {wert}:1. Enough for large text, not enough for body copy.\",\"befund_alles_besteht_hell\":\"All {anzahl} pairs of the light theme reach 4.5:1\",\"befund_alles_besteht_dunkel\":\"All {anzahl} pairs of the dark theme reach 4.5:1\",\"befund_rolle_unklar\":\"{anzahl} colours whose role is a guess || For instance {namen}. The role is read from the name, and a colour does not know what it is used for \u2014 pairs built on a guess should be read as a hint, not a verdict.\",\"befund_mit_alpha\":\"{anzahl} colours carry an alpha value || The ratio is computed on the colour itself. What it actually looks like depends on the layer beneath it, which a palette alone does not say.\",\"befund_themen_uneinig\":\"{anzahl} pairs pass in one theme and fail in the other || For instance {beispiel}. This is the failure that only shows up when both palettes are read together.\",\"dekor_hinweis\":\"Pairs marked with * are decoration \u2014 soft lines, veils, shadows and on-colours outside their own base. They are measured and listed, but no lower bound is applied to them, so they stay out of the findings.\",\"befund_rand_schwach_hell\":\"{anzahl} border and control pairs below 3:1 in the light theme || The weakest is {schlechtestes} at {wert}:1. WCAG 1.4.11 asks 3:1 only for outlines that are needed to recognise a component \u2014 a card edge or a photo frame is not automatically one of those, so this is a judgement call rather than a defect.\",\"befund_rand_schwach_dunkel\":\"{anzahl} border and control pairs below 3:1 in the dark theme || The weakest is {schlechtestes} at {wert}:1. WCAG 1.4.11 asks 3:1 only for outlines that are needed to recognise a component \u2014 a card edge or a photo frame is not automatically one of those, so this is a judgement call rather than a defect.\"};<\/p>\n<p>    var STUFEN = ['hoch', 'mittel', 'niedrig', 'info', 'gut'];\n    var GRENZEN = { normal_aa: 4.5, normal_aaa: 7, gross_aa: 3, gross_aaa: 4.5, ui: 3 };<\/p>\n<p>    function text(schluessel, daten) {\n        var t = T[schluessel];\n        if (typeof t !== 'string') { return null; }\n        return t.replace(\/\\{([a-z_0-9]+)\\}\/g, function (m, k) {\n            return (daten && daten[k] !== undefined && daten[k] !== null) ? String(daten[k]) : m;\n        });\n    }<\/p>\n<p>    \/\/ --- Farben lesen -----------------------------------------------------\n    function hslZuRgb(h, s, l) {\n        h = ((h % 360) + 360) % 360 \/ 360; s = s \/ 100; l = l \/ 100;\n        if (s === 0) { var g = Math.round(l * 255); return [g, g, g]; }\n        function hue(p, q, t) {\n            if (t < 0) { t += 1; } if (t > 1) { t -= 1; }\n            if (t < 1 \/ 6) { return p + (q - p) * 6 * t; }\n            if (t < 1 \/ 2) { return q; }\n            if (t < 2 \/ 3) { return p + (q - p) * (2 \/ 3 - t) * 6; }\n            return p;\n        }\n        var q = l < 0.5 ? l * (1 + s) : l + s - l * s;\n        var p = 2 * l - q;\n        return [Math.round(hue(p, q, h + 1 \/ 3) * 255), Math.round(hue(p, q, h) * 255), Math.round(hue(p, q, h - 1 \/ 3) * 255)];\n    }\n\n    \/** Gibt [r,g,b] oder null. Alpha wird gemeldet, aber nicht verrechnet. *\/\n    function farbeLesen(roh) {\n        var s = String(roh || '').trim().toLowerCase();\n        if (s === '') { return null; }\n        var m;\n        if ((m = s.match(\/^#([0-9a-f]{3,8})$\/))) {\n            var h = m[1];\n            if (h.length === 3 || h.length === 4) {\n                return { rgb: [parseInt(h[0] + h[0], 16), parseInt(h[1] + h[1], 16), parseInt(h[2] + h[2], 16)],\n                    alpha: h.length === 4 ? parseInt(h[3] + h[3], 16) \/ 255 : 1 };\n            }\n            if (h.length === 6 || h.length === 8) {\n                return { rgb: [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)],\n                    alpha: h.length === 8 ? parseInt(h.slice(6, 8), 16) \/ 255 : 1 };\n            }\n            return null;\n        }\n        if ((m = s.match(\/^rgba?\\(([^)]+)\\)$\/))) {\n            var t = m[1].split(\/[\\s,\\\/]+\/).filter(function (x) { return x !== ''; });\n            if (t.length < 3) { return null; }\n            function kanal(w) { return w.indexOf('%') !== -1 ? Math.round(parseFloat(w) * 2.55) : parseInt(w, 10); }\n            var a = t.length > 3 ? (t[3].indexOf('%') !== -1 ? parseFloat(t[3]) \/ 100 : parseFloat(t[3])) : 1;\n            var rgb = [kanal(t[0]), kanal(t[1]), kanal(t[2])];\n            if (rgb.some(function (x) { return isNaN(x); })) { return null; }\n            return { rgb: rgb, alpha: isNaN(a) ? 1 : a };\n        }\n        if ((m = s.match(\/^hsla?\\(([^)]+)\\)$\/))) {\n            var u = m[1].split(\/[\\s,\\\/]+\/).filter(function (x) { return x !== ''; });\n            if (u.length < 3) { return null; }\n            var hh = parseFloat(u[0]), ss = parseFloat(u[1]), ll = parseFloat(u[2]);\n            if (isNaN(hh) || isNaN(ss) || isNaN(ll)) { return null; }\n            var aa = u.length > 3 ? (u[3].indexOf('%') !== -1 ? parseFloat(u[3]) \/ 100 : parseFloat(u[3])) : 1;\n            return { rgb: hslZuRgb(hh, ss, ll), alpha: isNaN(aa) ? 1 : aa };\n        }\n        var NAMEN = { white: [255, 255, 255], black: [0, 0, 0], red: [255, 0, 0], transparent: null };\n        if (Object.prototype.hasOwnProperty.call(NAMEN, s)) {\n            return NAMEN[s] ? { rgb: NAMEN[s], alpha: 1 } : null;\n        }\n        return null;\n    }<\/p>\n<p>    function leuchtdichte(rgb) {\n        var k = rgb.map(function (w) {\n            var c = Math.max(0, Math.min(255, w)) \/ 255;\n            return c <= 0.03928 ? c \/ 12.92 : Math.pow((c + 0.055) \/ 1.055, 2.4);\n        });\n        return 0.2126 * k[0] + 0.7152 * k[1] + 0.0722 * k[2];\n    }\n\n    function verhaeltnis(a, b) {\n        var la = leuchtdichte(a), lb = leuchtdichte(b);\n        var hell = Math.max(la, lb), dunkel = Math.min(la, lb);\n        return (hell + 0.05) \/ (dunkel + 0.05);\n    }\n\n    function runden(w) { return Math.round(w * 100) \/ 100; }\n\n    \/\/ --- Variablen lesen --------------------------------------------------\n    \/** Aus einem CSS-Block die eigenen Eigenschaften mit Farbwert holen. *\/\n    function variablenLesen(css) {\n        var raus = [];\n        var rx = \/(--[A-Za-z0-9_\\-]+)\\s*:\\s*([^;{}]+)\/g;\n        var m;\n        while ((m = rx.exec(String(css || ''))) !== null) {\n            var name = m[1];\n            var wert = m[2].trim().replace(\/\\s*!important\\s*$\/, '');\n            var f = farbeLesen(wert);\n            if (f === null) { continue; }\n            if (raus.some(function (x) { return x.name === name; })) { continue; }\n            raus.push({ name: name, wert: wert, rgb: f.rgb, alpha: f.alpha, rolle: rolleRaten(name) });\n        }\n        return raus;\n    }\n\n    \/**\n     * Rolle aus dem Namen raten. Das ist eine Vermutung und wird als solche\n     * ausgewiesen - eine Farbe weiss nicht, wofuer sie verwendet wird.\n     *\n     * Zwei Rollen halten Scheinbefunde heraus:\n     *   auf    - eine --on-*-Farbe gehoert auf ihre eine Grundfarbe und auf\n     *            keine andere Flaeche. Gegen alle Hintergruende gemessen\n     *            liefert sie Werte um 1:1, die nie jemand zu sehen bekommt.\n     *   dekor  - weiche Linien, Schleier und Schatten sind absichtlich kaum\n     *            sichtbar. Sie werden gemessen und gezeigt, zaehlen aber\n     *            nicht in die Befunde, weil eine Untergrenze auf sie nicht\n     *            anzuwenden ist.\n     *\/\n    function rolleRaten(name) {\n        var n = name.toLowerCase();\n        if (\/^--on-\/.test(n)) { return 'auf'; }\n        if (\/(soft|hairline|tint|wash|edge|glow|shadow|overlay|chip|scrim|veil)\/.test(n)) { return 'dekor'; }\n        if (\/(bg|background|panel|surface|body|paper|fond)\/.test(n)) { return 'hintergrund'; }\n        if (\/(text|fg|foreground|ink|label|heading)\/.test(n)) { return 'vordergrund'; }\n        if (\/(border|line|divider|outline|rule)\/.test(n)) { return 'element'; }\n        if (\/(accent|brand|primary|link|action|button|cta)\/.test(n)) { return 'element'; }\n        return 'unbekannt';\n    }\n\n    \/** Zu welcher Grundfarbe gehoert eine --on-*-Farbe? *\/\n    function grundfarbeZu(name) { return name.replace(\/^--on-\/, '--'); }\n\n    \/** Alle sinnvollen Paare eines Themas bilden und bewerten. *\/\n    function paareBilden(vars) {\n        function eintragen(paare, v, h, zaehlt) {\n            if (v.name === h.name) { return; }\n            var w = verhaeltnis(v.rgb, h.rgb);\n            paare.push({\n                vorne: v.name, hinten: h.name, wert: runden(w),\n                rolle: v.rolle, zaehlt: zaehlt,\n                normalAA: w >= GRENZEN.normal_aa, normalAAA: w >= GRENZEN.normal_aaa,\n                grossAA: w >= GRENZEN.gross_aa, ui: w >= GRENZEN.ui,\n                alpha: v.alpha < 1 || h.alpha < 1\n            });\n        }\n\n        var hg = vars.filter(function (v) { return v.rolle === 'hintergrund'; });\n        var vg = vars.filter(function (v) { return v.rolle === 'vordergrund' || v.rolle === 'element' || v.rolle === 'unbekannt'; });\n        var dekor = vars.filter(function (v) { return v.rolle === 'dekor'; });\n        var auf = vars.filter(function (v) { return v.rolle === 'auf'; });\n        \/\/ Ohne erkannte Hintergruende hilft nur: jede gegen jede.\n        if (!hg.length) { hg = vars.slice(); vg = vars.slice(); dekor = []; auf = []; }\n\n        var paare = [];\n        hg.forEach(function (h) {\n            vg.forEach(function (v) { eintragen(paare, v, h, true); });\n            dekor.forEach(function (v) { eintragen(paare, v, h, false); });\n        });\n        \/\/ Eine --on-*-Farbe nur gegen ihre eigene Grundfarbe.\n        auf.forEach(function (v) {\n            var grund = grundfarbeZu(v.name);\n            vars.forEach(function (h) { if (h.name === grund) { eintragen(paare, v, h, true); } });\n        });\n\n        paare.sort(function (a, b) { return a.wert - b.wert; });\n        return paare;\n    }\n\n    function befund(liste, key, stufe, daten) { liste.push({ key: key, stufe: stufe, daten: daten || {} }); }\n\n    \/** Die ganze Auswertung, beide Themen zusammen. *\/\n    function bewerten(cssHell, cssDunkel) {\n        var b = [];\n        var hell = variablenLesen(cssHell);\n        var dunkel = variablenLesen(cssDunkel);\n\n        if (!hell.length &#038;&#038; !dunkel.length) {\n            befund(b, 'keine_farben', 'hoch', {});\n            return { fehler: null, hell: { vars: [], paare: [] }, dunkel: { vars: [], paare: [] },\n                fehlend: [], befunde: b, stufengruppen: gruppieren(b), urteil: 'kritisch' };\n        }\n\n        var themen = {\n            hell: { vars: hell, paare: hell.length ? paareBilden(hell) : [] },\n            dunkel: { vars: dunkel, paare: dunkel.length ? paareBilden(dunkel) : [] }\n        };\n\n        \/\/ Beide Themen muessen dieselben Namen fuehren, sonst faellt im einen\n        \/\/ Thema eine Farbe auf die Vorgabe zurueck.\n        var namenHell = hell.map(function (v) { return v.name; });\n        var namenDunkel = dunkel.map(function (v) { return v.name; });\n        var fehlend = [];\n        if (hell.length &#038;&#038; dunkel.length) {\n            namenHell.forEach(function (n) { if (namenDunkel.indexOf(n) === -1) { fehlend.push({ name: n, fehlt_in: 'dunkel' }); } });\n            namenDunkel.forEach(function (n) { if (namenHell.indexOf(n) === -1) { fehlend.push({ name: n, fehlt_in: 'hell' }); } });\n            if (fehlend.length) {\n                befund(b, 'namen_ungleich', 'mittel', { anzahl: fehlend.length,\n                    namen: fehlend.slice(0, 5).map(function (f) { return f.name; }).join(', ') });\n            }\n        }\n\n        ['hell', 'dunkel'].forEach(function (thema) {\n            var t = themen[thema];\n            if (!t.vars.length) { return; }\n            \/\/ Nur zaehlende Paare gehen in die Befunde. Dekor bleibt sichtbar,\n            \/\/ aber ohne Untergrenze.\n            var zaehlend = t.paare.filter(function (p) { return p.zaehlt !== false; });\n            \/\/ Text unter 3:1 ist unstrittig kaputt. Ein Rand unter 3:1 ist es\n            \/\/ nicht: WCAG 1.4.11 verlangt die Grenze nur fuer Umrisse, die\n            \/\/ ein Bedienelement ueberhaupt erst erkennbar machen - eine\n            \/\/ Kartenkante gehoert nicht zwangslaeufig dazu. Deshalb zwei\n            \/\/ getrennte Befunde statt eines strengen.\n            var textPaare = zaehlend.filter(function (p) { return p.rolle === 'vordergrund' || p.rolle === 'auf'; });\n            var randPaare = zaehlend.filter(function (p) { return p.rolle === 'element' || p.rolle === 'unbekannt'; });\n            var unterUi = textPaare.filter(function (p) { return !p.ui; });\n            var randSchwach = randPaare.filter(function (p) { return !p.ui; });\n            var unterAA = textPaare.filter(function (p) { return p.ui &#038;&#038; !p.normalAA; });\n            var unbekannt = t.vars.filter(function (v) { return v.rolle === 'unbekannt'; });\n            var durchsichtig = t.vars.filter(function (v) { return v.alpha < 1; });\n\n            if (unterUi.length) {\n                befund(b, 'unter_drei_' + thema, 'hoch', { anzahl: unterUi.length,\n                    schlechtestes: unterUi[0].vorne + ' auf ' + unterUi[0].hinten, wert: unterUi[0].wert });\n            }\n            if (randSchwach.length) {\n                befund(b, 'rand_schwach_' + thema, 'mittel', { anzahl: randSchwach.length,\n                    schlechtestes: randSchwach[0].vorne + ' auf ' + randSchwach[0].hinten, wert: randSchwach[0].wert });\n            }\n            if (unterAA.length) {\n                befund(b, 'unter_aa_' + thema, 'mittel', { anzahl: unterAA.length,\n                    schlechtestes: unterAA[0].vorne + ' auf ' + unterAA[0].hinten, wert: unterAA[0].wert });\n            }\n            if (!unterUi.length &#038;&#038; !unterAA.length &#038;&#038; zaehlend.length) {\n                befund(b, 'alles_besteht_' + thema, 'gut', { anzahl: zaehlend.length });\n            }\n            if (unbekannt.length) {\n                befund(b, 'rolle_unklar', 'info', { anzahl: unbekannt.length,\n                    namen: unbekannt.slice(0, 4).map(function (v) { return v.name; }).join(', ') });\n            }\n            if (durchsichtig.length) {\n                befund(b, 'mit_alpha', 'niedrig', { anzahl: durchsichtig.length });\n            }\n        });\n\n        \/\/ Der Vergleich, um den es bei zwei Themen geht: ein Paar, das im\n        \/\/ einen Thema besteht und im anderen nicht.\n        var abweichung = [];\n        if (themen.hell.paare.length &#038;&#038; themen.dunkel.paare.length) {\n            var karte = {};\n            themen.dunkel.paare.forEach(function (p) { karte[p.vorne + '|' + p.hinten] = p; });\n            themen.hell.paare.forEach(function (p) {\n                if (p.zaehlt === false) { return; }\n                var g = karte[p.vorne + '|' + p.hinten];\n                if (!g) { return; }\n                if (p.normalAA !== g.normalAA) {\n                    abweichung.push({ vorne: p.vorne, hinten: p.hinten, hell: p.wert, dunkel: g.wert,\n                        besteht_hell: p.normalAA });\n                }\n            });\n            if (abweichung.length) {\n                befund(b, 'themen_uneinig', 'mittel', { anzahl: abweichung.length,\n                    beispiel: abweichung[0].vorne + ' auf ' + abweichung[0].hinten });\n            }\n        }\n\n        var gruppen = gruppieren(b);\n        var urteil = gruppen.hoch.length ? 'kritisch' : (gruppen.mittel.length ? 'warnung' : 'gut');\n        return { fehler: null, hell: themen.hell, dunkel: themen.dunkel, fehlend: fehlend,\n            abweichung: abweichung, befunde: b, stufengruppen: gruppen, urteil: urteil };\n    }\n\n    function gruppieren(b) {\n        var g = {};\n        STUFEN.forEach(function (s) { g[s] = b.filter(function (f) { return f.stufe === s; }); });\n        return g;\n    }\n\n    window.LW_TEST = window.LW_TEST || {};\n    window.LW_TEST.KO = { farbeLesen: farbeLesen, leuchtdichte: leuchtdichte, verhaeltnis: verhaeltnis,\n        variablenLesen: variablenLesen, rolleRaten: rolleRaten, paareBilden: paareBilden,\n        bewerten: bewerten, darstellen: null, text: text, STUFEN: STUFEN, GRENZEN: GRENZEN, T: T };\n\n    var btn = document.getElementById('ko-btn');\n    var out = document.getElementById('ko-ausgabe');\n    if (!btn || !out) { return; }\n\n    function el(tag, stil, txt) { var e = document.createElement(tag); if (stil) { e.setAttribute('style', stil); } if (txt !== undefined &#038;&#038; txt !== null) { e.textContent = txt; } return e; }\n    function wert(id) { var e = document.getElementById(id); return e ? e.value : ''; }\n    var UEBERSCHRIFT = 'font-family: \"Nunito Sans\", sans-serif; font-weight: 700; color: var(--text-primary, #e8e8ee); font-size: 0.95rem; margin: 22px 0 8px;';\n    var ZELLE = 'padding: 5px 12px 5px 0; color: var(--text-secondary, #a0a0b0); font-size: 0.87rem;';\n    var ZELLE_WERT = 'padding: 5px 12px 5px 0; color: var(--text-primary, #e8e8ee); font-size: 0.87rem; font-family: monospace; word-break: break-all;';\n    var KASTEN = 'background: var(--bg-body, #14141a); border: 1px solid var(--border, #2a2a35); border-radius: 8px; padding: 14px 16px; margin: 0 0 10px;';\n    var GUT = ' color: #7ee787;', WARN = ' color: #ffa94d;', ROT = ' color: #ff7b72;', GRAU = ' color: #8a8a99;';\n    var FARBEN = { hoch: '#ff7b72', mittel: '#ffa94d', niedrig: '#e3b341', info: '#8a8a99', gut: '#7ee787' };\n    var URTEIL = { gut: GUT, warnung: WARN, kritisch: ROT };\n    function gitter(sp) { return el('div', 'display: grid; grid-template-columns: ' + sp + '; gap: 0 18px; align-items: baseline;'); }\n\n    function themaZeigen(ziel, name, t) {\n        if (!t.vars.length) { return; }\n        ziel.appendChild(el('div', UEBERSCHRIFT, T['h_thema_' + name]));\n        ziel.appendChild(el('p', ZELLE + ' margin: 0 0 8px;', text('thema_kopf', { farben: t.vars.length, paare: t.paare.length })));\n        var huelle = el('div', 'overflow-x: auto;');\n        var g = gitter('auto auto auto auto auto');\n        [T.spalte_vorne, T.spalte_hinten, T.spalte_wert, T.spalte_normal, T.spalte_gross].forEach(function (s) {\n            g.appendChild(el('div', ZELLE + ' font-weight: 700; color: var(--text-primary, #e8e8ee); white-space: nowrap;', s));\n        });\n        t.paare.slice(0, 40).forEach(function (p) {\n            var stil = p.normalAA ? GUT : (p.ui ? WARN : ROT);\n            g.appendChild(el('div', ZELLE_WERT + (p.zaehlt === false ? GRAU : '') + ' white-space: nowrap;', p.vorne + (p.zaehlt === false ? ' *' : '')));\n            g.appendChild(el('div', ZELLE_WERT + GRAU + ' white-space: nowrap;', p.hinten));\n            g.appendChild(el('div', ZELLE_WERT + stil, p.wert.toFixed(2) + ':1'));\n            g.appendChild(el('div', ZELLE_WERT + (p.normalAA ? GUT : ROT), p.normalAAA ? T.stufe_aaa : (p.normalAA ? T.stufe_aa : T.stufe_durchgefallen)));\n            g.appendChild(el('div', ZELLE_WERT + (p.grossAA ? GUT : ROT), p.grossAA ? T.stufe_aa : T.stufe_durchgefallen));\n        });\n        huelle.appendChild(g);\n        ziel.appendChild(huelle);\n        if (t.paare.some(function (p) { return p.zaehlt === false; })) {\n            ziel.appendChild(el('p', ZELLE + GRAU + ' margin: 4px 0 0;', T.dekor_hinweis));\n        }\n        if (t.paare.length > 40) {\n            ziel.appendChild(el('p', ZELLE + GRAU + ' margin: 4px 0 0;', text('nur_erste', { gezeigt: 40, gesamt: t.paare.length })));\n        }\n    }<\/p>\n<p>    function darstellen(b, ziel) {\n        ziel.innerHTML = '';\n        ziel.appendChild(el('div', 'font-size: 1.05rem; font-weight: 700; margin: 0 0 6px;' + URTEIL[b.urteil], T['urteil_' + b.urteil]));\n        ziel.appendChild(el('p', ZELLE + ' margin: 0 0 4px;', T.grenzen_erklaerung));<\/p>\n<p>        themaZeigen(ziel, 'hell', b.hell);\n        themaZeigen(ziel, 'dunkel', b.dunkel);<\/p>\n<p>        if (b.abweichung && b.abweichung.length) {\n            ziel.appendChild(el('div', UEBERSCHRIFT, T.h_abweichung));\n            var g2 = gitter('auto auto auto auto');\n            [T.spalte_vorne, T.spalte_hinten, T.spalte_hell, T.spalte_dunkel].forEach(function (s) {\n                g2.appendChild(el('div', ZELLE + ' font-weight: 700; color: var(--text-primary, #e8e8ee); white-space: nowrap;', s));\n            });\n            b.abweichung.slice(0, 20).forEach(function (a) {\n                g2.appendChild(el('div', ZELLE_WERT + ' white-space: nowrap;', a.vorne));\n                g2.appendChild(el('div', ZELLE_WERT + GRAU + ' white-space: nowrap;', a.hinten));\n                g2.appendChild(el('div', ZELLE_WERT + (a.besteht_hell ? GUT : ROT), a.hell.toFixed(2) + ':1'));\n                g2.appendChild(el('div', ZELLE_WERT + (a.besteht_hell ? ROT : GUT), a.dunkel.toFixed(2) + ':1'));\n            });\n            ziel.appendChild(g2);\n        }<\/p>\n<p>        if (b.fehlend && b.fehlend.length) {\n            ziel.appendChild(el('div', UEBERSCHRIFT, T.h_fehlend));\n            var g3 = gitter('auto auto');\n            b.fehlend.slice(0, 20).forEach(function (f) {\n                g3.appendChild(el('div', ZELLE_WERT, f.name));\n                g3.appendChild(el('div', ZELLE_WERT + WARN, f.fehlt_in === 'hell' ? T.fehlt_in_hell : T.fehlt_in_dunkel));\n            });\n            ziel.appendChild(g3);\n        }<\/p>\n<p>        ziel.appendChild(el('div', UEBERSCHRIFT, T.h_befunde));\n        var irgendwas = false;\n        STUFEN.forEach(function (s) {\n            var gr = b.stufengruppen[s];\n            if (!gr.length) { return; }\n            irgendwas = true;\n            ziel.appendChild(el('div', 'font-weight: 700; font-size: 0.82rem; text-transform: uppercase; letter-spacing: 0.5px; margin: 14px 0 6px; color: ' + FARBEN[s] + ';', T['stufe_' + s] + ' (' + gr.length + ')'));\n            gr.forEach(function (f) {\n                var k = el('div', KASTEN + ' border-left: 3px solid ' + FARBEN[s] + ';');\n                var t = text('befund_' + f.key, f.daten) || f.key;\n                var teile = t.split(' || ');\n                k.appendChild(el('div', 'color: var(--text-primary, #e8e8ee); font-size: 0.9rem; font-weight: 700;', teile[0]));\n                if (teile[1]) { k.appendChild(el('div', ZELLE + ' padding: 6px 0 0; line-height: 1.55;', teile[1])); }\n                ziel.appendChild(k);\n            });\n        });\n        if (!irgendwas) { ziel.appendChild(el('p', ZELLE + ' margin: 0;', T.keine_befunde)); }\n    }\n    window.LW_TEST.KO.darstellen = darstellen;<\/p>\n<p>    btn.addEventListener('click', function () {\n        darstellen(bewerten(wert('ko-hell'), wert('ko-dunkel')), out);\n    });\n})();\n<\/script><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Takes the CSS custom properties of a light and a dark palette, forms every foreground-on-background pair and measures its contrast ratio against WCAG 2.2. The point of holding both at once: a pair that passes in one theme and fails in the other, and a variable that exists in one palette but not the other.<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":38,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"template-tool-base.php","meta":{"footnotes":""},"tags":[92757],"class_list":["post-15728","page","type-page","status-publish","hentry","tag-web-development"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.1 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Colour Contrast and Palette Auditor: two themes, one set of names - Lukas Wojcik - Blog<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/colour-contrast-palette-auditor\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Colour Contrast and Palette Auditor: two themes, one set of names - Lukas Wojcik - Blog\" \/>\n<meta property=\"og:description\" content=\"Takes the CSS custom properties of a light and a dark palette, forms every foreground-on-background pair and measures its contrast ratio against WCAG 2.2. The point of holding both at once: a pair that passes in one theme and fails in the other, and a variable that exists in one palette but not the other.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/colour-contrast-palette-auditor\/\" \/>\n<meta property=\"og:site_name\" content=\"Lukas Wojcik - Blog\" \/>\n<meta property=\"article:modified_time\" content=\"2026-09-06T09:20:14+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/08\/og-default.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"630\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data1\" content=\"1 minute\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/colour-contrast-palette-auditor\\\/\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/colour-contrast-palette-auditor\\\/\",\"name\":\"Colour Contrast and Palette Auditor: two themes, one set of names - Lukas Wojcik - Blog\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#website\"},\"datePublished\":\"2026-09-06T09:15:28+00:00\",\"dateModified\":\"2026-09-06T09:20:14+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/colour-contrast-palette-auditor\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/colour-contrast-palette-auditor\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/colour-contrast-palette-auditor\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Toolbox\",\"item\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/\"},{\"@type\":\"ListItem\",\"position\":3,\"name\":\"Colour Contrast and Palette Auditor: two themes, one set of names\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/\",\"name\":\"Lukas Wojcik - Blog\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\",\"name\":\"luky\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"contentUrl\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"width\":424,\"height\":636,\"caption\":\"luky\"},\"logo\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\"},\"sameAs\":[\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Colour Contrast and Palette Auditor: two themes, one set of names - Lukas Wojcik - Blog","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/colour-contrast-palette-auditor\/","og_locale":"en_US","og_type":"article","og_title":"Colour Contrast and Palette Auditor: two themes, one set of names - Lukas Wojcik - Blog","og_description":"Takes the CSS custom properties of a light and a dark palette, forms every foreground-on-background pair and measures its contrast ratio against WCAG 2.2. The point of holding both at once: a pair that passes in one theme and fails in the other, and a variable that exists in one palette but not the other.","og_url":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/colour-contrast-palette-auditor\/","og_site_name":"Lukas Wojcik - Blog","article_modified_time":"2026-09-06T09:20:14+00:00","og_image":[{"width":1200,"height":630,"url":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/08\/og-default.jpg","type":"image\/jpeg"}],"twitter_card":"summary_large_image","twitter_misc":{"Est. reading time":"1 minute"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/colour-contrast-palette-auditor\/","url":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/colour-contrast-palette-auditor\/","name":"Colour Contrast and Palette Auditor: two themes, one set of names - Lukas Wojcik - Blog","isPartOf":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/#website"},"datePublished":"2026-09-06T09:15:28+00:00","dateModified":"2026-09-06T09:20:14+00:00","breadcrumb":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/colour-contrast-palette-auditor\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/colour-contrast-palette-auditor\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/colour-contrast-palette-auditor\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.lukaswojcik.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Toolbox","item":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/"},{"@type":"ListItem","position":3,"name":"Colour Contrast and Palette Auditor: two themes, one set of names"}]},{"@type":"WebSite","@id":"https:\/\/www.lukaswojcik.com\/blog\/#website","url":"https:\/\/www.lukaswojcik.com\/blog\/","name":"Lukas Wojcik - Blog","description":"","publisher":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.lukaswojcik.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Person","Organization"],"@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9","name":"luky","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","url":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","contentUrl":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","width":424,"height":636,"caption":"luky"},"logo":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg"},"sameAs":["https:\/\/www.lukaswojcik.com\/blog"]}]}},"_links":{"self":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/pages\/15728","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/comments?post=15728"}],"version-history":[{"count":2,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/pages\/15728\/revisions"}],"predecessor-version":[{"id":15734,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/pages\/15728\/revisions\/15734"}],"up":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/pages\/38"}],"wp:attachment":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/media?parent=15728"}],"wp:term":[{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/tags?post=15728"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}