Frontend Development 15 min read

How to Build a VSCode‑Compatible Theme Service for Your Custom IDE

This article explains how to design and implement a theme service for a VSCode‑like IDE, covering color contributions, theme participants, CSS variable handling, and integration of VSCode Material Theme into the Monaco editor.

Taobao Frontend Technology
Taobao Frontend Technology
Taobao Frontend Technology
How to Build a VSCode‑Compatible Theme Service for Your Custom IDE

Background Introduction

This year I joined the IDE co‑development project, responsible for designing and implementing the theme service. When we think of a theme service, VSCode’s rich theme ecosystem immediately comes to mind. VSCode’s massive extension marketplace includes theme extensions that let users customize every UI component via a list of color keys, enabling personalized visual experiences.

The IDE we are building, referred to as Kaitan IDE, has a layout and components similar to VSCode, so it can largely reuse VSCode’s color keys. The structural comparison between Kaitan IDE and VSCode is shown below.

Figure: VSCode layout vs. Kaitan IDE layout

Beyond functional components, an IDE’s core capability is its editor. We chose VSCode’s built‑in editor, Monaco. Because Monaco is an independent editor built from the VSCode project, its themes can be applied directly after a few simple rule conversions.

Figure: VSCode theme information (left) vs. Monaco theme information (right)

The final compatible theme plugin implementation looks like this:

Figure: Current demo

VSCode Theme Service

VSCode’s theme service provides foreground and background replacement for component areas, as well as token colors and font style definitions for the editor. With a simple configuration, a theme extension can customize virtually any VSCode UI component. Developing a theme extension requires implementing the

themes

or

colors

contribution points.

“colors” contribution adds a new color value or overrides an existing one.
<code>"contributes": {
  "colors": [{
    "id": "superstatus.error",
    "description": "Color for error message in the status bar.",
    "defaults": {
      "dark": "errorForeground",
      "light": "errorForeground",
      "highContrast": "#010203"
    }
  }]
}
</code>
“themes” contribution adds a new theme.
<code>"contributes": {
    "themes": [{
        "label": "Monokai",
        "uiTheme": "vs-dark",
        "path": "./themes/Monokai.tmTheme"
    }]
}
</code>

We start with the basic

colors

contribution to see how VSCode implements its theme service.

colors contribution

Implementing a color contribution requires a ColorRegistry to maintain the mapping between color IDs and their values, and a rendering strategy to apply those values to the view layer.

Figure: VSCode color contribution class diagram

The implementation distinguishes two scenarios: static style declarations and dynamic color usage.

For static declarations, VSCode defines an IThemeParticipant interface:

<code>export interface ICssStyleCollector {
    addRule(rule: string): void;
}
export interface IThemingParticipant {
        (theme: ITheme, collector: ICssStyleCollector, environment: IEnvironmentService): void;
}
</code>

The theme service registers a participant that describes how to convert theme colors into CSS rules:

<code>registerThemingParticipant((theme, collector) => {
    const lineHighlight = theme.getColor(editorLineHighlight);
    if (lineHighlight) {
      collector.addRule(`.MONACO-editor .margin-view-overlays .current-line-margin { background-color: ${lineHighlight}; border: none; }`);
    }
});
</code>

When

monaco.setTheme

is called, all participants are invoked to regenerate the CSS and append it to the document head.

For dynamic usage, components retrieve colors directly from the

ColorRegistry

. VSCode provides a Themable base class that handles color filtering and theme‑switching logic.

Figure: Themable base class

The overall flow from plugin color data to rendered CSS is illustrated below:

Figure: VSCode color contribution declaration to application

themes contribution

The

colorRegistry

implementation underpins the theme service; the

themes

contribution builds on it to manage theme‑level color data.

A theme consists of two parts: tokenColors for Monaco editor styling and colors for IDE UI components.

The

colors

section can be seen as bulk registration of color contributions, while

tokenColors

are applied to Monaco as shown next.

Monaco’s theme type is defined as:

<code>export interface IStandaloneThemeData {
  base: BuiltinTheme;
  inherit: boolean;
  rules: ITokenThemeRule[];
  encodedTokensColors?: string[];
  colors: IColors;
}
</code>

The

base

property can be

'vs'

,

'vs-dark'

, or

'hc-black'

(light, dark, high‑contrast). The

inherit

flag indicates whether the theme extends a base theme. The

colors

map defines colors for built‑in Monaco UI components, while

rules

map token scopes to foreground colors and font styles.

Token colors can be defined in JSON:

