Skip to content

Semantic Token Colors

semanticTokenColors is the newest and least understood theme layer. It provides language-aware styling powered by language servers rather than TextMate grammars. Where tokenColors pattern-matches against syntax, semanticTokenColors styles based on meaning.

TextMate grammars are powerful but limited. They parse code as text patterns — they don’t understand what the code means. Consider this JavaScript:

const name = "hello"
console.log(name)

A TextMate grammar sees name on both lines as a generic variable. It can’t tell you that the first is a declaration and the second is a reference. It can’t distinguish a readonly variable from a mutable one, or a local variable from a global one.

A language server can. It performs full semantic analysis — type checking, scope resolution, symbol tracking. Semantic tokens expose this information to the theme.

semanticTokenColors is an object (not an array like tokenColors). Each key is a semantic token selector, and values can be either a string or an object:

{
"semanticTokenColors": {
"variable.declaration": {
"foreground": "#e6e6e6",
"fontStyle": "italic"
},
"function.declaration": {
"foreground": "#56b6c2",
"fontStyle": "bold"
},
"string:escape": "#ffd93d"
}
}
FormExampleMeaning
String"#ffd93d"Shorthand — sets foreground only
Object{"foreground": "#e6e6e6", "fontStyle": "italic"}Full control over foreground and/or fontStyle

Both forms are valid. The string form is a convenience for when you only need to set the colour.

Selectors follow the pattern: type.modifier:language

ComponentRequiredExamplePurpose
typeYesvariable, function, parameterThe kind of token
.modifierNo.declaration, .readonly, .asyncRefines the type
:languageNo:javascript, :pythonTargets a specific language
TypeWhat It Targets
variableVariables
variable.declarationVariable declarations specifically
variable.readonlyConstants and readonly variables
functionFunction references
function.declarationFunction declarations
parameterFunction parameters
propertyObject properties
property.readonlyReadonly properties
classClass references
class.declarationClass declarations
interfaceInterface references
typeType references
enumEnum types
enumMemberEnum members
namespaceNamespaces/modules
stringString tokens
string:escapeEscape sequences within strings
commentComments
keywordKeywords
numberNumeric literals
operatorOperators
decoratorDecorators/annotations
macroMacros (Rust, C/C++)

Modifiers refine a token type. Multiple modifiers can be combined:

ModifierMeaning
.declarationWhere the symbol is declared
.definitionWhere the symbol is defined (similar to declaration)
.readonlyImmutable (const, final, readonly)
.staticStatic members
.asyncAsync functions/methods
.deprecatedDeprecated symbols (often paired with strikethrough)
.modificationWhere a variable is modified
.documentationDocumentation comments
.defaultLibraryStandard library symbols
{
"semanticTokenColors": {
"variable.readonly": "#e5c07b",
"function.async": { "fontStyle": "italic" },
"class.deprecated": { "fontStyle": "strikethrough" },
"function.declaration:python": { "foreground": "#61afef" }
}
}

The : separator targets specific languages. string:escape is a special case — it’s not a language filter but a well-known compound token type for escape sequences inside strings.

When a language server is active and provides semantic tokens, VS Code applies them on top of TextMate scopes. The override is per-token:

  1. VS Code first applies the matching tokenColors rule (TextMate)
  2. If a semanticTokenColors entry also matches, its properties override the TextMate result
  3. Properties not set by semanticTokenColors are kept from the tokenColors match

This means:

{
"tokenColors": [
{
"scope": "variable",
"settings": { "foreground": "#abb2bf" }
}
],
"semanticTokenColors": {
"variable.readonly": { "fontStyle": "bold" }
}
}

A readonly variable gets foreground: #abb2bf from tokenColors and fontStyle: bold from semanticTokenColors. The semantic layer adds to the TextMate base rather than replacing it entirely.

This is the single most common reason semantic token colours don’t work: custom themes must explicitly enable semantic highlighting by setting "semanticHighlighting": true at the root of the theme JSON.

{
"name": "My Theme",
"type": "dark",
"semanticHighlighting": true,
"colors": {},
"tokenColors": [],
"semanticTokenColors": {}
}

Without this flag, VS Code treats your semanticTokenColors entries as if they don’t exist. Only VS Code’s built-in themes (like “Dark+” and “Light+”) have this enabled by default. Every other theme must opt in.

The user setting editor.semanticHighlighting.enabled also plays a role. When set to configuredByTheme (the default), it defers to the theme’s semanticHighlighting property. Users can override this to true or false to force semantic highlighting on or off regardless of the theme.

In Sassy, you enable this via config.custom:

config:
name: "My Theme"
type: dark
custom:
semanticHighlighting: true

Semantic tokens require a language server that supports the textDocument/semanticTokens capability. Most popular languages have this (TypeScript, Python, Rust, Go, Java, C#, C/C++), but some don’t. For those languages, semanticTokenColors has no effect and tokenColors is all you’ve got.

When defining semanticTokenColors across imports, the merge behaviour depends on value type:

  • String values are replaced entirely by later definitions
  • Object values are deep-merged — you can override just fontStyle in an import while keeping foreground from the base

Unlike TextMate scopes (which you can inspect with Developer: Inspect Editor Tokens and Scopes), semantic tokens are harder to discover. The same inspector command shows semantic token information when available — look for the “semantic token type” and “semantic token modifiers” fields in the inspector popup.

Just like with tokenColors, setting fontStyle to "" (empty string) explicitly clears all font styling. This is useful when you want semantic tokens to strip italic or bold from a TextMate base rule.

Sassy maps semanticTokenColors under theme.semanticTokenColors. Both value forms are supported:

# Raw VS Code JSON
# "semanticTokenColors": {
# "variable.declaration": {
# "foreground": "#e6e6e6",
# "fontStyle": "italic"
# },
# "string:escape": "#ffd93d"
# }
# Sassy equivalent
theme:
semanticTokenColors:
variable.declaration:
foreground: $(std.fg)
fontStyle: italic
function.declaration:
foreground: $(accent)
fontStyle: bold
"string:escape": $$yellow

Variable references, palette aliases, and colour functions work in both the string form and inside object properties. Sassy resolves everything to hex and writes the standard VS Code structure.