Skip to main content

Themes

Extensions can contribute custom color themes that control the appearance of the entire application, the code editor, and syntax highlighting.

Overview

A theme contribution defines a complete visual style for the app. Each theme targets three distinct layers:

  • App colors -- the chrome surrounding the editor (backgrounds, surfaces, borders, text, status indicators).
  • Editor colors -- the CodeMirror editor pane (editor background, foreground, caret, selection, gutter).
  • Token colors -- syntax highlighting for code (keywords, strings, comments, types, and more).

Themes are declared in your extension's manifest.json under the contributes.themes array. They are registered automatically at extension load time and appear in the built-in theme picker alongside the default Dark and Light themes.

Theme structure

Every theme contribution must include these required fields:

FieldTypeDescription
idStringA unique identifier for the theme (e.g. "my-dark-theme").
labelStringThe display name shown in the theme picker (e.g. "My Dark Theme").
typeStringMust be "dark" or "light". Determines the base brightness and default fallback colors. Any other value causes a validation error.

And these optional color maps:

FieldTypeDescription
appColorsMap<String, String>?Hex color overrides for app-level UI elements.
editorColorsMap<String, String>?Hex color overrides for the CodeMirror editor pane.
tokenColorsMap<String, String>?Hex color overrides for syntax token highlighting.

The extensionId field is set automatically from the extension manifest -- you do not declare it.

