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.
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
themesor
colorscontribution 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
colorscontribution 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.setThemeis 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
colorRegistryimplementation underpins the theme service; the
themescontribution 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
colorssection can be seen as bulk registration of color contributions, while
tokenColorsare 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
baseproperty can be
'vs',
'vs-dark', or
'hc-black'(light, dark, high‑contrast). The
inheritflag indicates whether the theme extends a base theme. The
colorsmap defines colors for built‑in Monaco UI components, while
rulesmap 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><dict>
<key>name</key> <string>Comment</string>
<key>scope</key> <string>comment</string>
<key>settings</key> <dict>
<key>foreground</key> <string>#75715E</string>
</dict>
</dict>
</code>Flattening the
scope + settingsrules 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 + CssCollectorapproach. For example, the color key
input.backgroundbecomes 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
colorsand
themescontributions declared in the extension’s
package.json. The
colorscontributions are registered in a global
colorRegistry. The
themescontribution is loaded into memory as a
ThemeDataobject.
<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
onStartlifecycle, producing a
ThemeDatainstance:
<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
colorspart is transformed into CSS variables (e.g.,
list.hoverBackground → --list-hoverBackground) and appended to the page head. Components use these variables directly. The
ThemeDataobject also calls
monaco.defineThemeto apply
tokenColorsand 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
getColorAPI to retrieve a color value by its key.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.