<code>{
  "tokenColors": [{
    "name": "coloring of the Java import and package identifiers",
    "scope": ["storage.modifier.import.java", "variable.language.wildcard.java", "storage.modifier.package.java"],
    "settings": {"foreground": "#d4d4d4"}
  }]
}
</code>

or in TextMate plist format:

<code>&lt;dict&gt;
  &lt;key&gt;name&lt;/key&gt; &lt;string&gt;Comment&lt;/string&gt;
  &lt;key&gt;scope&lt;/key&gt; &lt;string&gt;comment&lt;/string&gt;
  &lt;key&gt;settings&lt;/key&gt; &lt;dict&gt;
    &lt;key&gt;foreground&lt;/key&gt; &lt;string&gt;#75715E&lt;/string&gt;
  &lt;/dict&gt;
&lt;/dict&gt;
</code>

Flattening the

scope + settings

rules yields a simple array that Monaco can consume:

<code>rules: [
  { token: 'comment', foreground: 'ffa500', fontStyle: 'italic underline' },
  { token: 'comment.js', foreground: '008800', fontStyle: 'bold' },
  { token: 'comment.css', foreground: '0000ff' }
]
</code>
Details on how colors are applied to parsed tokens will be covered in a language‑specific article.

Kaitan IDE Design and Implementation

Kaitan IDE’s theme service is largely similar to VSCode’s, but static style application uses CSS variables instead of the

ThemeParticipant + CssCollector

approach. For example, the color key

input.background

becomes the CSS variable

--input-background

, which is injected into the page head. Component developers simply reference the variable in their CSS, without needing to know about the theme service or handle theme switches, reducing compatibility effort.

The simplified sequence diagram for Kaitan IDE’s theme service is shown below:

How Material Theme Is Applied to the IDE

Using the popular Material Theme extension as an example, the IDE reads the

colors

and

themes

contributions declared in the extension’s

package.json

. The

colors

contributions are registered in a global

colorRegistry

. The

themes

contribution is loaded into memory as a

ThemeData

object.

<code>// Material Theme package.json
"name": "vsc-material-theme",
"themes": [
  { "label": "Material Theme", "path": "./out/themes/Material-Theme-Default.json", "uiTheme": "vs-dark" },
  { "label": "Material Theme High Contrast", "path": "./out/themes/Material-Theme-Default-High-Contrast.json", "uiTheme": "vs-dark" }
]
</code>

Each theme receives a unique ID, e.g.,

vs-dark vsc-material-theme-out-themes-material_theme_default-json

. The actual JSON file is read asynchronously during the IDE’s

onStart

lifecycle, producing a

ThemeData

instance:

<code>export interface IThemeData {
  id: string;
  name: string;
  colors: IColors;
  encodedTokensColors: string[];
  rules: ITokenThemeRule[];
  base: BuiltinTheme;
  inherit: boolean;
  initializeFromData(data): void;
  initializeThemeData(id, name, themeLocation: string): Promise<void>;
}
</code>

Figure: Material Theme configuration loaded into ThemeData

The conversion from VSCode theme data to Monaco is performed by code such as:

<code>// Convert color hex strings to Color objects
for (let colorId in colors) {
  const colorHex = colors[colorId];
  if (typeof colorHex === 'string') {
    resultColors[colorId] = Color.fromHex(colors[colorId]);
  }
}
// Expand tokenColors
const settings = Object.keys(tokenColor.settings).reduce((previous, current) => {
  let value = tokenColor.settings[current];
  if (typeof value === typeof '') {
    value = value.replace(/^\#/, '').slice(0, 6);
  }
  previous[current] = value;
  return previous;
}, {});
this.rules.push({ ...settings, token: scope });
</code>

After loading, the

colors

part is transformed into CSS variables (e.g.,

list.hoverBackground → --list-hoverBackground

) and appended to the page head. Components use these variables directly. The

ThemeData

object also calls

monaco.defineTheme

to apply

tokenColors

and UI colors to Monaco.

With these steps, the Material Theme is fully functional in the IDE.

Figure: Material Theme high‑contrast effect

How IDE Plugins Can Use the Theme

If a plugin is implemented via a Webview, simply use the CSS variables that correspond to VSCode theme colors. For logic‑level access, plugins can call the

getColor

API to retrieve a color value by its key.

frontendVSCodeCSS variablesIDE themingMonacotheme extension
Taobao Frontend Technology
Written by

Taobao Frontend Technology

The frontend landscape is constantly evolving, with rapid innovations across familiar languages. Like us, your understanding of the frontend is continually refreshed. Join us on Taobao, a vibrant, all‑encompassing platform, to uncover limitless potential.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.