Skip to content

Customizing Themes

Nokkvi comes with 23 built-in themes, but you can easily create your own using simple TOML files.

Switch themes from Settings → Theme → Browse Themes… (open Settings with the ` backtick). The picker is searchable and paints every row in its own palette, so scrolling the list is a live preview; press Enter or center-click a row to apply it. Here’s a sampler of the 23 built-ins — every frame is the same Queue view, so the only thing changing is the theme’s background, accent (the focused row + progress bar), text, and visualizer gradient.

Nokkvi in the Svalbard theme — sage-green accent on a cool dark background

Two toggles in Settings → Theme take colors from the cover of whatever is playing, so the app changes with the music. Both are off by default, and you can use either one on its own.

  • Accent From Album Art (dynamic_accent) replaces the theme’s accent: highlights, active buttons and tabs, the progress bar, the now-playing row, and rating stars and love hearts. Backgrounds, text, and the warning/error colors stay the theme’s.
  • Visualizer From Album Art (dynamic_visualizer) builds the visualizer gradient from the cover for the bars, lines, and scope modes, and for Nokkvi’s own MilkDrop presets.

Here are both toggles on with the Svalbard theme, playing four different covers. The cover in the artwork column sets the accent of the now-playing row, the Queue tab, the transport buttons, and the stars, plus the gradient of the bars over it.

Accent and Visualizer From Album Art on a navy cover of night photos lit in violet: the now-playing row, Queue tab, and play button turn soft violet, and the bars over the cover turn violet too

How the colors are chosen:

  • Nokkvi picks the cover’s most prominent vivid color for the accent, and up to four of its hues for the visualizer gradient. A tiny speck of color doesn’t count.
  • The color is adjusted to stay readable on your theme in both light and dark mode, so it is usually a lighter or darker version of the one on the cover. Filled things like the now-playing row and active buttons stay close to the cover; text drawn in the accent color uses a shade with more contrast.
  • Light mode pulls colors darker, so bright covers (yellow especially) come out deeper there.
  • Black-and-white covers, including sepia or tinted prints, turn the accent and gradient grey.
  • The visualizer colors are kept lighter or darker than the cover itself, since bars, lines, and scope draw on top of the artwork.
  • Stars and hearts take the accent color. On the now-playing row they take the row’s text color, so they never blend into it.
  • The colors change on each track change, with no fade. When playback stops, the theme’s own colors come back.

Nothing is written to your theme file; turn a toggle off and the theme looks exactly as before. For radio, the station’s logo stands in for the cover.

Custom themes are stored in:

~/.config/nokkvi/themes/

Any .toml file placed in this directory will automatically appear in the theme picker (Settings → Theme → Browse Themes…). Built-in themes are seeded to this directory on first launch, so you can open any of them as a starting point. Fresh installs start on Svalbard (Nokkvi’s default theme); switch from the picker at any time.

A theme file has a name field and two palette sections — [dark] and [light] — each containing the same color keys. This lets the same file drive both light and dark mode, toggled by the light_mode setting.

name = "My Theme"
[dark.background]
hard = "#1d2021"
default = "#282828"
# ...
[dark.foreground]
bright = "#fbf1c7"
# ...
[light.background]
hard = "#f9f5d7"
default = "#fbf1c7"
# ...

All keys have defaults. Both [dark] and [light] use the same underlying ThemePalette struct, and omitted fields fall back to the dark defaults — not to the light defaults shown below. So a partial [light] section can look surprisingly dark. For a clean light theme, set every light-side key explicitly; the Light columns in the tables below are the recommended values for a complete light palette.


All color values are 6-digit hex strings ("#rrggbb").

The background scale runs from darkest (hard) to lightest (level3). The UI layers components on this scale to create depth.

KeyDark defaultLight defaultUsed for
hard#1d1d20#ffffffWindow chrome, outermost container
default#222226#fafafbMain view background
soft#2e2e32#ebebedPanel backgrounds, sidebar
level1#2e2e32#ebebedSlot row backgrounds
level2#36363a#dedddaHovered row, secondary surfaces
level3#3d3846#c0bfbcDividers, input borders

The foreground scale runs from brightest text to near-invisible muted text.

KeyDark defaultLight defaultUsed for
bright#ffffff#000000Primary text, titles
level1#f6f5f4#241f31Standard body text
level2#deddda#3d3846Secondary text, metadata
level3#c0bfbc#5e5c64Tertiary text, timestamps
level4#9a9996#77767bPlaceholder text, hints

Accent colors drive interactive elements — the progress bar, focused item, playing indicator, and link color.

KeyDark defaultLight defaultUsed for
primary#3584e4#3584e4Default accent: progress bar, toggles, links
bright#62a0ea#62a0eaHover state, brighter highlight
border_light#1c71d8#99c1f1Light border / glow around accent elements
now_playing#3584e4#1c71d8Legacy/round-trip-only: parsed but not consumed. The now-playing row fill is derived from primary and bright with a contrast guard; setting this key has no visible effect.
selected#3584e4#3584e4Legacy/round-trip-only: parsed but not consumed. The selected/center slot fill is derived from bright with a contrast guard; setting this key has no effect.

Unlike the other palette keys, border is a single hex string at the top level of each palette block — not a sub-table. It sets the color of chrome separators: the 1 px lines between nav-bar cells, list rows, header buttons, modal frames, and capsule outlines.

[dark]
border = "#151515"
[light]
border = "#c4bc9b"
KeyDark defaultLight defaultUsed for
borderderivedderivedChrome separators throughout the UI. When empty, derives from background.hard × 0.70.

Set border as a top-level key under [dark] / [light] in the theme file (as shown above); omit it to fall back to the derived default.

Used for error states, destructive action indicators, and conflict badges.

KeyDark defaultLight default
base#e01b24#c01c28
bright#f66151#ed333b

Used for enabled status indicators and success toasts.

KeyDark defaultLight default
base#33d17a#26a269
bright#8ff0a4#33d17a

Used for warning toasts and capture prompts.

KeyDark defaultLight default
base#f6d32d#e5a50a
bright#f9f06b#f6d32d

Used for star rating fills.

KeyDark defaultLight default
base#f6d32d#e5a50a
bright#f9f06b#f6d32d

Each mode ([dark.visualizer] / [light.visualizer]) has its own set of visualizer colors. See the Visualizer Reference for how gradient_mode maps these onto bars and lines. With Visualizer From Album Art on, the gradients come from the playing cover instead (see Colors from the album art); border_color and the opacities still come from the theme.

KeyDark defaultLight defaultDescription
border_color#1d1d20#ffffffBar and LED outline color
border_opacity1.00.0Bar border opacity (0.0 to 1.0)
led_border_opacity1.00.0Opacity of the outline around each LED in LED bar mode
bar_gradient_colors["#3584e4", "#1c71d8", "#62a0ea"]["#3584e4", "#1a5fb4", "#1c71d8"]1–8 hex colors used for the bar gradient
peak_gradient_colors["#ed333b", "#ff7800", "#f6d32d"]["#e66100", "#f5c211", "#ff7800"]1–8 hex colors used for peak indicators
[dark.visualizer]
border_color = "#1d2021"
border_opacity = 1.0
led_border_opacity = 0.8
bar_gradient_colors = ["#b8bb26", "#98971a", "#d8a657"]
peak_gradient_colors = ["#fb4934", "#fabd2f"]

You don’t need to specify every key — omitted fields use the dark defaults shown in the tables above (one exception: border derives from background.hard × 0.70 rather than copying a hex default). Here’s a theme that only changes the accent color:

name = "Cobalt Accent"
[dark.accent]
primary = "#0088cc"
bright = "#33aaee"
border_light = "#005f99"
[light.accent]
primary = "#0077bb"
bright = "#0088cc"
border_light = "#005f99"

A complete theme file with all sections filled out:

name = "Example Theme"
[dark]
border = "#151515"
[dark.background]
hard = "#1d2021"
default = "#282828"
soft = "#32302f"
level1 = "#3c3836"
level2 = "#504945"
level3 = "#665c54"
[dark.foreground]
bright = "#fbf1c7"
level1 = "#ebdbb2"
level2 = "#d5c4a1"
level3 = "#bdae93"
level4 = "#a89984"
[dark.accent]
primary = "#458588"
bright = "#83a598"
border_light = "#689d6a"
[dark.danger]
base = "#cc241d"
bright = "#fb4934"
[dark.success]
base = "#98971a"
bright = "#b8bb26"
[dark.warning]
base = "#d79921"
bright = "#fabd2f"
[dark.star]
base = "#d79921"
bright = "#fabd2f"
[dark.visualizer]
border_color = "#1d2021"
border_opacity = 1.0
led_border_opacity = 0.8
bar_gradient_colors = ["#b8bb26", "#fabd2f", "#83a598"]
peak_gradient_colors = ["#fb4934", "#fabd2f"]
[light]
border = "#c4bc9b"
[light.background]
hard = "#f9f5d7"
default = "#fbf1c7"
soft = "#f2e5bc"
level1 = "#ebdbb2"
level2 = "#d5c4a1"
level3 = "#bdae93"
[light.foreground]
bright = "#282828"
level1 = "#3c3836"
level2 = "#504945"
level3 = "#665c54"
level4 = "#7c6f64"
[light.accent]
primary = "#076678"
bright = "#458588"
border_light = "#83a598"
[light.danger]
base = "#9d0006"
bright = "#cc241d"
[light.success]
base = "#79740e"
bright = "#98971a"
[light.warning]
base = "#b57614"
bright = "#d79921"
[light.star]
base = "#b57614"
bright = "#d79921"
[light.visualizer]
border_color = "#fbf1c7"
border_opacity = 0.0
led_border_opacity = 0.0
bar_gradient_colors = ["#458588", "#076678", "#83a598"]
peak_gradient_colors = ["#cc241d", "#d79921"]