DEV Community

Cover image for Dark mode with Shiki and Code Hike
Sebastian Sdorra
Sebastian Sdorra

Posted on • Originally published at sdorra.dev

Dark mode with Shiki and Code Hike

Warning: This article refers to versions of Code Hike before 0.8.0. Since version 0.8.0, Code Hike uses lighter instead of Shiki. The approach presented in this article uses shiki's color replacement method, which no longer works with lighter.

I'm using Code Hike a lot, including for the code snippets on my blog, and I love this library.
It has many nice features, like tabs or focus.
But it is lacking support for switching themes at runtime, which I want to use for a dark mode implementation.

https://github.com/code-hike/codehike/issues/271

That is really a pity.
But why actually?
Code Hike uses Shiki and Shiki supports changing themes via CSS variables.
We should try that out?

Shiki

In order to switch Shiki colors at runtime,
we have to load a special theme which uses CSS variables instead of hard coded colors.
This should look like this:

{
  options: {
    remarkPlugins: [
      [remarkCodeHike, { theme: "css-variables" }]
    ],
  },
}
Enter fullscreen mode Exit fullscreen mode

After that we can define our colors for the theme with css variables:

:root {
  --shiki-color-text: #24292f;
  --shiki-color-background: #ffffff;
  --shiki-token-constant: #0550ae;
  --shiki-token-string: #24292f;
  --shiki-token-comment: #6e7781;
  --shiki-token-keyword: #cf222e;
  --shiki-token-parameter: #24292f;
  --shiki-token-function: #8250df;
  --shiki-token-string-expression: #0a3069;
  --shiki-token-punctuation: #24292f;
  --shiki-token-link: #000012;
}
Enter fullscreen mode Exit fullscreen mode

And we can override them for the dark mode:

@media (prefers-color-scheme: dark) {
  :root {
    --shiki-color-text: #c9d1d9;
    --shiki-color-background: #0d1117;
    --shiki-token-constant: #79c0ff;
    --shiki-token-string: #a5d6ff;
    --shiki-token-comment: #8b949e;
    --shiki-token-keyword: #ff7b72;
    --shiki-token-parameter: #c9d1d9;
    --shiki-token-function: #d2a8ff;
    --shiki-token-string-expression: #a5d6ff;
    --shiki-token-punctuation: #c9d1d9;
    --shiki-token-link: #000012;
  }
}
Enter fullscreen mode Exit fullscreen mode

Tailwind CSS

If you use Tailwind CSS and use the dark mode with the class strategy instead of media query,
you can override the variables like this:

html.dark {
  --shiki-color-text: #c9d1d9;
  --shiki-color-background: #0d1117;
  --shiki-token-constant: #79c0ff;
  --shiki-token-string: #a5d6ff;
  --shiki-token-comment: #8b949e;
  --shiki-token-keyword: #ff7b72;
  --shiki-token-parameter: #c9d1d9;
  --shiki-token-function: #d2a8ff;
  --shiki-token-string-expression: #a5d6ff;
  --shiki-token-punctuation: #c9d1d9;
  --shiki-token-link: #000012;
}
Enter fullscreen mode Exit fullscreen mode

Bonus:
You can use colors from Tailwind CSS by using the theme function:

:root {
  --shiki-color-text: theme("colors.zinc.700");
  --shiki-color-background: theme("colors.white");
  --shiki-token-constant: theme("colors.emerald.700");
  --shiki-token-string: theme("colors.emerald.700");
  --shiki-token-comment: theme("colors.zinc.500");
  --shiki-token-keyword: theme("colors.cyan.700");
  --shiki-token-parameter: theme("colors.pink.700");
  --shiki-token-function: theme("colors.violet.700");
  --shiki-token-string-expression: theme("colors.emerald.700");
  --shiki-token-punctuation: theme("colors.zinc.800");
  --shiki-token-link: theme("colors.zinc.700");
}
Enter fullscreen mode Exit fullscreen mode

Code Hike

Ok, now we have everything together let's test it.
Most of the colors are now loaded from our variables, but not all.

background and text color not loaded

The background and the default text color are always very dark.
Why is that?
After some time of searching in the source code of Code Hike, I found the answer.
It reads some color information from the theme itself (theme.ts).
But shouldn't it then get variables instead of hard coded blackish color codes?
After a look into the Shiki theme,
I have to realize that there are no variables defined.
After another research phase, I found out that Shiki does some weird color code to variable replacement.

