Skip to main content

Where a theme lives

A theme is a folder with two files: its manifest.json and a bundle.json that contains the definition below. The bundle’s filename is fixed. The loader looks for exactly bundle.json next to the manifest. It skips the theme if the file is missing or doesn’t parse into a JSON object.
Unlike plugins, themes ignore the manifest’s main field. The field is still required for the manifest to validate, but nothing reads it when locating the bundle. If your colors aren’t applying, check that the file is named bundle.json, not whatever main points to.

The shape of a theme

A theme definition has three parts that set its colors: a base type, semantic colors, and raw colors. Together they override Discord’s palette.
bundle.json

Base type

type sets the base palette your theme builds on. Override only the colors you want to change. Everything else falls back to this base.
A pure-black base, for OLED/AMOLED-style themes.

Semantic colors

Semantic colors are Discord’s named color roles, such as BACKGROUND_PRIMARY and TEXT_NORMAL. Setting a role recolors every place that role is used, so the result stays consistent across the app. Each key is the selector name of the color Unbound hooks. A key that doesn’t match any selector does nothing. It’s never looked up, and you get no warning. Each entry is an object with a type (color for a literal color, or raw to reference a raw color), a value, and an optional opacity.
'color' | 'raw'
color is a literal value. raw points to one of your raw colors, so several roles can share it.
string
The color, or the raw key when type is raw.
number
Optional opacity from 0 to 1, applied on top of the parsed color. It replaces any alpha already in the color string. It’s ignored when type is raw.
If an entry is missing type or value, it’s skipped and the role keeps its original color.

Raw colors

Raw colors are a flat map keyed by the selector name of a system color, such as systemBlueColor. Setting one replaces that color everywhere the app uses it, below the level of Discord’s roles. Semantic roles that point to a raw color with type: 'raw' all share its value.
Raw overrides are swapped in when the theme is applied and restored when you switch to another theme, so only the applied theme’s raw colors are in effect.
Put your brand colors in raw and point semantic roles at them. This keeps the theme DRY: one edit recolors everything that shares that value.

Color formats

Every color string, whether in raw or in a semantic color entry, is parsed the same way. These formats are accepted:
hex
Six-digit hex. Alpha is treated as fully opaque.
hex
Eight-digit hex, with alpha as the final byte (00 to FF).
function
Three channels, each 0 to 255. Fully opaque.
function
Three channels 0 to 255, plus alpha.
In rgba, alpha is a fraction from 0 to 1, while r, g, and b go from 0 to 255. rgba(255, 0, 0, 255) does not give you opaque red. A 0 alpha renders the color fully transparent without any error. For opaque red, use rgba(255, 0, 0, 1).
These strings fail to parse:
  • An rgba string with fewer than four values.
  • An rgb string with fewer than three values.
  • Any string that doesn’t start with #, rgb, or rgba.
Named CSS colors like red are not supported.
A color that fails to parse doesn’t show an error. Unbound falls back to the original color for that entry and the app keeps rendering, so a malformed value looks like an override that didn’t apply. When a color doesn’t change, check its format first.
See Creating an Addon for building and installing a theme, and The Manifest for the metadata that ships beside the bundle.