<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Hs Yi</title>
    <description>The latest articles on DEV Community by Hs Yi (@shadowbox).</description>
    <link>https://dev.to/shadowbox</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4054065%2F5909bb4e-d18e-413f-b70b-8f1600549d4d.jpg</url>
      <title>DEV Community: Hs Yi</title>
      <link>https://dev.to/shadowbox</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shadowbox"/>
    <language>en</language>
    <item>
      <title>I shipped a VS Code theme that only chose 67 of its colors</title>
      <dc:creator>Hs Yi</dc:creator>
      <pubDate>Fri, 31 Jul 2026 06:23:05 +0000</pubDate>
      <link>https://dev.to/shadowbox/i-shipped-a-vs-code-theme-that-only-chose-67-of-its-colors-15o9</link>
      <guid>https://dev.to/shadowbox/i-shipped-a-vs-code-theme-that-only-chose-67-of-its-colors-15o9</guid>
      <description>&lt;p&gt;Last week I wrote about the build gate that refuses to ship any color failing&lt;br&gt;
a WCAG contrast target. Every color in my theme is measured, and the build&lt;br&gt;
exits non-zero if one misses.&lt;/p&gt;

&lt;p&gt;Then I counted the colors.&lt;/p&gt;

&lt;p&gt;VS Code exposes somewhere north of 400 workbench color keys. My theme set&lt;br&gt;
&lt;strong&gt;67&lt;/strong&gt;. The other 350-odd weren't absent — VS Code filled them in from its&lt;br&gt;
built-in defaults. My "fully measured, contrast-gated" theme was quietly&lt;br&gt;
showing colors it had never chosen, let alone measured.&lt;/p&gt;

&lt;p&gt;That's the thing about a gate: it only guards what you hand it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Unset is not neutral
&lt;/h2&gt;

&lt;p&gt;This is the part I'd want another theme author to take away. When you leave a&lt;br&gt;
workbench key unset, you don't get "nothing" — you get VS Code's fallback,&lt;br&gt;
computed from your theme's declared &lt;code&gt;type&lt;/code&gt; (&lt;code&gt;dark&lt;/code&gt; or &lt;code&gt;light&lt;/code&gt;). It's a&lt;br&gt;
reasonable guess. It is not your palette.&lt;/p&gt;

&lt;p&gt;So a theme with 67 keys is a theme with 67 opinions and 350 borrowed ones.&lt;br&gt;
Everywhere those borrowed colors surface — the peek view, the merge editor,&lt;br&gt;
notebook cells, the testing gutter — your careful palette gets diluted by&lt;br&gt;
something that was never checked against anything.&lt;/p&gt;

&lt;p&gt;I went from 67 to &lt;strong&gt;417 keys&lt;/strong&gt;. Bracket-pair colorization (six levels, plus&lt;br&gt;
the unexpected-bracket color), inlay hints, ghost text from inline&lt;br&gt;
suggestions, sticky scroll, peek view, the suggest and hover widgets, the&lt;br&gt;
minimap and overview ruler, notebooks, testing icons, the merge editor, debug&lt;br&gt;
icons, symbol icons in autocomplete, the command center, the settings and&lt;br&gt;
welcome pages.&lt;/p&gt;

&lt;p&gt;None of that is exotic. It's what a normal day in the editor looks like.&lt;/p&gt;
&lt;h2&gt;
  
  
  The other gap: your regexes stop at the language server
&lt;/h2&gt;

&lt;p&gt;Classic TextMate theming matches scopes with regular expressions. That works&lt;br&gt;
until it doesn't, and where it stops is exactly where modern editing begins.&lt;/p&gt;

&lt;p&gt;A regex cannot tell you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;whether &lt;code&gt;Foo&lt;/code&gt; here is a type or a value — same spelling, same scope&lt;/li&gt;
&lt;li&gt;whether &lt;code&gt;count&lt;/code&gt; is a parameter or a local&lt;/li&gt;
&lt;li&gt;whether a binding is &lt;code&gt;readonly&lt;/code&gt;, which arguably matters more than its type&lt;/li&gt;
&lt;li&gt;whether the symbol you're looking at is deprecated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The language server knows all four. VS Code will hand you its answer through&lt;br&gt;
semantic tokens — but only if your theme asks. Mine didn't. Which means the&lt;br&gt;
moment a language server attached, the colors people actually saw drifted&lt;br&gt;
away from the ones I'd measured.&lt;/p&gt;

