Skip to content

Personal · 2026 · Open source on

GitHub

Kohra — A cool fog-grey dark theme

Most editor themes are a bag of hand-picked hex values that look right until you add a sixteenth one. Kohra is an attempt to design a dark theme the way you’d design a type system (with rules instead of taste) and then ship it to every editor I use from one source of truth. The name is Hindi for fog.

Tokens picked in isolation#

Pick any well-known dark theme and inspect its tokens in a perceptual colour space. Keywords are louder than types. Strings dominate functions. Comments fight the surface they’re supposed to recede into. Each token was picked in isolation; nothing was picked against everything else. It’s the kind of problem you don’t see until you look for it, and then can’t stop seeing.

One rule#

The rule that organises Kohra is one sentence long. Every token belongs to a bucket. Inside a bucket, lightness (L) and chroma (C) are locked in OKLCH; only hue varies. That’s the entire system.

Tokens of the same role read as siblings because they are siblings in perceptual coordinates: keyword and string and function-name sit at the same lightness and the same chroma, just at different hues. Comments don’t shout because they live in the lowest-luminance syntax bucket. Diagnostics break the rule on purpose, because their job is to interrupt; if they sat at the same chroma as the surrounding code they’d disappear into it.

Why OKLCH, not HSL#

The choice of colour space matters here more than it might seem. HSL lies. Two HSL colours with the same lightness value will not appear equally bright: yellow at L 50% is visually a different planet from blue at L 50%. That’s fine for a colour picker. It’s lethal for a system whose entire claim is that tokens in the same bucket feel the same.

OKLCH is perceptually uniform; L 0.62 looks like L 0.62 whether you’re at hue 30° or hue 240°. Built in HSL, the muted greens would have read as quieter than the muted blues at the same nominal lightness, and the whole premise would have collapsed at the first colour grading session. Hex is the export format: values are picked in OKLCH in Figma and only converted when they’re written into the theme JSONs.

Fifteen buckets#

Fifteen buckets cover every variable in the system. The table is the spec.

  • neutral ramps
    L per step · C 0.007 · H 240°

    surface + text, terminal black/white/bright

  • syntax-muted
    L per step · C 0.025

    comments

  • syntax-neutral
    L 0.78 · C 0.012

    variables, classes

  • syntax-faint
    L 0.56 · C 0.012

    operators, escapes

  • syntax-primary
    L 0.62 · C 0.075

    keyword, string, function, type, tag, number, parameter

  • accent-primary
    L 0.62 · C 0.075

    base accent hues

  • accent-dim
    L 0.55 · C 0.06

    muted / dark accent variants

  • accent-bright
    L 0.72 · C 0.075

    bright accent variants

  • brand
    L 0.45 / 0.30 · C 0.10 / 0.06

    brand blue + dim

  • diag
    L 0.65 · C 0.13

    error, warning, info, hint — foreground

  • vcs-fg
    L 0.60 · C 0.07

    version-control foreground

  • vcs-bg
    L 0.25 · C 0.04

    version-control backgrounds

  • term-normal
    L 0.60 · C 0.10

    terminal ANSI 8 (red, green, yellow, blue, magenta, cyan)

  • term-bright
    L 0.75 · C 0.09

    terminal ANSI bright 8

  • chart
    L 0.65 · C 0.11

    chart series

Two things in the table earn a second look. Syntax-primary and accent-primary share coordinates; they only differ in role, not in colour, which is how a custom highlight ends up reading as part of the same family as a keyword. And diagnostics are the only bucket allowed to break the chroma ceiling at 0.13 against the 0.075 cap on syntax-primary, because they have to interrupt.

Adding any new token is a two-step move: classify it, then conform it to the bucket’s (L, C). If you can’t classify it, the bucket system is incomplete and the rule is what needs editing, not the colour.

A cold, still palette#

The chromatic identity is cold and still: a winter morning under fog, not a stylistic substitution. The neutral ramp runs at chroma 0.007 with a faint blue tint at hue ≈ 240°; there’s no warmth anywhere in the chrome. Accent hues are muted dusk-blue, teal, cyan, faded purple, and a desaturated green and amber held back toward grey. Hues were picked for visual comfort over long reading sessions, not strict colour-wheel logic.

There’s no system accent: nothing in the chrome shouts to remind you which brand it is. The active tab is a luminance shift, not a hue shift. The selection highlight is a low-chroma surface, not blue. After a few weeks of looking at default VS Code’s bright blue activity bar, the absence is the feature.

Five editors, one source of truth#

From the same Figma file, five implementations track one source of truth.

  • VS Code — uses the original Tokyo Night scope coverage with every colour replaced.
  • Cursor — reads the VS Code extension natively, so the file there is currently byte-identical; carved out anyway so the Cursor-specific surfaces (ghost text, inline AI diff, composer chrome) can drift later without touching the parent.
  • Zed — has a different scope vocabulary; the bucket each token belongs to is the bridge, so a token in Tokyo Night gets ported to Zed by looking up its bucket and assigning the same (L, C) at the appropriate hue.
  • Ghostty — exposes only the 16 ANSI colours plus bg/fg/cursor/selection, which means the term-normal and term-bright buckets are the only ones that survive the port (fine for a terminal, but the limit is worth knowing).
  • Sublime — separates syntax from chrome into two files; the chrome theme is scaffolded from ayu-mirage and uses ayu’s PNG icon assets verbatim, which means the textures only resolve when the repo is mounted at Packages/Kohra/.

Five targets, one palette. When a hue shifts in Figma, a script propagates the new hex through the VS Code and Cursor files; the Zed and Sublime files are updated in parallel against the same values.

What it isn’t#

  • It’s not a marketplace product.
  • There’s no light variant, and one isn’t planned — perceptual chroma headroom on a light background is much lower, so a light variant under the same bucket discipline would be a second pass through every coordinate, not a port.
  • It isn’t WCAG or APCA certified — contrast was tuned by eye against the editor surface and held above Lc 60 for body syntax and Lc 45 for muted tokens, but every grader will have a token they want to push.

It’s a personal tool, made public for anyone curious about the bucket system. The README and CLAUDE.md in the repo are the long-form spec. The story of how it came about is in Todd Hido’s fog and one lit window.

Credits and license#

Scaffolded from Tokyo Night by Enkia (MIT); the VS Code extension manifest and full scope coverage are inherited and every colour value has been replaced. The Sublime chrome theme and its icon assets are scaffolded separately from ayu-mirage by Ike Ku (MIT). License remains MIT; third-party attribution is in NOTICE.