https://github.com/shikijs/shiki/pull/314

Shiki does this because the underlying library vscode-textmate or vscode-oniguruma returns black if the value is not a valid color.
This explains why Code Hike uses blackish color codes instead of variables.
But what if we write a variable directly into the theme instead of the color code.

Custom theme

At first we copy the css-variables theme from Shiki and
modify the values of editor.foreground and editor.background from the blackish color codes to variables.

{
  "name": "css-variables",
  "type": "css",
  "colors": {
    "editor.foreground": "var(--shiki-color-text)",
    "editor.background": "var(--shiki-color-background)",
  },
  ...
}
Enter fullscreen mode Exit fullscreen mode

Now we can load our custom theme.

import { remarkCodeHike } from "@code-hike/mdx";
import theme from "./lib/ch-theme.json" assert { type: "json" };

const options = {
  remarkPlugins: [[remarkCodeHike, { theme }]],
};

// configure mdx ...
Enter fullscreen mode Exit fullscreen mode

Great, that seems to work.
But what about tabs, if we use CH.Code?

tab colors are broken

After some research in the source code,
we add some more variables to our theme and to our css:

"colors": {
  "editor.foreground": "var(--shiki-color-text)",
  "editor.background": "var(--shiki-color-background)",

  "editorGroupHeader.tabsBackground": "var(--ch-tabs-bg)",
  "tab.activeBackground": "var(--ch-tab-active-bg)",
  "tab.inactiveForeground": "var(--ch-tab-inactive-color)",
  "tab.inactiveBackground": "var(--ch-tab-inactive-bg)",
  "icon.foreground": "var(--ch-icon-text)",

  "tab.border": "var(--ch-tab-border)",
  "tab.activeBorder": "var(--ch-tab-active-border)",
},
Enter fullscreen mode Exit fullscreen mode
:root {
  --ch-icon-text: #3f3f46;
  --ch-tabs-bg: #e4e4e7;
  --ch-tab-border: #d4d4d8;
  --ch-tab-active-border: #ffffff;
  --ch-tab-active-bg: #ffffff;
  --ch-tab-active-color: #3f3f46;
  --ch-tab-inactive-color: #71717a;
  --ch-tab-inactive-bg: #e4e4e7;
}
Enter fullscreen mode Exit fullscreen mode

But the color definition for tab.activeForeground caused problems.
Code Hike tries to convert the color from hex to rgba in order to add opacity to the color (transparent).
But our color definition (var(--ch-tab-active-color) is not a hex color code and the function throws an error.
With a bit of css we are able to workaround the problem:

div.ch-editor-tab-active {
  color: var(--ch-tab-active-color);
}
Enter fullscreen mode Exit fullscreen mode

That's it, we can now use CSS variables for the style of our syntax highlighting
and we can use different themes for light and dark mode.

Warning: I've tested not all feature of Code Hike. It is very likely that there will still be problems at one point or another.

Existing Themes

That is all nice, but how can we use existing VS Code themes with this approach (Shiki and Code Hike are using VS Code themes).
We have to read the theme and copy the color codes from the theme to our css variables.
But this is a very tedious work, so I have written a small tool that can take this task from us.

$ npx convert-sh-theme https://raw.githubusercontent.com/shikijs/shiki/main/packages/shiki/themes/dracula.json

style.css
=======================================

:root {
  --shiki-color-text: #F8F8F2;
  --shiki-color-background: #282A36;

  --shiki-token-constant: #BD93F9;
  --shiki-token-string: #8BE9FD;
  --shiki-token-comment: #6272A4;
  --shiki-token-keyword: #FF79C6;
  --shiki-token-parameter: #F8F8F2;
  --shiki-token-function: #8BE9FD;
  --shiki-token-string-expression: #FF79C6;
  --shiki-token-punctuation: #F8F8F2;
  --shiki-token-link: #8BE9FD;
}
Enter fullscreen mode Exit fullscreen mode

To create styles and the theme for Code Hike:

$ pnpx convert-sh-theme --code-hike --out output https://raw.githubusercontent.com/shikijs/shiki/main/packages/shiki/themes/dracula.json

... write file style.css
... write file theme.json
Enter fullscreen mode Exit fullscreen mode

For more information about the converter tool, have a look at convert-sh-theme.

Top comments (0)