&lt;p&gt;Turning it on is two things: a flag, and a mapping.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"semanticHighlighting"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"semanticTokenColors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"variable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#d8dae5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"variable.readonly"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#e08a4e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="c1"&gt;// reads as a constant, because it is one&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"parameter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"foreground"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#d8dae5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"fontStyle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"italic"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"property.readonly"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#e08a4e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"class.defaultLibrary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#4fb5ad"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// stdlib differs from your own types&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"*.deprecated"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"strikethrough"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;37 rules in my case. The two I'd argue hardest for:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;variable.readonly&lt;/code&gt; colored as a constant.&lt;/strong&gt; A regex sees a variable and&lt;br&gt;
colors it like one. The language server knows it can never be reassigned.&lt;br&gt;
That's not a cosmetic distinction — it's the difference between "watch this"&lt;br&gt;
and "ignore this" while reading unfamiliar code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;*.deprecated&lt;/code&gt; as a strikethrough, not a dimmer color.&lt;/strong&gt; This was a contrast&lt;br&gt;
decision disguised as a style one. The obvious way to mark deprecated code is&lt;br&gt;
to fade it. But fading is exactly what my gate exists to prevent: it takes a&lt;br&gt;
color I measured at 4.6:1 and quietly drops it below threshold. A&lt;br&gt;
strikethrough carries the same signal at zero contrast cost. When the&lt;br&gt;
accessible option and the pretty option disagree, the gate breaks the tie.&lt;/p&gt;
&lt;h2&gt;
  
  
  Growing the surface means growing the gate
&lt;/h2&gt;

&lt;p&gt;Here's the trap. I'd just added 350 color keys — buttons with text on them,&lt;br&gt;
badges, the error status bar, syntax colors rendered inside the autocomplete&lt;br&gt;
popup. Every one of those is a foreground on a background. My gate didn't&lt;br&gt;
know about any of them.&lt;/p&gt;

&lt;p&gt;If I'd stopped there, I'd have shipped &lt;em&gt;more&lt;/em&gt; unmeasured color than before,&lt;br&gt;
under a README that still said "every pair is checked." Last time an&lt;br&gt;
adversarial audit caught that kind of gap for me. This time it was the obvious&lt;br&gt;
next question: what did I just add that the gate can't see?&lt;/p&gt;

&lt;p&gt;The gate now checks &lt;strong&gt;177 distinct pairs&lt;/strong&gt;, and while I was adding them I found&lt;br&gt;
the old number was flattering itself. Contrast is symmetric — &lt;code&gt;contrast(a, b)&lt;/code&gt;&lt;br&gt;
and &lt;code&gt;contrast(b, a)&lt;/code&gt; are the same measurement — and the palette reuses colors&lt;br&gt;
across roles: the error color &lt;em&gt;is&lt;/em&gt; the red accent, the cursor &lt;em&gt;is&lt;/em&gt; the yellow.&lt;br&gt;
Counting checks instead of distinct pairs had been inflating the figure. The&lt;br&gt;
gate now deduplicates, keeping the &lt;strong&gt;strictest&lt;/strong&gt; threshold when one pair is&lt;br&gt;
claimed by two roles. (Keep the looser one and you've quietly lowered your own&lt;br&gt;
bar.) These are the checks that cover genuinely new ground:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Text sitting ON a colored surface — buttons, badges, error/warning status bar.
# The foreground here is the background color. Easy to forget it's a pair at all.
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cyan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orange&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;button text on &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;accent&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mf"&gt;4.5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;warning&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;info&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status bar text on &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mf"&gt;4.5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# Syntax colors rendered inside the suggest/hover widget, which uses a
# different background than the editor. Same color, different surface.
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;function&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;keyword&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;variable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;constant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;property&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;class&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; in suggest widget&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;syn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;bg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;overlay&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rule of thumb I'd offer: &lt;strong&gt;every new color key is a claim that some text will&lt;br&gt;
be readable somewhere.&lt;/strong&gt; If you can't name the pair, you haven't finished&lt;br&gt;
adding the key.&lt;/p&gt;
&lt;h2&gt;
  
  
  When the gate overruled the design
&lt;/h2&gt;