All color values are CSS hex strings (#rrggbb or #rrggbbaa). Shorthand three-character hex (#rgb) is also supported.

App colors

The appColors map controls the application shell: navigation, panels, dialogs, buttons, and status indicators. Every key is optional -- omitted keys fall back to the built-in defaults for the theme's type (dark or light).

Available keys

Layout

KeyDescriptionDark defaultLight default
backgroundMain background color#1e1e1e#ffffff
surfaceElevated surfaces (cards, panels, dialogs)#252526#f3f3f3
borderBorders and dividers#333333#d4d4d4

Accent

KeyDescriptionDark defaultLight default
primaryPrimary accent (buttons, links, active tabs)#569cd6#0066b8
secondarySecondary accent#4ec9b0#267f99
accentTertiary accent#c586c0#af00db
positivePositive/affirmative accent#6a9955#008000
highlightHighlight accent#d7ba7d#795e26
warmWarm accent#ce9178#a31515

Text

KeyDescriptionDark defaultLight default
textPrimary text#d4d4d4#333333
textMutedSecondary/muted text#9e9e9e#616161
textFaintDisabled/faint text#6d6d6d#888888

Status

KeyDescriptionDark defaultLight default
errorError indicators#f14c4c#e51400
warningWarning indicators#cca700#bf8803
successSuccess indicators#89d185#388a34

Editor (via appColors)

KeyDescriptionDark defaultLight default
selectionText selection highlight#2f8cea84#2f8cea84
cursorCursor/caret color#aeafad#000000
lineNumberLine number gutter text#858585#237893

Example

{
"appColors": {
"background": "#1a1a2e",
"surface": "#16213e",
"primary": "#e94560",
"text": "#eaeaea",
"textMuted": "#8888aa",
"error": "#ff6b6b"
}
}

Editor colors

The editorColors map targets the CodeMirror code editor pane specifically. These values are passed directly to the CodeMirror createTheme() configuration as the settings block.

Available keys

KeyDescription
backgroundEditor background
foregroundDefault text color in the editor
caretCursor/caret color
selectionSelected text highlight
gutterForegroundLine number color in the gutter

Cross-layer derivation

If you provide only appColors without editorColors, the editor colors are automatically derived from the corresponding app color keys:

Editor keyDerived from app key
backgroundbackground
foregroundtext
caretcursor
selectionselection
gutterForegroundlineNumber

Conversely, if you provide only editorColors without appColors, certain app colors are derived from the editor colors:

App keyDerived from editor key
backgroundbackground
textforeground
cursorcaret
selectionselection
lineNumbergutterForeground

If both maps are provided, each is applied independently with no derivation.

Example

{
"editorColors": {
"background": "#0f0f1a",
"foreground": "#d4d4d4",
"caret": "#e94560",
"selection": "#e9456033",
"gutterForeground": "#4a4a6a"
}
}

Token colors

The tokenColors map controls syntax highlighting in the code editor. Each key corresponds to a CodeMirror syntax tag. Omitted keys fall back to brightness-based defaults.

Available keys

KeyDescriptionDark defaultLight default
keywordLanguage keywords (if, return, class)#569cd6#0000ff
stringString literals#d7ba7d#a31515
commentComments#6a9955#008000
numberNumeric literals#b5cea8#098658
typeNameType names and annotations#4ec9b0#267f99
functionFunction and method names#dcdcaa#795e26
variableNameVariable names#9cdcfe#001080
specialSpecial tokens (decorators, macros)#c586c0#af00db

Example

{
"tokenColors": {
"keyword": "#e94560",
"string": "#0f3460",
"comment": "#4a4a6a",
"number": "#e94560",
"typeName": "#4ec9b0",
"function": "#dcdcaa",
"variableName": "#9cdcfe",
"special": "#c586c0"
}
}

Dark vs. light themes

The type field determines the base brightness of your theme and has two important effects:

  1. Fallback colors. Any color key you omit from appColors, editorColors, or tokenColors is filled from the built-in dark or light defaults listed in the tables above.
  2. System integration. The brightness value propagates to the Flutter ThemeData, which affects system UI elements such as the status bar style, keyboard appearance, and dialog defaults.

The type field is validated at load time. If the value is anything other than "dark" or "light", the theme is rejected and not registered. An error is logged to the extension console.

Manifest example

Below is a complete manifest.json for a theme extension that contributes both a dark and a light variant:

{
"id": "ocean-themes",
"name": "Ocean Themes",
"version": "1.0.0",
"description": "Dark and light ocean-inspired color themes",
"author": "Theme Author",
"contributes": {
"themes": [
{
"id": "ocean-dark",
"label": "Ocean Dark",
"type": "dark",
"appColors": {
"background": "#1a1a2e",
"surface": "#16213e",
"border": "#0f3460",
"primary": "#e94560",
"secondary": "#4ec9b0",
"text": "#eaeaea",
"textMuted": "#8888aa",
"textFaint": "#555577"
},
"editorColors": {
"background": "#0f0f1a",
"foreground": "#d4d4d4",
"caret": "#e94560",
"selection": "#e9456033",
"gutterForeground": "#4a4a6a"
},
"tokenColors": {
"keyword": "#e94560",
"string": "#ce9178",
"comment": "#4a4a6a",
"number": "#b5cea8",
"typeName": "#4ec9b0",
"function": "#dcdcaa",
"variableName": "#9cdcfe",
"special": "#c586c0"
}
},
{
"id": "ocean-light",
"label": "Ocean Light",
"type": "light",
"appColors": {
"background": "#f0f5ff",
"surface": "#e0eaff",
"border": "#b0c4de",
"primary": "#0066b8",
"text": "#1a1a2e"
},
"tokenColors": {
"keyword": "#0000ff",
"string": "#a31515",
"comment": "#008000"
}
}
]
}
}

A single extension can contribute multiple themes. Each theme entry in the array is registered independently.

Minimal theme example

You do not need to specify all three color maps. A theme can override only what it needs. For example, a theme that only customizes syntax highlighting:

{
"id": "syntax-only-theme",
"name": "Custom Syntax",
"version": "1.0.0",
"contributes": {
"themes": [
{
"id": "custom-syntax",
"label": "Custom Syntax Colors",
"type": "dark",
"tokenColors": {
"keyword": "#ff79c6",
"string": "#f1fa8c",
"comment": "#6272a4"
}
}
]
}
}

All app and editor colors will use the built-in dark defaults.

Reading the current theme

Extensions can read the currently active theme and listen for theme changes at runtime. This is useful for extensions that render custom UI (popups, webviews, bottom sheets) and need to match the app's color scheme.

Get current theme

import editor from "@srcnexus/ext-sdk";

const theme = await editor.workspace.getTheme();

console.log(theme.id); // "dark"
console.log(theme.type); // "dark" or "light"
console.log(theme.appColors.background); // "#1e1e1e"
console.log(theme.editorColors.caret); // "#aeafad"
console.log(theme.tokenColors.keyword); // "#569cd6"

The returned object contains the full resolved theme with all three color layers:

FieldTypeDescription
idstringTheme identifier (e.g. "dark", "ocean-dark")
labelstringDisplay name (e.g. "Dark (Default)")
type"dark" | "light"Theme brightness
appColorsobjectAll app UI colors as hex strings (see App colors)
editorColorsobjectEditor pane colors as hex strings (see Editor colors)
tokenColorsobjectSyntax token colors as hex strings (see Token colors)

Listen for theme changes

import editor from "@srcnexus/ext-sdk";

const unsubscribe = editor.events.onThemeChange((theme) => {
// theme has the same shape as workspace.getTheme() result
console.log("Theme changed to:", theme.label);
updateMyUI(theme.appColors);
});

// Later, to stop listening:
unsubscribe();

The onThemeChange event fires whenever the user switches to a different theme. The handler receives the full theme object, identical in shape to what workspace.getTheme() returns.

No special permission is required to read theme data.

Theme picker API

Extensions can programmatically open the theme picker using the window.showThemePicker RPC method. This opens the same theme selection dialog available via the command palette.

SDK usage

await editor.window.showThemePicker();

No parameters are required. The method opens a picker dialog listing all available themes (built-in and extension-contributed) with the currently active theme pre-selected. The user selects a theme and the selection is applied immediately and persisted to the workbench.theme setting.

The method returns when the user makes a selection or dismisses the dialog.

Theme selection command

The theme picker can also be triggered via the theme.select command:

// Open the picker dialog
await rpc.call("commands.execute", { id: "theme.select" });

// Or apply a specific theme directly by ID
await rpc.call("commands.execute", { id: "theme.select", args: "ocean-dark" });

When a string argument is passed, the theme is applied directly without showing the picker.

Terminal theme integration

Theme colors are also applied to the integrated terminal. The terminal theme is automatically derived from the app color tokens:

Terminal elementDerived from
Foregroundtext
Backgroundbackground
Cursorcursor
Selectionselection
Rederror
Greenpositive
Yellowwarning
Blueprimary
Magentaaccent
Cyansecondary

Bright ANSI color variants are computed automatically by adjusting the lightness or saturation of the base color.

Theme persistence

When the user selects a theme (whether built-in or extension-contributed), the theme ID is stored in the workbench.theme setting. On the next app launch, the persisted theme is reapplied after all extension themes have been registered. If a previously selected extension theme is no longer available (the extension was uninstalled), the app falls back to the default Dark theme.

Lifecycle

  1. The extension host parses the contributes.themes array from manifest.json.
  2. Each theme entry is validated (the type field must be "dark" or "light").
  3. Valid themes are merged with the built-in base colors and registered with the editor.
  4. Themes become available in the theme picker.
  5. After all extensions finish bootstrapping, the persisted theme preference is reapplied.
  6. When an extension is uninstalled or disabled, its themes are unregistered. If the active theme belonged to that extension, the app reverts to the Dark theme.