Where a theme lives
A theme is a folder holding two files: itsmanifest.json and a bundle.json holding the definition below. The bundle’s name is fixed. The loader looks for exactly bundle.json beside the manifest, and skips the theme if the file is missing or doesn’t parse into a JSON object.
The shape of a theme
A theme definition has three parts that decide its colors: a basetype, a set of semantic colors, and a set of raw colors. Together they override Discord’s palette.
bundle.json
Base type
type picks the foundation your theme builds on. You only override the colors you care about, and everything else falls back to this base.
- midnight
- darker
- light
A pure-black base, for OLED/AMOLED-style themes.
Semantic colors
Semantic colors are Discord’s named roles:BACKGROUND_PRIMARY, TEXT_NORMAL, and so on. Theming by role means a single value recolors everywhere 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 is inert: it’s never looked up, and nothing warns you about it.
Each entry is an object: a type of color (a literal color) or raw (a reference to a raw color), the value, and an optional opacity.
'color' | 'raw'
color is a literal value; raw points at one of your raw colors so many 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 the color string already carried, and is ignored when type is raw.type or value is 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 assystemBlueColor. Setting one replaces that color everywhere the app asks for it, below the level of Discord’s roles. Point semantic roles at one with type: 'raw' and they share whatever it resolves to.
Color formats
Every color string, inraw or in a semantic color entry, is parsed the same way. Three 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.rgba string with fewer than four values, or an rgb string with fewer than three, fails to parse, as does any string that doesn’t start with #, rgb, or rgba. Named CSS colors like red are not supported.
A color that fails to parse is not an error you’ll see. Unbound falls back to the original color for that entry and the app keeps rendering, so a malformed value looks like an override that simply didn’t apply. Check the format first when a color won’t take.