&lt;p&gt;The best moment in this pass was the one where the measurement changed my mind&lt;br&gt;
instead of confirming it.&lt;/p&gt;

&lt;p&gt;Inlay hints — those little inferred type annotations VS Code injects inline —&lt;br&gt;
conventionally get a subtle tinted background so they read as "not really your&lt;br&gt;
code." I did that. Then I measured it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;inlay hint text on tinted background:  3.12 – 3.48 : 1   ✗
inlay hint text on editor background:  4.57 – 4.70 : 1   ✓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The decoration was costing more than a full contrast tier. Two ways out:&lt;br&gt;
lower the threshold for hints because they're "supplementary," or drop the&lt;br&gt;
background.&lt;/p&gt;

&lt;p&gt;I dropped the background. The tint was there to say &lt;em&gt;this text is different&lt;/em&gt;,&lt;br&gt;
but italic-adjacent placement and the hint color already say that, and neither&lt;br&gt;
costs anything measurable. Lowering the threshold would have been the same&lt;br&gt;
move as picking a color by eye — deciding by what I wanted to be true.&lt;/p&gt;

&lt;p&gt;That's what a gate is for. Not to certify decisions you already made, but to&lt;br&gt;
occasionally make one for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed, concretely
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;before&lt;/th&gt;
&lt;th&gt;after&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;workbench keys&lt;/td&gt;
&lt;td&gt;67&lt;/td&gt;
&lt;td&gt;417&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;semantic token rules&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;distinct contrast pairs&lt;/td&gt;
&lt;td&gt;177&lt;/td&gt;
&lt;td&gt;177&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;build failures&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pair count didn't move because deduplicating cancelled out the additions —&lt;br&gt;
the old gate was counting the same pair up to three times. That row is the&lt;br&gt;
correction; the last row is the only one that was ever allowed to be non-zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell other theme authors
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Count your keys.&lt;/strong&gt; If it's under a hundred, most of your theme is VS
Code's defaults wearing your name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship &lt;code&gt;semanticHighlighting: true&lt;/code&gt;.&lt;/strong&gt; Without it your careful colors get
overruled the moment a language server attaches — which is always.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;variable.readonly&lt;/code&gt; is the highest-value rule in the file.&lt;/strong&gt; Regexes
can't reach it, and it changes how code reads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encode "de-emphasized" without spending contrast.&lt;/strong&gt; Strikethrough,
italics and placement are free; dimming is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When you add color surface, add gate coverage in the same commit.&lt;/strong&gt;
Otherwise your README is describing a build you no longer run.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GitHub (MIT — VS Code extension + terminal ports): &lt;a href="https://github.com/art220/dancheong" rel="noopener noreferrer"&gt;https://github.com/art220/dancheong&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;VS Code Marketplace: &lt;a href="https://marketplace.visualstudio.com/items?itemName=dancheong.dancheong-theme" rel="noopener noreferrer"&gt;https://marketplace.visualstudio.com/items?itemName=dancheong.dancheong-theme&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Open VSX (Cursor / Windsurf / VSCodium): &lt;a href="https://open-vsx.org/extension/dancheong/dancheong-theme" rel="noopener noreferrer"&gt;https://open-vsx.org/extension/dancheong/dancheong-theme&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;All-13-apps PRO bundle: &lt;a href="https://dancheong.gumroad.com/l/dancheong-pro" rel="noopener noreferrer"&gt;https://dancheong.gumroad.com/l/dancheong-pro&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Previous post: the contrast gate itself, and the palette it enforces —&lt;br&gt;
&lt;a href="https://dev.to/shadowbox/my-theme-build-fails-if-any-color-misses-wcag-contrast-heres-the-pipelinepublished-true-1c89"&gt;My theme build fails if any color misses WCAG contrast&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If a color reads wrong on your setup, open an issue. One palette edit and a&lt;br&gt;
rebuild fixes every port at once.&lt;/p&gt;

