---
title: Visual Themes (13 bundled skins + custom themes)
---

# Visual Themes (13 bundled skins + custom themes)

## What it is

Omniscio ships with **13 bundled skins** and supports importing your own `.theme.json` files. Themes are CSS-variable driven — picking a skin updates a small set of custom properties (`--accent-rgb`, `--panel-bg`, `--panel-border`, `--color-surface-*`, `--accent-glow`, etc.) and every component in the app recolors automatically. That means you never have to touch code to reskin the app, and theme authors never have to maintain a fork. Bundled options range from clean corporate (Linear, Vercel Clean, Minimal Luxury) to atmospheric (Aurora, Gradient, Neon Accent, Frosted Tiers) to glassy/textured (Glassmorphism, **Liquid Glass**, Neumorphic, Soft Depth, Warm Charcoal, Spotify). Liquid Glass is the newest — a cohesive frosted-glass skin (translucent blurred panels, accent glow, rounded corners) with both dark and light variants.

The atmospheric skins (**Glassmorphism** and **Aurora**) paint two soft blurred colour blobs behind the whole app. Those blobs are sized off the **longer** edge of the window, so they cover a monitor rotated to portrait as generously as a wide landscape one — on a tall screen, sizing them off width alone left two small dots in opposite corners with a flat dead band between. On any landscape window the sizing is mathematically identical to what it has always been, so nothing changes there. The blobs are **static** (they don't drift) — an animated blur re-rasterizes every frame and was a measured cause of typing lag — and Low Power Mode drops the blur entirely.

## Where to find it

**Settings → Appearance** holds the **Visual Theme** grid of thirteen cards and the **Custom Themes** grid beneath it, with **Import** and **Open themes folder** buttons on the custom side. The full authoring reference sits one click further along, behind **Custom Theme Guide** in the same pane.

## How it behaves

### How to use it

1. **Pick a bundled skin.** Settings → **Appearance** → the **Visual Theme** grid shows 13 cards, each with a small preview of base, panel, accent, and text colors. Click one — the app recolors instantly without a reload.
2. **Import a custom theme.** Same pane, **Custom Themes** grid below. Click **Import** → file picker filtered to `.json` → select a `.theme.json` file. The schema is validated; invalid files get a clear error (bad field names, missing required keys). Valid imports land in `{userData}/themes/{id}.theme.json` and appear as a new card in the custom grid.
3. **Switch between bundled and custom.** Any theme card is click-to-apply. Omniscio remembers your last choice; a fresh install starts on the default dark skin.
4. **Open the themes folder.** The **Open themes folder** button reveals `{userData}/themes/` in your OS file explorer — useful for dropping in theme files shared by others, or editing a custom theme by hand and seeing it reload.
5. **Delete a custom theme.** Any custom card has a trash button. Bundled themes can't be deleted (they re-seed from the binary anyway).
6. **Write your own.** Structure is a JSON file with `id`, `name`, `description`, `author`, `version: 1`, a `dark: { ... }` palette (required), an optional `light: { ... }` palette, and a small `preview: { ... }` for the card. Each palette maps a small set of camelCase keys to CSS colors — Omniscio translates those into the CSS variables at runtime. The full reference ships with Omniscio; fetch it via Settings → Appearance → **Custom Theme Guide**. Beyond surfaces / panels / accent / badges, a theme can also set the body **`fontFamily`** _and_ the **`fontFamilyMono`** (code blocks, terminal, diagnostics, IDs), the tooltip **`tooltipBg` / `tooltipBorder` / `tooltipText`**, the non-dial **shadow/depth** tokens (**`overlayElevation`** for floating menus, **`shadowTooltip`** / **`shadowSheet`** / **`shadowBtnHover`**), and the **full status-colour language** (`statusAgent` / `NeedsYou` / `Error` / `Ready` / `Question` / `Approval` / `Ember` + the `-strong` on-light variants + `linkRgb`) — the same tokens the app's error / warning / success / info badges, alerts and status chips read, so those recolour with the theme too. (The chat-message depth and the Motion & Polish depth stay under their own Appearance toggles, not the theme.)

## For agents

### How it works

Bundled theme definitions are a single TypeScript array in [/src/renderer/src/features/settings/visual-theme-definitions.ts](/src/renderer/src/features/settings/visual-theme-definitions.ts) (`VISUAL_THEME_DEFINITIONS`). Custom themes are loaded from `{userData}/themes/` by [/src/main/services/custom-theme-service.ts](/src/main/services/custom-theme-service.ts): each file is parsed, validated against the Zod schema in [/src/shared/custom-theme-types.ts](/src/shared/custom-theme-types.ts), and exposed to the renderer. Applying a theme sets CSS variables on the document root via a `THEME_VAR_MAP` (camelCase JSON key → CSS custom property name) — this is why every component that uses `bg-surface-100` or `text-accent` just works without per-component changes. Note the Tailwind `surface` utilities read the `--color-surface-N-rgb` _twin_ (not the hex var), so `buildCustomThemeCSS` **derives that twin from each hex/rgb surface value** — without it a custom theme's surfaces wouldn't actually recolour. For semantic status colours, components must use the `status-*` tokens (not raw `text-red-400` etc.) so a theme can recolour them; a ratcheting guard ([tests/unit/lint/no-raw-status-color.test.ts](/tests/unit/lint/no-raw-status-color.test.ts)) blocks new raw status colours. See [custom-theme-tokens-contract.md](/.claude/memory/contracts/custom-theme-tokens-contract.md). IPC surface is 5 channels registered in [/src/main/ipc/custom-theme-handlers.ts](/src/main/ipc/custom-theme-handlers.ts): `CUSTOM_THEMES_LIST`, `CUSTOM_THEME_IMPORT`, `CUSTOM_THEME_DELETE`, `CUSTOM_THEMES_OPEN_FOLDER`, `CUSTOM_THEME_GUIDE_GET`. One push channel — `CUSTOM_THEMES_CHANGED` — fires on import/delete so the Appearance grid refreshes without a full app rerender. The Appearance settings component is [/src/renderer/src/features/settings/AppearanceSettings.tsx](../../src/renderer/src/features/settings/sections/appearance/AppearanceSettings.tsx). The 13 bundled names: _Glassmorphism, Linear, Warm Charcoal, Aurora, Soft Depth, Neon Accent, Spotify, Minimal Luxury, Gradient, Neumorphic, Frosted Tiers, Vercel Clean, Liquid Glass_. Every bundled theme has a **light-mode variant** (`.theme-<id>:not(.dark)`) that must stay _visibly distinct_ — a hue-tinted surface scale and/or a clearly different accent (Vercel Clean's identity in light mode is deliberately monochrome white + black accent) — while keeping secondary text at WCAG AA on the tinted surfaces; both directions are locked by [tests/unit/lint/light-theme-identity.test.ts](/tests/unit/lint/light-theme-identity.test.ts). The light-variant blocks live **outside `@layer base`** on purpose: Tailwind tree-shakes custom layer rules whose class names never appear literally in source (theme classes are runtime-constructed), which once silently stripped every light variant from the shipped bundle — survival in the _compiled_ CSS is locked by [tests/unit/lint/light-theme-compiled-survival.test.ts](/tests/unit/lint/light-theme-compiled-survival.test.ts). Solid-accent button labels adapt to the live accent: a white label on most accents — with a crisp dark **halo** (a tight `text-shadow`, `--on-accent-text-shadow`) so small 12px labels stay readable on a mid/colored fill — and a **dark** label on a near-white accent like Vercel Clean's, whose fill stays LIGHT (never deepened, else dark-on-dark), all via `--on-accent-color`. On a too-pale accent that still uses white (gold / GitHub blue / Spotify green / Aurora, ~2.1–3.0:1) the solid PRIMARY button additionally **deepens its fill** through a button-only `--btn-accent-fill` (never the global `--accent-rgb`, so links / tints / borders are untouched) until white clears WCAG AA; the status-strong fills (danger / warning / success) carry the same halo via their own `.bg-status-*-strong.text-white` rules (kept OFF `.amc-btn-raised`, whose block the raised-chrome guards read for box-shadow depth). Computed in [accent-contrast.ts](/src/renderer/src/lib/accent-contrast.ts) (`pickOnAccentText` + `deepenAccentFill`); locked by [raised-accent-label-adaptive.test.ts](/tests/unit/lint/raised-accent-label-adaptive.test.ts) + [filled-button-label-halo.test.ts](/tests/unit/lint/filled-button-label-halo.test.ts) + [accent-contrast.test.ts](/tests/unit/accent-contrast.test.ts). A bundled theme id is registered in five aligned places (the `VISUAL_THEME_IDS` list + the `visualTheme` Zod enum in [/src/shared/ipc-schemas/settings/appearance-settings.ts](/src/shared/ipc-schemas/settings/appearance-settings.ts), the `.dark.theme-<id>` + `.theme-<id>:not(.dark)` CSS blocks in [/src/renderer/src/styles/globals.css](/src/renderer/src/styles/globals.css), and the preview card in `VISUAL_THEME_DEFINITIONS`); parity is enforced by `tests/unit/shared/visual-theme-parity.test.ts`.

## Related

- [chat-depth.md](chat-depth.md) — the **Chat Depth** dial (Flat / Soft / Glass) in the same Appearance pane, which controls how much shadow and glassy depth the chat messages have, independent of the theme
- [use-super-prompts.md](use-super-prompts.md) — another example of an extensible asset imported via `.json`
- [daily-digest.md](daily-digest.md) — digests render in whatever theme you've picked; sharing artifacts inline both the light and dark CSS so viewers see either
