I was reading an article by Nikita Prokopov about how “everyone is getting syntax highlighting wrong” [1]. It struck me that I never really thought about my code being highlighted or not. I’m pretty basic and have used the default VSCode Dark Theme since I switched away from JetBrains forever ago. I’ve gotten so accustomed to it over the years I don’t really even register it.

This section in Nikita’s post stood out to me the most:

Here’s another test. Close your eyes (not yet! Finish this sentence first) and try to remember what color your color theme uses for class names?

Can you?

If the answer for both questions is “no”, then your color theme is not functional. It might give you comfort (as in—I feel safe. If it’s highlighted, it’s probably code) but you can’t use it as a tool. It doesn’t help you.

What’s the solution? Have an absolute minimum of colors. So little that they all fit in your head at once.

Yeah, I know theres some blue and other blue and purple in there… not really sure which is which though, so that’s about it. Ok so if I can’t remember, its because there’s too many colors in my theme, so lets just make a new one with fewer colors, how hard could it be?

We’ll it turns out if you’re trying to do a light theme, it’s actually really hard! Basically, in order to have contrast with a white background, colors need to be darker than they would be on a dark theme. Darker colors are less vibrant, and thus offer less perceptual impact.

But subjectively, I found that it was just hard for me to make a theme that felt like it belonged on this site (mostly 1 bit aesthetic). It’s not that the colors didn’t work to make certain sections stand out, it was that they made things stand out too much.

This experince got me thinking…

Why should code even be highlighted in the first place?

So the obvious answer seems to be some combination of two things:

  1. “I want to be able to tell code apart” (selective attention)
  2. “I want to find certain code by sight” (quickly locate)

Now after a bunch of experimentation—while I would still agree with that—I think figuring out what are the things I actually need to stand out and locate is the question that needed answering the most.

So at this point in the process, I didn’t really like any light themes I saw—they were either too hard to read or too monotonous compared to the absolute impact of dark themes. This got me thinking even further…

Why does highlighting even need to be in color?

What if we just tried to see how far we could get without using any color contrast, and only using tones?

First step, remove all color syntax and rip it raw black on white.

Stripped Down to Nothing
/** Await all values in a property and preserve original types */
async function resolveProperties<
  const T extends Record<string, unknown>,
>(properties: T): Promise<{ [K in keyof T]: Awaited<T[K]> }> {
  const entries = await Promise.all(
    Object.entries(properties).map(async ([key, value]) => [
      key,
      await value,
    ] as const),
  );

  return Object.fromEntries(entries) as {
    [K in keyof T]: Awaited<T[K]>;
  };
}

I uhhhh…. wow I really hate that. It’s so flat I fell like I have to read every character individually for my eyes to properly navigate! We are going to need to add back in some kind of contrast here.

So in order to not make this post take forever, we are going to skip through all of the revisions of me playing around and just cover the end results and my decisions.

Grayscale Highlighting Example
/** Await all values in a property and preserve original types */
async function resolveProperties<
  const T extends Record<string, unknown>,
>(properties: T): Promise<{ [K in keyof T]: Awaited<T[K]> }> {
  const entries = await Promise.all(
    Object.entries(properties).map(async ([key, value]) => [
      key,
      await value,
    ] as const),
  );

  return Object.fromEntries(entries) as {
    [K in keyof T]: Awaited<T[K]>;
  };
}

So base text is still just black on white, because we need some mid point to center our focus. Variable and function references stay baseline, because thats just most of the code and you can’t pay special attention to most of anything. Things that differ are, in order of importance:

  1. Comments: these contain high level descriptions (faster than reading the code) and external context (the “why” that reading code can’t answer). This is the most scannable thing in any big code file.
  2. Variable and Function Definitions: when creating something, it’s especially useful to stand out so that I can scan all of the object names being created. This supports my first scan of the code itself to figure out what’s going on. Click any one of those and now you can see all occurrences.
  3. Call Stack Flow: return, throw, yield are all branch terminators and mark where code in the function’s scope can stop or pause execution. These are structural and should be pretty quick to find by sight.

Now on the flip side, it dawned on me that similar to how I want to pay more attention to some areas, I also wanted to pay less attention to others.

  1. Common keywords like let, const: I basically never need to look at them after typing them out initially. These could be all but invisible and it would make no difference to me.
  2. Punctuation: Brackets, semicolon statement terminators, colons, commas: these are required so that the language can function, but they aren’t really that meaningful unless there is an error by improperly typing them. We make these just a tad lighter than base text so that they visually fall back a bit.

Outside of the static highlighting, we have the dynamic click based highlighting for same-words and scope/bracket locating.

The idea here is that the highlighting should let your eyes skip between the pieces that matter the most when reading, so that you can focus on the semantic parts and skim over the purely syntactical. It should be distinct enough to offer real value, but subtle enough that parts I am not trying to look at don’t distract from what I am.

Comments and More Control Flow
type Job = {
  id: string;
  label: string;
  enabled: boolean;
};

async function* completedJobs(jobs: Job[]): AsyncGenerator<string> {
  for (const job of jobs) {
    if (!job.enabled) continue;

    // Keep the request lazy so disabled jobs never wake the service.
    const response = await fetch(`/api/jobs/${job.id}`);
    if (!response.ok) continue;

    const result = (await response.json()) as {
      status: "complete" | "failed";
      message: string;
    };
    if (result.status !== "complete") continue;

    yield `${job.label} (${job.id}): ${result.message}`;
  }
}

Overall, optimizing highlighting based on what I wanted to pay attention to was a really interesting exercise in figuring out what exactly it was that I wanted to pay attention to.