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 themes or colors contribution points.
“colors” contribution adds a new color value or overrides an existing one.
"contributes": {
"colors": [{
"id": "superstatus.error",
"description": "Color for error message in the status bar.",
"defaults": {
"dark": "errorForeground",
"light": "errorForeground",
"highContrast": "#010203"
}
}]
}“themes” contribution adds a new theme.
"contributes": {
"themes": [{
"label": "Monokai",
"uiTheme": "vs-dark",
"path": "./themes/Monokai.tmTheme"
}]
}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:
export interface ICssStyleCollector {
addRule(rule: string): void;
}
export interface IThemingParticipant {
(theme: ITheme, collector: ICssStyleCollector, environment: IEnvironmentService): void;
}The theme service registers a participant that describes how to convert theme colors into CSS rules:
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; }`);
}
});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:
export interface IStandaloneThemeData {
base: BuiltinTheme;
inherit: boolean;
rules: ITokenThemeRule[];
encodedTokensColors?: string[];
colors: IColors;
}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:
{
"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"}
}]
}or in TextMate plist format:
<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>Flattening the scope + settings rules yields a simple array that Monaco can consume:
rules: [
{ token: 'comment', foreground: 'ffa500', fontStyle: 'italic underline' },
{ token: 'comment.js', foreground: '008800', fontStyle: 'bold' },
{ token: 'comment.css', foreground: '0000ff' }
]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.
// 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" }
]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:
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>;
}Figure: Material Theme configuration loaded into ThemeData
The conversion from VSCode theme data to Monaco is performed by code such as:
// 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 });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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
