Codemirror Themes Using CSS Variables

Context

I wanted to a multi-theme setup for the code editor I'm using in fflags.com as it has a dark and light theme. I'm using codemirror for the code editor and it has a rich set of themes here. But, turns out you need JavaScript to switch themes in codemirror. I already have a theme switcher in the app and wanted to avoid writing more JavaScript for this.

Solution

I digged into the codemirror themes and found instead of hard coding the colors, you can hard code the CSS variables and use them in the themes. This way, you can switch themes by changing the CSS variables.

I picked the code from codemirror github theme and modified it like so:

import { tags as t } from "@lezer/highlight";
import { createTheme, type CreateThemeOptions } from "@uiw/codemirror-themes";

export const defaultSettings: CreateThemeOptions["settings"] = {
  background: "var(--cm_bg, #0d1117)",
  foreground: "var(--cm_fg, #c9d1d9)",
  caret: "var(--cm_caret, #c9d1d9)",
  selection: "var(--cm_selection, #003d73)",
  selectionMatch: "var(--cm_selectionMatch, #003d73)",
  lineHighlight: "var(--cm_lineHighlight, #36334280)",
  gutterBackground: "var(--cm_gutterBg, #0d1117)",
  gutterForeground: "var(--cm_gutterFg, #6e7681)",
};

export const style: CreateThemeOptions["styles"] = [
  { tag: [t.standard(t.tagName), t.tagName], color: "var(--cm_tag, #7ee787)" },
  { tag: [t.comment, t.bracket], color: "var(--cm_comment, #8b949e)" },
  { tag: [t.className, t.propertyName], color: "var(--cm_property, #d2a8ff)" },
  {
    tag: [t.variableName, t.attributeName, t.number, t.operator],
    color: "var(--cm_variable, #79c0ff)",
  },
  {
    tag: [t.keyword, t.typeName, t.typeOperator, t.typeName],
    color: "var(--cm_keyword, #ff7b72)",
  },
  { tag: [t.string, t.meta, t.regexp], color: "var(--cm_string, #a5d6ff)" },
  { tag: [t.name, t.quote], color: "var(--cm_name, #7ee787)" },
  {
    tag: [t.heading, t.strong],
    color: "var(--cm_heading, #d2a8ff)",
    fontWeight: "bold",
  },
  {
    tag: [t.emphasis],
    color: "var(--cm_emphasis, #d2a8ff)",
    fontStyle: "italic",
  },
  {
    tag: [t.deleted],
    color: "var(--cm_deleted, #ffdcd7)",
    backgroundColor: "var(--cm_deletedBg, #ffeef0)",
  },
  {
    tag: [t.atom, t.bool, t.special(t.variableName)],
    color: "var(--cm_atom, #ffab70)",
  },
  { tag: t.link, textDecoration: "underline" },
  { tag: t.strikethrough, textDecoration: "line-through" },
  { tag: t.invalid, color: "var(--cm_invalid, #f97583)" },
];

export const theme: ReturnType<typeof createTheme> = createTheme({
  theme: "light",
  settings: defaultSettings,
  styles: style,
});

Now, in my CSS, I can define the CSS variables like so:

:root {
  --cm_bg: #fff;
  --cm_fg: #24292e;
  --cm_caret: #24292e;
  --cm_selection: #bbdfff;
  --cm_selectionMatch: #bbdfff;
  --cm_lineHighlight: #f6f8fa;
  --cm_gutterBg: #fff;
  --cm_gutterFg: #6e7781;
  --cm_tag: #116329;
  --cm_comment: #6a737d;
  --cm_property: #6f42c1;
  --cm_variable: #005cc5;
  --cm_keyword: #d73a49;
  --cm_string: #032f62;
  --cm_name: #22863a;
  --cm_heading: #24292e;
  --cm_emphasis: #24292e;
  --cm_deleted: #b31d28;
  --cm_deletedBg: #ffeef0;
  --cm_atom: #e36209;
  --cm_link: #032f62;
  --cm_invalid: #cb2431;
}

.dark {
  --cm_bg: #0d1117;
  --cm_fg: #c9d1d9;
  --cm_caret: #c9d1d9;
  --cm_selection: #003d73;
  --cm_selectionMatch: #003d73;
  --cm_lineHighlight: #36334280;
  --cm_gutterBg: #0d1117;
  --cm_gutterFg: #6e7681;
  --cm_tag: #7ee787;
  --cm_comment: #8b949e;
  --cm_property: #d2a8ff;
  --cm_variable: #79c0ff;
  --cm_keyword: #ff7b72;
  --cm_string: #a5d6ff;
  --cm_name: #7ee787;
  --cm_heading: #d2a8ff;
  --cm_emphasis: #d2a8ff;
  --cm_deleted: #ffdcd7;
  --cm_deletedBg: #ffeef0;
  --cm_atom: #ffab70;
  --cm_link: #a5d6ff;
  --cm_invalid: #f97583;
}

Now, I can switch themes by adding the dark class to html element which I'm already doing for tailwind.