[BUG] dark-daltonized: userMessageBackground renders at 1.18:1 on a default dark terminal, leaving user messages with no visible separator
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
In viewMode: "focus", the only visual cue separating a user message from Claude's output is a background block painted with the userMessageBackground theme token. In both dark themes that token is rgb(55,55,55) (#373737).
Against a default dark terminal background, that block is not merely hard to see, it is imperceptible. Measured with WCAG 2.x relative luminance:
| Terminal background | Contrast of the #373737 block |
| --- | --- |
| Ghostty 1.3.1 default, #282c34 | 1.18:1 |
| Pure black, #000000 | 1.76:1 |
| VS Code dark+ default, #1f1f1f | 1.38:1 |
For reference, WCAG's floor for non-text UI elements is 3:1.
The text drawn on the block is not the problem. text is #ffffff, which is 11.90:1 against #373737. It is the block-versus-terminal-background separation that fails, which means the cue is absent rather than low quality. Nothing is drawn that a reader can find.
Why I think this is a bug and not a preference: it lands hardest in dark-daltonized, the theme labeled "Dark mode (colorblind-friendly)". Diffing that palette against plain dark, the tokens userMessageBackground, userMessageBackgroundHover, subtle, inactive, and text are identical. Every token the daltonized theme actually changes is a hue substitution.
So the theme advertised as colorblind-friendly leaves untouched the one cue in the transcript that carries no hue information at all. A user who cannot rely on the hue channel gets no benefit from the daltonized theme on the hardest part of reading a transcript, which is telling their own words apart from the assistant's at a glance. The theme's stated purpose and its actual effect diverge precisely where it matters most.
Same root cause, lower severity, mentioning it here only for context rather than as a second report: subtle is #505050, which is 1.74:1 against #282c34. Happy to file separately if you prefer strict one-token-per-issue.
What Should Happen?
The block behind user messages should be perceptible against a typical dark terminal background.
Worth knowing before picking a fix: there is a hard ceiling on the obvious approach, because the block's foreground is the global text token. The renderer draws Do("text", theme) over Do("userMessageBackground", theme, "background"), so raising the block's luminance to separate it from the terminal background lowers the contrast of the user's own typed text sitting on it, at close to a 1:1 exchange. Measured against a pure black terminal background:
| userMessageBackground | Block vs. background | White text on block |
| --- | --- | --- |
| #373737 (current) | 1.76:1 | 11.90:1 |
| #5a7596 | 4.42:1 | 4.75:1 |
| #63809f | 5.12:1 | 4.10:1 |
| #6c8aa8 | 5.84:1 | 3.60:1 |
#5a7596 is the brightest value where both numbers still clear 4.5:1. Past it, you are trading the legibility of the user's own text for the visibility of the block.
Three options, in my order of preference:
- Add a channel that is not color. A left rule, a border, or a gutter glyph on user messages would survive any terminal background, any theme, and any type of color vision. It is the only fix here with no ceiling, and it is the standard remedy for cue-carried-by-color-alone. Related in spirit: #83570 asks for a shape channel on session status for the same reason.
- Raise the dark-theme value to roughly
#5a7596, which clears 3:1 against every common dark terminal background while keepingtexton the block above 4.5:1. - Decouple the block's foreground from
textby giving it its own token. The current coupling is what creates the ceiling in the first place. This also composes with #77041, which asks foruserMessageBackgroundto be split because it is shared with the conversation selector and the Plugin/Skill/MCP badges.
Option 1 plus option 3 would leave the palette free to be tuned without a trade-off.
Error Messages/Logs
Not applicable, this is a rendering issue with no error output.
Steps to Reproduce
- Use Ghostty 1.3.1 with a default config, or any terminal with a dark background near
#282c34. - In
~/.claude/settings.json, set"theme": "dark-daltonized"and"viewMode": "focus". - Start a session and exchange a few messages.
- Scroll back through the transcript. There is no perceptible block behind the user messages, so in a long thread the only remaining separator is the
You/Claudelabel, which is a hue cue. - To confirm numerically rather than by eye:
def lum(h):
h = h.lstrip('#')
c = [int(h[i:i+2], 16) / 255 for i in (0, 2, 4)]
c = [(v / 12.92 if v <= 0.04045 else ((v + 0.055) / 1.055) ** 2.4) for v in c]
return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]
def contrast(a, b):
la, lb = lum(a), lum(b)
hi, lo = max(la, lb), min(la, lb)
return (hi + 0.05) / (lo + 0.05)
print(round(contrast('#373737', '#282c34'), 2)) # 1.18 userMessageBackground vs Ghostty default
print(round(contrast('#505050', '#282c34'), 2)) # 1.74 subtle vs Ghostty default
Claude Model
Not model dependent.
Is this a regression?
Not that I can tell. #373737 appears to be the long-standing value.
Claude Code Version
2.1.229
Platform
macOS (Darwin 25.5.0), Ghostty 1.3.1, TERM=xterm-ghostty, COLORTERM=truecolor
Workaround, for anyone who finds this issue
Custom themes work today and can override the token, though the mechanism is undocumented. Create ~/.claude/themes/<slug>.json:
{
"name": "High contrast",
"base": "dark-daltonized",
"overrides": {
"userMessageBackground": "#5a7596",
"userMessageBackgroundHover": "#6c8aa8",
"briefLabelYou": "#8ecbff",
"briefLabelClaude": "#ffc14d",
"subtle": "#7d8590",
"inactive": "#b0b8c2"
}
}
Then set "theme": "custom:<slug>" in settings.json and restart. Accepted color formats are #rrggbb, #rgb, rgb(r,g,b), ansi256(n), and ansi:<name>. Note that unrecognized override keys are discarded silently, so a typo degrades contrast without any warning.
The briefLabelYou and briefLabelClaude values above are a blue/amber pair chosen so the label cue also survives dichromacy. Simulated with the Viénot 1999 model, the CIELAB delta E between them is 96.2 under protanopia, 107.5 under deuteranopia, and 54.2 under tritanopia, versus 65.3 / 79.8 / 69.6 for the stock daltonized pair.
Related issues
- #77041,
userMessageBackgroundis a shared token and should be split - #77129, distinguishing user prompts from assistant responses in long threads
- #83570, session status encoded by color alone, add a shape channel
- #32880, low-contrast syntax highlighting in the colorblind theme (closed as duplicate)
3 Comments
Correcting my own reproduction step 4, which overstated what the default configuration gives you.
I wrote that when the block is imperceptible, "the only remaining separator is the
You/Claudelabel." That is wrong. In a default install those labels do not render at all.The brief layout that draws them is gated:
So the label layout needs either
CLAUDE_CODE_BRIEFin the environment or thetengu_kairos_briefgate, which defaults tofalse. Neither is set on a default install,viewMode: "focus"alone does not enable it.And the block and the labels are mutually exclusive by construction, one line later in the same component:
Brief layout suppresses the block and shows the labels. Without brief layout you get the block and no
Youlabel. You never get both.The consequence is that this is somewhat worse than I first described, not better. In the default configuration the 1.18:1 block is the only separator between user and assistant turns, and when it is imperceptible there is no fallback cue of any kind. The label pair I referenced as a mitigation is latent, not active.
One related inconsistency I noticed while tracing this. The assistant-side label checks
isBriefOnlyon its own:while the user-side label and the block suppression check
isBriefOnlyand the gate. So the two sides can disagree about which layout is active, and a configuration exists where theClaudelabel renders but theYoulabel does not. Happy to split that into its own issue if it is worth tracking separately.Reproduced on 2.1.233 (Linux). With theme
dark-daltonized(same fordark), the user-message block renders at rgb(55,55,55) — captured as48;2;55;55;55on truecolor terminals,48;5;237(≈rgb(58,58,58)) when quantized to 256 colors.Measured WCAG contrast of the block vs common terminal backgrounds: 1.18:1 on #282c34, 1.76:1 on #000000, 1.38:1 on #1f1f1f — all below the 3:1 guideline for non-text UI, so your numbers check out (text on the block is fine at 11.9:1).
Two notes: user messages also have a
❯prefix, so the background isn't the sole marker, and there's no version where this value was different — it's a longstanding default, not a regression. A fixed grey can't clear 3:1 against every dark terminal background, so we're considering a lighter default and/or deriving it from the detected terminal background.In the meantime you can override it today with a custom theme:
/theme→ create a custom theme and set theuserMessageBackgroundkey (see https://code.claude.com/docs/en/terminal-config).🤖 Generated with Claude Code
Ah yes, for the meantime i created this terminal config to adapt the colors for my needs.
reference: https://github.com/samgutentag/claude-code-contrast