</description>
      <category>vscode</category>
      <category>webdev</category>
      <category>designsystem</category>
      <category>opensource</category>
    </item>
    <item>
      <title>My theme build fails if any color misses WCAG contrast — here's the pipeline</title>
      <dc:creator>Hs Yi</dc:creator>
      <pubDate>Thu, 30 Jul 2026 01:41:23 +0000</pubDate>
      <link>https://dev.to/shadowbox/my-theme-build-fails-if-any-color-misses-wcag-contrast-heres-the-pipelinepublished-true-1c89</link>
      <guid>https://dev.to/shadowbox/my-theme-build-fails-if-any-color-misses-wcag-contrast-heres-the-pipelinepublished-true-1c89</guid>
      <description>&lt;p&gt;Korean temples and palaces have been painted with the same mineral pigment&lt;br&gt;
system — &lt;em&gt;dancheong&lt;/em&gt; (단청) — for about 1,500 years. Deep indigo woodwork,&lt;br&gt;
cinnabar red, ochre gold. Mineral pigments read vivid but never neon, which&lt;br&gt;
turns out to be exactly what you want to stare at for eight hours a day.&lt;/p&gt;

&lt;p&gt;I turned that palette into a theme family for VS Code and 12 other apps.&lt;br&gt;
This post is about the part I actually think is worth stealing: &lt;strong&gt;the build&lt;br&gt;
refuses to ship any color that fails a WCAG contrast target.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The problem with picking colors by eye
&lt;/h2&gt;

&lt;p&gt;Every theme author has done this: nudge a hex value until it "looks right"&lt;br&gt;
on your monitor, at your brightness, in your lighting. Then someone opens it&lt;br&gt;
on a glossy laptop in daylight and your comments are invisible.&lt;/p&gt;

&lt;p&gt;Contrast is measurable, so I made it a build gate instead of a taste call.&lt;/p&gt;
&lt;h2&gt;
  
  
  One palette file, thirteen apps
&lt;/h2&gt;

&lt;p&gt;The entire theme family lives in a single &lt;code&gt;palette.json&lt;/code&gt;. Variants (Night,&lt;br&gt;
Dawn, Storm, Mist) each define background, foreground tiers, and accent roles.&lt;br&gt;
A build script generates every port from it — VS Code, JetBrains, Neovim,&lt;br&gt;
Sublime, Obsidian, iTerm2, Alacritty, Kitty, Ghostty, Windows Terminal, Warp,&lt;br&gt;
tmux, Zed. 4 variants × 13 apps = 52 files, byte-identical colors everywhere.&lt;/p&gt;

&lt;p&gt;Change one hex value, run the build, and every port updates. No drift.&lt;/p&gt;
&lt;h2&gt;
  
  
  The gate
&lt;/h2&gt;

&lt;p&gt;WCAG contrast is ~20 lines of Python, straight from the spec:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rel_lum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;12.92&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;0.04045&lt;/span&gt; &lt;span class="nf"&gt;else &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.055&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;1.055&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mf"&gt;2.4&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;hex_to_rgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;0.2126&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.7152&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.0722&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;contrast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;la&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rel_lum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;rel_lum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;hi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;la&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lb&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;la&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every text role is checked against &lt;strong&gt;every background it actually ships&lt;br&gt;
on&lt;/strong&gt; — the editor, the sidebar, inputs, the terminal, and the selection&lt;br&gt;
highlight. 222 checks per build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;pairs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body/base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;bg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mf"&gt;7.0&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;          &lt;span class="c1"&gt;# AAA
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;surface&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;overlay&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;highlight&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;selection&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;bg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mf"&gt;4.5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;       &lt;span class="c1"&gt;# AA
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;surface&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;overlay&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;muted/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;muted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;bg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mf"&gt;4.5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;     &lt;span class="c1"&gt;# UI text
&lt;/span&gt;&lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;comment/base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subtle&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;bg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mf"&gt;4.5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c1"&gt;# AA
&lt;/span&gt;&lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;comment/sel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subtle&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;bg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;selection&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;accent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;                            &lt;span class="c1"&gt;# syntax
&lt;/span&gt;    &lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mf"&gt;4.5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/sel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;selection&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c1"&gt;# … plus ANSI colors, diagnostics, git decorations, the cursor
&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fg_c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bg_c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;need&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;contrast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fg_c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bg_c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;need&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; below &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;need&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;problems&lt;/code&gt; is non-empty, the build exits. There is no &lt;code&gt;--force&lt;/code&gt; flag on&lt;br&gt;
purpose. A theme that fails contrast isn't "stylistically different" — it's&lt;br&gt;
broken for someone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Confession: v1 of this gate had holes
&lt;/h2&gt;

