Files
omarchycn/docs/theming.md
T
David Heinemeier HanssonandClaude Fable 5 f4189398bc Bring docs/ up to date and cover the undocumented subsystems
Every reference doc was audited claim-by-claim against the code.
file-layout and omarchy-shell were the most decayed (renamed commands,
the etc/ overrides source split, dead IPC entry points and example keys);
update-process lagged the recent pipeline changes and gains a channels
section; theming and audio-tuning were accurate but thin around their
lifecycles.

New reference docs for the subsystems that had none: the menu system,
the CLI router, the notification daemon, and the non-acceptance test
architecture. AGENTS.md links the two of those agents will need most.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 16:21:25 +02:00

11 KiB

Omarchy theming

Omarchy themes live under themes/<name>/ in the source tree (installed at /usr/share/omarchy/themes/<name>/), with optional user themes under ~/.config/omarchy/themes/<name>/. A theme normally starts with a colors.toml; Omarchy generates the active theme files from default/themed/*.tpl when omarchy-theme-set <name> runs.

Beyond colors.toml and hand-written config overrides, a theme can ship backgrounds/ (users overlay their own via ~/.config/omarchy/backgrounds/<name>/; the active image is the ~/.local/state/omarchy/current/background symlink), preview.png and preview-unlock.png for the theme switcher, icons.theme, keyboard.rgb, unlock.png, and a light.mode marker file.

Theme activation flow

omarchy-theme-set <name> builds a clean staging directory at ~/.local/state/omarchy/current/next-theme:

  1. Copy the first-party theme from themes/<name>/.
  2. Overlay any user theme files from ~/.config/omarchy/themes/<name>/.
  3. If needed, generate colors.toml from alacritty.toml.
  4. Run omarchy-theme-set-templates to render templates into the staging theme.
  5. Move the staging theme into ~/.local/state/omarchy/current/theme, write ~/.local/state/omarchy/current/theme.name, and notify the running shell.

Template rendering only happens when the staged theme has colors.toml. Existing files are never overwritten by a template, so a hand-written themes/<name>/shell.toml or hyprland.lua wins over default/themed/shell.toml.tpl or hyprland.lua.tpl.

User templates in ~/.config/omarchy/themed/*.tpl are processed before the built-in templates. If a user template has the same output filename as a built-in template, the built-in output is skipped.

After activation, omarchy-theme-set fires the theme-set hook (~/.config/omarchy/hooks/theme-set*, theme name in $1) and dispatches a parallel retint of running apps — terminals, Hyprland, btop, browser, editors, and the rest of the post_theme_commands list in bin/omarchy-theme-set. Making a new app follow theme changes means adding its restart/retint command to that list. Runs serialize on a flock, so scripted theme changes queue instead of racing.

colors.toml

colors.toml provides the palette keys used by templates. Keys are grouped semantic-first: accent/selection/muted, then the backgrounds, then the foregrounds, then the named colors:

mode = "dark"

accent = "#7aa2f7"
selection = "#292e42"
muted = "#414868"

background = "#1a1b26"
dark_background = "#13141c"
darker_background = "#0e0e14"
lighter_background = "#24283b"

foreground = "#a9b1d6"
dark_foreground = "#565f89"
light_foreground = "#b4bee6"
bright_foreground = "#c0caf5"

red = "#f7768e"
blue = "#7aa2f7"

Any key can be referenced from a template with {{ key }}. The foundational shell palette is loaded from:

  • foreground — primary readable text color
  • background — primary background color
  • accent — preferred when present; otherwise some places fall back to color4
  • muted — de-emphasized elements (comments, placeholders, dividers); also serves as ANSI color8
  • red / color1 — populate the shell's urgent role; there is no urgent palette key (one defined in colors.toml is ignored)

Themes and user templates using the legacy short names remain supported. Canonical names take precedence when both forms are defined, and resolved canonical values are also exposed through their legacy names:

Canonical Legacy
background bg
dark_background dark_bg
darker_background darker_bg
lighter_background lighter_bg
foreground fg
dark_foreground dark_fg
light_foreground light_fg
bright_foreground bright_fg

The neutral ramp is centered on background -> bright_foreground. Dark themes should read from darkest to lightest; light themes should read from lightest to darkest. Terminal and editor cursors use bright_foreground; there is no separate cursor palette key. selection is the text-selection background stop in that ramp; Omarchy derives selection_background = selection and selection_foreground = bright_foreground. Use omarchy dev theme-preview [theme] to inspect that ramp, including dark_background, darker_background, and a selected-text sample.

Template placeholders

Templates are plain files ending in .tpl. omarchy-theme-set-templates replaces placeholders with values from colors.toml.

Color placeholders

For a color key such as accent = "#7aa2f7":

Placeholder Output
{{ accent }} #7aa2f7
{{ accent_strip }} 7aa2f7
{{ accent_rgb }} 122,162,247

Color mixing

mix, mix_strip, and mix_rgb blend two hex colors by a fraction or percentage:

{{ mix background foreground 15% }}
{{ mix_strip background accent 0.35 }}
{{ mix_rgb color0 color7 50 }}

Gradient helpers

Some theme keys can be either a solid color or a Hyprland-style gradient:

hyprland_active_border = "rgba(33ccffee) rgba(00ff99ee) 45deg"

Gradient helper placeholders understand those values:

Helper Use Example output
{{ hypr_gradient hyprland_active_border accent }} Hyprland Lua config { colors = { "rgba(33ccffee)", "rgba(00ff99ee)" }, angle = 45 }
{{ shell_gradient hyprland_active_border accent }} shell border tokens rgba(33ccffee) rgba(00ff99ee) 45deg
{{ gradient_start hyprland_active_border accent }} flat-color-only consumers #33ccff

The second argument is a fallback. For example, {{ shell_gradient hyprland_active_border accent }} means: use hyprland_active_border if the theme defines it; otherwise use accent. The helper does not choose the first color unless you use gradient_start.

shell.toml

shell.toml contains shell surface roles, control states, spacing, typography, and bar sizing. The default generated file comes from default/themed/shell.toml.tpl.

Themes can override the entire generated file by shipping shell.toml, or just one section by shipping shell.<section>.toml. For example, shell.lock.toml replaces only the [lock] section after the default shell.toml has been generated:

text        = "#ffffff"
placeholder = "#ffffff"
border      = "#ffffff"

The filename decides the target section, so the [lock] header is optional.

The running shell reads shell.toml into two QML singletons:

  • Color for palette and surface roles like Color.menu.border.
  • Style for controls, spacing, font scale, corner radius, and bar sizing.

Borders

Shell border tokens accept either a solid color or a gradient in the same key:

[notifications]
border = "#7aa2f7"

or:

[notifications]
border = "rgba(33ccffee) rgba(00ff99ee) 45deg"

Do not add a separate border-gradient key for new themes. The parser still accepts border-gradient and *-border-gradient for compatibility with older configs, but the canonical form is the border key itself.

Border alphas apply to solid borders and to every gradient stop:

[notifications]
border       = "rgba(33ccffee) rgba(00ff99ee) 45deg"
border-alpha = 0.8

If a color stop already includes alpha, the stop alpha and border-alpha are combined.

Border widths

Border widths accept CSS-style lists:

border-width = 2          # all sides
border-width = "2 4"      # top/bottom, right/left
border-width = "2 4 6"    # top, right/left, bottom
border-width = "2 4 6 8"  # top, right, bottom, left

Per-side keys override the list:

[notifications]
border-width = 2
border-width-left = 6

That gives notifications a 2px border on the top, right, and bottom, and a 6px left edge.

State-specific borders follow the same pattern. A selected menu row can use a different width from the card border:

[menu]
selected-border = "accent"
selected-border-width = "1 1 1 4"

For state-specific surfaces such as lock and polkit, the token name prefixes the width key:

[lock]
border-active = "rgba(33ccffee) rgba(00ff99ee) 45deg"
border-active-width-left = 6

Control borders

[controls] governs shared controls such as buttons, dropdowns, text fields, toggles, and cursor rows. Each state has a fill color, optional border value, border width, and border alpha:

[controls]
normal-color        = "#a9b1d6"
normal-border       = "#a9b1d6"
normal-border-width = 1
normal-border-alpha = 0.4

hover-cursor-color        = "#a9b1d6"
hover-cursor-border       = "#a9b1d6"
hover-cursor-border-width = 1
hover-cursor-border-alpha = 0.25

The *-border keys can also be gradients:

[controls]
focus-border = "rgba(33ccffee) rgba(00ff99ee) 45deg"
focus-border-width = "2 2 2 4"

Set a border width to 0 to keep the fill but remove that state border.

Surface sections

Common shell sections include:

  • [bar]
  • [controls]
  • [popups]
  • [tooltip]
  • [notifications]
  • [launcher]
  • [menu]
  • [polkit]
  • [lock]
  • [image-picker]
  • [spacing]
  • [font]

Clipboard and emojis inherit menu tokens. Popups are used by bar flyouts, dropdowns, OSD, and popup cards.

QML border API

Plugin and shell QML should use BorderSurface for theme-aware borders:

import qs.Commons
import qs.Ui

BorderSurface {
  color: Color.popups.background
  borderSpec: Border.surfaceSpec("popups", "border", Color.popups.border, 2)
  padding: Style.spacing.popupPadding

  Item {
    anchors.fill: parent
    anchors.topMargin: parent.contentTopInset
    anchors.rightMargin: parent.contentRightInset
    anchors.bottomMargin: parent.contentBottomInset
    anchors.leftMargin: parent.contentLeftInset
  }
}

Use Border.surfaceSpec(section, token, fallbackColor, fallbackWidth, alphaKey) for shell theme tokens (the optional alphaKey names the alpha token, e.g. "border-alpha"), Border.controlSpec(state, foreground, accent, urgent) for shared controls, and Border.flat(color, width) for a deliberate local border that should not be overridden by the active theme. Color.<section>.border is the flat first-stop color for consumers that cannot render full border specs.

Hyprland templates

Hyprland theme output is generated from default/themed/hyprland.lua.tpl. Use hypr_gradient for border values because Hyprland's Lua config wants a Lua string for solid colors and a Lua table for gradients:

local active_border_color = {{ hypr_gradient hyprland_active_border accent }}

For a solid fallback this renders:

local active_border_color = "#7aa2f7"

For a gradient it renders:

local active_border_color = { colors = { "rgba(33ccffee)", "rgba(00ff99ee)" }, angle = 45 }

Adding or overriding theme files

  • Add palette values to themes/<name>/colors.toml.
  • Prefer generated files when the theme can be expressed with templates.
  • Add a hand-written file in themes/<name>/ only when that theme needs to override the generated output entirely.
  • Add a new built-in template under default/themed/<file>.tpl when every theme should generate that file.
  • Add a user-wide template under ~/.config/omarchy/themed/<file>.tpl when a local customization should apply across themes.

When changing templates or theme helpers, run focused tests such as:

./test/cli
./test/shell