I spent an embarrassingly long time tweaking my editor colors before it occurred to me that I might as well turn the whole thing into a proper extension and ship it. Making a VS Code theme is one of those projects that looks intimidating — JSON files, TextMate scopes, token grammars — but turns out to be more satisfying than complex. This is how I actually did it.
If you have ever found yourself copy-pasting hex values into settings.json just to get one string color to look right, you have already done the hardest part of building a theme. The rest is structure.
Why bother publishing?
The honest answer is that I wanted to stop syncing a giant settings.json between machines. Packaging a theme as an extension lets you install it anywhere in two clicks, version it with git, and share it with others who might like the same aesthetic. The Marketplace has hundreds of themes, but almost none of them match your exact taste — because your taste is yours.
Scaffolding the project
VS Code's official generator handles the boilerplate. You need Node.js and a global install of Yeoman plus the VS Code extension generator:
npm install -g yo generator-code
yo code
Pick New Color Theme when prompted. The generator asks whether you want to start from scratch or import an existing theme (useful if you are riffing on something like One Dark). Choose your starting point, give the theme a name, and let it scaffold. You end up with a folder that looks roughly like this:
my-theme/
themes/
my-theme-color-theme.json
package.json
README.md
CHANGELOG.md
.vscodeignore
The only file you will spend real time in is the JSON inside themes/. Everything else is metadata.
Understanding what you are actually editing
A VS Code color theme JSON has two main sections: colors and tokenColors.
colors controls the workbench — the editor background, sidebar, status bar, activity bar, panel borders, input boxes, all of it. These keys are named editor.background, sideBar.background, statusBar.background, and so on. You will spend a lot of time here because these are what make a theme feel cohesive even before you open a file.
tokenColors controls syntax highlighting. This is where TextMate scopes come in. Every token in your code has one or more scope labels, and your theme maps those labels to colors and font styles. A typical rule looks like this:
{
"scope": ["string", "string.quoted"],
"settings": {
"foreground": "#a8e6cf",
"fontStyle": ""
}
}
The scope names are dotted strings like keyword.control, variable.parameter, or entity.name.function. To see what scope any token under your cursor has, open the Command Palette and run Developer: Inspect Editor Tokens and Scopes. This is the single most useful tool while building a theme — you click a piece of code, see the exact scope path, and know exactly what to target.
Common scopes worth knowing
You do not need to memorize hundreds of scope names. Getting these ten right covers 90 percent of what people notice:
comment— all commentskeyword— language keywords likeif,return,constkeyword.operator—=,+,===string— string literalsconstant.numeric— numbersentity.name.function— function names at definitionvariable.parameter— function parametersentity.name.type— class and type namessupport.function— built-in functions likeconsole.logmarkup.heading— Markdown headings
My actual process for picking colors
I started with a base palette of six colors before touching the JSON at all. I picked them in a tool that showed me contrast ratios against my intended background. If a color does not pass at least 4.5:1 contrast against the background, it is too dim to read comfortably for long sessions — that threshold matters more than whether the palette looks pretty in isolation.
Then I assigned roles: one color for strings (warm), one for keywords (cool), one for functions (neutral but distinct), one for comments (muted, intentionally lower contrast), one for types, one for operators. Having the palette fixed before you write a single scope rule keeps the theme from drifting into muddy territory where everything is a slightly different shade of the same hue.
From there I opened a real project I work in daily — a mix of TypeScript, CSS, and Markdown — and iterated with the extension host window open. Press F5 in VS Code while inside your theme folder, and a new window opens with your theme active. Every time you save the JSON, reload the window and you see the change immediately.
Semantic highlighting
VS Code has a second layer on top of TextMate scopes called semantic highlighting. Some language servers (TypeScript's is the most complete) emit semantic tokens that are more accurate than what a regex-based TextMate grammar can produce — they can distinguish, say, a local variable from a parameter, or a readonly property from a mutable one. You opt into it in your theme:
{
"name": "My Theme",
"type": "dark",
"semanticHighlighting": true,
"semanticTokenColors": {
"parameter": "#f0a070",
"variable.readonly": "#c3e8a0"
},
"colors": { ... },
"tokenColors": [ ... ]
}
If you set semanticHighlighting to true without any semanticTokenColors rules, VS Code falls back to the TextMate mappings automatically. So you can enable it and only add semantic rules where you want to override the TextMate behavior. I found it was worth adding at minimum a rule for parameter because TypeScript's semantic token for function parameters is much more precise than variable.parameter in the TextMate grammar.
Packaging and publishing
Before you publish, the package.json in your extension root needs a few things filled in correctly:
{
"name": "my-theme",
"displayName": "My Theme",
"description": "A calm dark theme with warm string highlights.",
"version": "1.0.0",
"publisher": "your-publisher-id",
"engines": { "vscode": "^1.90.0" },
"categories": ["Themes"],
"contributes": {
"themes": [{
"label": "My Theme",
"uiTheme": "vs-dark",
"path": "./themes/my-theme-color-theme.json"
}]
},
"icon": "icon.png",
"galleryBanner": {
"color": "#1a1a2e",
"theme": "dark"
}
}
The publisher field must match a publisher ID you create at marketplace.visualstudio.com/manage. You will need a Microsoft account and an Azure DevOps organization to generate a Personal Access Token with Marketplace publish scope. The vsce CLI handles the rest:
npm install -g @vscode/vsce
vsce login your-publisher-id
vsce publish
The first publish creates the listing. Subsequent updates bump the version in package.json — you can do that manually or let vsce handle it with vsce publish minor. After publish, the extension usually appears in the Marketplace within a few minutes, though search indexing can take a bit longer.
A few things I wish I had known earlier
Your .vscodeignore matters. By default, vsce packages everything in your folder. Exclude anything that is not needed at runtime — source control folders, large screenshots, build artifacts. A bloated extension is not a dealbreaker, but it is sloppy.
Test in both light and dark contexts. If you are shipping a dark theme, try it in a meeting or a bright room. What looks fine on your monitor at night can be genuinely hard to read in daylight. The reverse applies to light themes.
Write a real README. The Marketplace page is just your README.md, and most theme pages are thin. A screenshot, a brief description of the intended mood, and a note about which language you optimized for will put yours above most of the competition.
Version discipline pays off. Even for a theme, using semantic versioning builds trust. Patch for small color tweaks, minor for adding a new token rule, major for a full palette overhaul. Users who like your theme will appreciate not having a jarring change land without warning.
What the process actually costs
From first yo code run to a published listing took me an afternoon, maybe four hours including the time spent staring at the same purple trying to decide if it was too saturated. The tooling is solid and the feedback loop — save JSON, reload window, see result — is fast enough that iteration never feels like waiting. If you have a palette you like and some opinions about which parts of code deserve visual emphasis, building a VS Code theme is well within a weekend project. Ship it even if it feels unfinished. You can iterate in public, and someone else will probably find it useful.