&lt;p&gt;The first version of the gate checked 11 colors per variant — against &lt;strong&gt;one&lt;/strong&gt;&lt;br&gt;
background. It said "all OK" while the shipped theme had line numbers at&lt;br&gt;
1.83:1 and comments that vanished to 1.9:1 the moment you selected them.&lt;br&gt;
The selection highlight &lt;em&gt;is&lt;/em&gt; a background; so is the sidebar; so is the&lt;br&gt;
input field. A gate that only knows about &lt;code&gt;bg.base&lt;/code&gt; is a gate with a&lt;br&gt;
one-pixel view of your theme.&lt;/p&gt;

&lt;p&gt;An adversarial audit (I pointed review agents at my own product and told&lt;br&gt;
them to break it) found 19 shipped pairs the old gate never saw. Expanding&lt;br&gt;
the gate to every pair the emitters actually produce forced a palette retune —&lt;br&gt;
comments moved up to AA (4.5:1), selection backgrounds got darker so text&lt;br&gt;
stays ≥3:1 while selected. The lesson: &lt;strong&gt;a gate is only as honest as its&lt;br&gt;
pair inventory.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Update: the 222 figure above was itself counting the same pair more than&lt;br&gt;
once. Contrast is symmetric, and the palette reuses colors across roles —&lt;br&gt;
the error color&lt;/em&gt; is &lt;em&gt;the red accent. The honest count is 177 distinct pairs;&lt;br&gt;
&lt;a href="https://dev.to/shadowbox/i-shipped-a-vs-code-theme-that-only-chose-67-of-its-colors-15o9"&gt;post 2 has the details&lt;/a&gt;.)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Tuning by binary search, not by eye
&lt;/h2&gt;

&lt;p&gt;Here's the inversion that made the palette coherent: instead of picking a&lt;br&gt;
color and checking its contrast, &lt;strong&gt;pick the contrast and solve for the color.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Take the hue and saturation from the pigment reference, then binary-search&lt;br&gt;
lightness until the pair lands on the target ratio (e.g. exactly 7:1 for body&lt;br&gt;
text). Every variant's colors sit at &lt;em&gt;chosen&lt;/em&gt; contrast levels instead of&lt;br&gt;
accidental ones.&lt;/p&gt;

&lt;p&gt;This caught a real design bug: the first pass of Storm (the high-contrast&lt;br&gt;
variant) produced accent colors &lt;em&gt;darker&lt;/em&gt; than the regular dark variant,&lt;br&gt;
because I'd reused the same 4.5:1 target. High contrast has to mean a higher&lt;br&gt;
target — raising it to 7.5:1 fixed the whole variant in one rebuild. Storm's&lt;br&gt;
body text now measures 14.6:1.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell other theme authors
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Make contrast a build gate, not a review comment.&lt;/strong&gt; The math is 20 lines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enumerate every background text can sit on&lt;/strong&gt; — selection, sidebar,
inputs, terminal. Checking against just the editor background is how
invisible line numbers ship with a green checkmark.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solve for lightness given a contrast target&lt;/strong&gt; — hue and saturation carry
the identity; lightness is a free variable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate every port from one source file.&lt;/strong&gt; Hand-synced ports always drift.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Different contrast tiers are a feature.&lt;/strong&gt; Comments at 4.5:1 stay quieter
than 7:1 body text but never disappear — not even mid-selection.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GitHub (MIT — VS Code extension + terminal ports): &lt;a href="https://github.com/art220/dancheong" rel="noopener noreferrer"&gt;https://github.com/art220/dancheong&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;VS Code Marketplace: &lt;a href="https://marketplace.visualstudio.com/items?itemName=dancheong.dancheong-theme" rel="noopener noreferrer"&gt;https://marketplace.visualstudio.com/items?itemName=dancheong.dancheong-theme&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;All-13-apps PRO bundle: &lt;a href="https://dancheong.gumroad.com/l/dancheong-pro" rel="noopener noreferrer"&gt;https://dancheong.gumroad.com/l/dancheong-pro&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's week one. If a color fails on your setup, open an issue — the fix is one&lt;br&gt;
palette edit and a rebuild away.&lt;/p&gt;

</description>
      <category>vscode</category>
      <category>webdev</category>
      <category>designsystem</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
