Skip to content

Themed dimText falls below WCAG AA contrast on 19 of 22 first-party themes #10533

Description

@slhuckstead

System details

Omarchy 4.0.2, Arch, Hyprland. Not hardware-specific — this lives in
default/themed/*.tpl and reproduces from the shipped theme files alone.
Confirmed present at quattro HEAD.

What's wrong?

default/themed/pi.json.tpl derives dimmed text with a fixed blend toward the
background:

34:    "mutedText": "{{ mix foreground background 34% }}",
35:    "dimText":   "{{ mix foreground background 52% }}"

dimText isn't decoration — the same file maps it to text people read:

46:    "dim": "dimText",
48:    "thinkingText": "dimText",
73:    "syntaxComment": "dimText",

A fixed percentage assumes every theme has the same foreground-to-background
headroom, and they don't. Mixing toward the background also drops luminance
non-linearly, so the same 52% costs far more contrast on a low-headroom theme
than a high-headroom one.

Measured across the 22 themes in themes/ that ship a colors.toml,
19 of 22 land below the WCAG AA 4.5:1 minimum for normal text, and 6 fall
below even the 3:1 large-text floor:

theme dimText @ 52% @ 25%
rose-pine 2.17:1 3.70:1
catppuccin-latte 2.21:1 3.85:1
tokyo-night 2.89:1 5.15:1
everforest 2.93:1 4.91:1
lupine 2.98:1 6.92:1
gruvbox 3.03:1 5.29:1
miasma 3.14:1 5.59:1
matte-black 3.21:1 6.10:1
osaka-jade 3.22:1 5.96:1
flexoki-light 3.36:1 8.59:1
nord 3.41:1 6.00:1
solitude 3.52:1 6.93:1
kanagawa 3.65:1 6.92:1
ristretto 3.66:1 6.83:1
catppuccin 3.66:1 6.95:1
white 3.69:1 10.37:1
ethereal 3.80:1 7.87:1
lumon 3.86:1 7.41:1
retro-82 3.92:1 7.90:1
hackerman 4.61:1 9.91:1
vantablack 4.89:1 11.42:1
last-horizon 4.89:1 10.77:1

The same pattern appears in three other text-bearing tokens:

template token mix below 4.5:1 worst
pi.json.tpl:34 mutedTextmdQuote, syntaxPunctuation 34% 5/22 3.08:1
claude.json.tpl:9 inactive 40% 10/22 2.72:1
shell.toml.tpl:216 placeholder 34% 5/22 3.08:1

Steps to reproduce

Reads only the shipped theme files; changes nothing:

python3 - <<'PY'
import re, glob, os
def lum(h):
    h = h.lstrip("#")[:6]
    c = [int(h[i:i+2], 16)/255 for i in (0, 2, 4)]
    c = [x/12.92 if x <= 0.04045 else ((x+0.055)/1.055)**2.4 for x in c]
    return 0.2126*c[0] + 0.7152*c[1] + 0.0722*c[2]
def ratio(a, b):
    la, lb = lum(a), lum(b)
    return (max(la, lb) + 0.05) / (min(la, lb) + 0.05)
def mix(a, b, amt):
    a, b = a.lstrip("#"), b.lstrip("#")
    return "#" + "".join(f"{int(int(a[i:i+2],16)*(1-amt)+int(b[i:i+2],16)*amt+0.5):02x}"
                         for i in (0, 2, 4))
bad = 0
for d in sorted(glob.glob("/usr/share/omarchy/themes/*")):
    f = os.path.join(d, "colors.toml")
    if not os.path.exists(f):
        continue
    t = open(f).read()
    m1 = re.search(r'^foreground\s*=\s*"(#\w{6})"', t, re.M)
    m2 = re.search(r'^background\s*=\s*"(#\w{6})"', t, re.M)
    if not (m1 and m2):
        continue
    fg, bg = m1.group(1), m2.group(1)
    r = ratio(mix(fg, bg, 0.52), bg)
    bad += r < 4.5
    print(f"{os.path.basename(d):20} dimText {mix(fg,bg,0.52)} = {r:5.2f}:1"
          f"{'  BELOW AA' if r < 4.5 else ''}")
print(f"\n{bad} themes below 4.5:1")
PY

Suggested fix

Lowering the constant helps but does not solve it. At 25%, dimText clears
4.5:1 on 20 of 22 — rose-pine (3.70:1) and catppuccin-latte (3.85:1) still
fail, because their own undimmed foreground is only 6.66:1 and 7.06:1, so any
meaningful dimming drops under. Going low enough to save those two (~15%) makes
dimText nearly indistinguishable from normal text, which defeats the token.

The robust fix is to solve for a contrast floor instead of a fixed blend:
mix toward the background until the result would drop below a target ratio,
then stop. Something like {{ dim foreground background 52% min 4.5 }}. That
keeps dim genuinely dim on themes with headroom, and stops it becoming
unreadable on themes without — without either constraining theme authors'
palettes or hand-tuning a constant per theme.

Happy to send a PR for whichever direction you'd prefer.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions