Banner of The Visual Stack: Deterministic Themes, Transparency, and Custom Highlights

The Visual Stack: Deterministic Themes, Transparency, and Custom Highlights


Category: vim

📅 May 15, 2026   |   👁️ Views: 1

Author:   mosaid

When the Editor Must Look the Same Everywhere

A developer’s editor is a visual environment. Inconsistent colours, jarring flash‑of‑white on startup, or missing highlights break concentration. This configuration treats the visual layer as a deterministic build step: a fallback colourscheme is set immediately, the real theme loads asynchronously, and a series of overrides applies custom highlights that survive theme updates.

This article walks through lua/config/colors.lua – the single module that governs everything visual. We’ll see how it selects between Nordic and Tokyonight, how it makes almost every UI element transparent, and how it re‑applies critical highlights after every ColorScheme event.

The Fallback Strategy

Inside init.lua, right after the basic options, we call vim.cmd("colorscheme desert"). This is not the final theme – it is a bullet‑proof fallback. If the later scheduled call to colors.setup() fails (for example, because the Nordic plugin is missing or broken), the editor still has a working, readable colourscheme.

The fallback also eliminates the “flash of unstyled text” during startup. By setting desert synchronously, Neovim never renders a completely unstyled buffer before the real theme arrives.

The colors.lua Module

The module is structured as a single setup() function that:

  • Enables true‑colour support (vim.o.termguicolors = true).
  • Sets background = "dark".
  • Attempts to load nordic (the preferred theme). If that fails, it falls back to desert and logs a warning.
  • Restores number and cursorline settings (some themes override them).
  • Applies dozens of custom highlight groups for cursor, statusline, search, folds, spelling, line numbers, bufferline, and more.

The entire file is reproduced below. Note how every highlight uses explicit colours (no reference to theme palette variables). This is deliberate: it ensures the highlights look identical whether Nordic or Tokyonight is active.


-- lua/config/colors.lua
-- Colors and highlights configuration (safe + explicit)
local M = {}

function M.setup()
  -- Ensure true color support
  vim.o.termguicolors = true

  -- Preferred background
  vim.o.background = "dark"

  -- Try to load the tokyonight colorscheme safely
  local ok, err = pcall(vim.cmd, "colorscheme nordic")
  if not ok then
    -- Fallback to desert and warn the user in :messages
    vim.cmd("colorscheme desert")
    vim.notify("Warning: Falling back to 'desert'. Error: " .. tostring(err), vim.log.levels.WARN)
  end

  -- Ensure line numbers and cursorline are enabled (set after colorscheme to avoid overrides)
  vim.wo.number = true
  vim.wo.relativenumber = true
  vim.wo.cursorline = true

  -- Then set custom highlight groups
  -- Cursor line / column
  vim.api.nvim_set_hl(0, "CursorLine", { bg = "#3b4261", bold = true })
  vim.api.nvim_set_hl(0, "CursorColumn", { bg = "#3b4261", blend = 80 })
  vim.api.nvim_set_hl(0, "Cursor", { bg = "#2a2e38", fg = "#ffffff", blend = 70 })
  -- Perfect cursor for Nordic: subtle blue, visible, does not obscure text
  vim.api.nvim_set_hl(0, "Cursor", {
    bg = "#434C5E",  -- softer Nordic blue
    fg = "#ECEFF4",  -- visible over any code
    blend = 60       -- transparency so text stays readable underneath
  })

  -- Status line and visual selection
  vim.api.nvim_set_hl(0, "StatusLine", { bg = "#1f2335", fg = "#c0caf5", bold = true })
  vim.api.nvim_set_hl(0, "Visual", { bg = "#FFD966", fg = "black" })

  -- Fold / Search
  vim.api.nvim_set_hl(0, "Folded", { bg = "none", fg = "#737aa2", italic = true })
  vim.api.nvim_set_hl(0, "Search", { bg = "#ff9e64", fg = "black", bold = true })

  -- Spell checking highlights
  vim.api.nvim_set_hl(0, "SpellBad", { underline = true, sp = "#db4b4b" })
  vim.api.nvim_set_hl(0, "SpellLocal", { underline = true, sp = "#0db9d7" })
  vim.api.nvim_set_hl(0, "SpellRare", { underline = true, sp = "#bb9af7" })
  vim.api.nvim_set_hl(0, "SpellCap", { underline = true, sp = "#e0af68" })

  -- Line number colors
  vim.api.nvim_set_hl(0, "LineNr", { fg = "#FFFF00" })
  vim.api.nvim_set_hl(0, "CursorLineNr", { fg = "#c0caf5", bold = true })

  -- BufferLine colors
  vim.api.nvim_set_hl(0, "BufferLineBufferSelected", { bold = true, fg = "#ffca00" })
  vim.api.nvim_set_hl(0, "BufferLineModifiedSelected", { fg = "#ff9e64" })
end

-- Final transparency patch (works for all themes)
local transparent_groups = {
  "Normal", "NormalNC", "NormalFloat",
  "SignColumn", "MsgArea",
  "TelescopeNormal", "TelescopeBorder",
  "StatusLine", "StatusLineNC",
  "BufferLineFill", "BufferLineBackground",
  "LineNr", "CursorLineNr",
  "FloatBorder",
}

for _, group in ipairs(transparent_groups) do
  vim.api.nvim_set_hl(0, group, { bg = "none" })
end

-- Reapply custom highlight after everything is loaded
vim.api.nvim_create_autocmd({"ColorScheme", "User"}, {
  pattern = {"LazyDone", "VeryLazy"},
  callback = function()
    vim.api.nvim_set_hl(0, "MatchParen", { bg = "#ff33aa", fg = "#000000", bold = true })
    vim.api.nvim_set_hl(0, "MatchParenCur", { bg = "#ff33aa", fg = "#000000", bold = true })
    vim.api.nvim_set_hl(0, "MatchWord",  { bg = "#5555ff", fg = "#000000" })
  end,
})

return M

    

Transparency as a Design Decision

The bottom half of the file sets bg = "none" on a carefully chosen set of highlight groups. Groups like Normal, SignColumn, FloatBorder, and BufferLineBackground become transparent, letting the terminal background show through. This creates a clean, borderless look that works with any terminal colour scheme.

Groups that must remain opaque (e.g., CursorLine, Visual, Search) are explicitly given backgrounds, so readability is never compromised. The separation is deliberate: structural UI elements become transparent, content‑focused elements keep their solid backgrounds.

The Autocommand That Fixes Everything

Some plugin managers or theme reloads can reset highlight groups. To combat this, the file registers an autocommand on ColorScheme and User events (specifically LazyDone and VeryLazy) that re‑applies a set of critical highlights:

  • MatchParen and MatchParenCur – a bright pink background that makes matching brackets unmistakable.
  • MatchWord – a blue background for the word under cursor when using * or #.

By re‑applying these after every theme change, the configuration guarantees that your most important visual cues never disappear.

Why Not Just Use a Theme’s Variables?

Many colour configurations use theme palette variables (e.g., colors.blue) so that highlight groups automatically adapt to the current theme. This configuration intentionally avoids that. By using raw hex codes, it decouples the custom highlights from any particular theme’s palette. Whether you’re running Nordic, Tokyonight, or fallback Desert, the cursor line is always #3b4261, the search highlight is always #ff9e64, and matching brackets always explode in pink.

The tradeoff is that you must choose colours that work across multiple themes. The benefit is absolute determinism – the visual experience does not shift when you change or update a theme.

The Full Picture

Together, the fallback colourscheme, the scheduled theme application, the transparent UI groups, and the autocommand re‑application create a visual stack that is:

  • Resilient: the editor always starts with a functional theme.
  • Consistent: custom highlights never depend on which theme loads.
  • Clean: transparency removes visual noise.
  • Reproducible: the same hex values produce the same pixels on any machine.

In the next article, we’ll explore the keymaps and commands that wire all these subsystems together into a unified user interface.


← Engineering a Modal Text Object Engine in Vim: Custom Motions and Operator-Pending Grammars The Toolbelt: Keymaps, User Commands, and File‑Operation Pipelines →