Skip to content

Custom Themes

alsi-lawr edited this page Jul 11, 2026 · 1 revision

Custom themes

Set theme = "custom" to start with seven empty semantic category tables. configure_palette must populate the roles your theme needs.

Minimal partial custom palette

require("neotheme").setup({
	theme = "custom",
	configure_palette = function(palette)
		palette.surface.base = "#181818"
		palette.text.primary = "#e4e4e4"
	end,
})

vim.cmd.colorscheme("neotheme")

Partial palettes load, but Neotheme emits one warning containing every missing semantic path. A complete custom theme avoids the warning and gives every highlight a defined color.

Complete custom-theme pattern

The example below deliberately reuses a compact color set while supplying every semantic role:

local c = {
	deepest = "#0e0e0e",
	dark = "#161616",
	base = "#1d1d1d",
	raised = "#262626",
	selected = "#3c3935",
	border = "#4a4743",
	muted = "#635d54",
	text = "#e4dfd7",
	bright = "#f4eee5",
	strong = "#fff8ef",
	red = "#e06b63",
	yellow = "#e8c05b",
	green = "#93c476",
	blue = "#9eb1d1",
	teal = "#9daf9e",
	purple = "#ab9bc9",
}

local custom = {
	surface = {
		deepest = c.deepest,
		dark = c.dark,
		base = c.base,
		raised = c.raised,
		selected = c.selected,
		border = c.border,
		muted = c.muted,
		addition = "#29332e",
		error = "#a6504c",
	},
	text = {
		primary = c.text,
		bright = c.bright,
		strong = c.strong,
		muted = c.teal,
		on_accent = c.deepest,
		on_error = c.strong,
	},
	syntax = {
		comment = "#bf8a52",
		string = c.green,
		keyword = c.yellow,
		function_name = c.blue,
		type = c.teal,
		property = "#8997a3",
		literal = c.purple,
		operator = c.yellow,
		punctuation = c.bright,
		regexp = c.red,
		special = c.red,
		attribute = c.teal,
		tag = c.blue,
	},
	diagnostic = {
		error = c.red,
		warning = c.yellow,
		information = c.blue,
		hint = c.purple,
		success = c.green,
	},
	markup = {
		heading_1 = c.yellow,
		heading_2 = c.blue,
		heading_3 = c.green,
		heading_4 = c.purple,
		heading_5 = c.teal,
		heading_6 = "#bf8a52",
		quote = "#bf8a52",
		math = c.purple,
		link = c.blue,
		link_label = c.yellow,
		raw = c.green,
		list = c.yellow,
		checked = c.green,
		unchecked = c.teal,
	},
	version_control = {
		added = c.green,
		changed = c.yellow,
		removed = c.red,
		ignored = c.muted,
		conflict = "#ec7c72",
	},
	ui = {
		accent = c.yellow,
		cursor = c.yellow,
		directory = c.blue,
		search = c.yellow,
		current_search = c.strong,
		match = c.purple,
		focus = c.strong,
	},
}

require("neotheme").setup({
	theme = "custom",
	configure_palette = function(palette)
		for category, roles in pairs(custom) do
			for role, color in pairs(roles) do
				palette[category][role] = color
			end
		end
	end,
})

vim.cmd.colorscheme("neotheme")

Custom background behavior

Neotheme sets background to dark for theme = "custom". If your custom palette is light, set the option after loading the colorscheme:

vim.cmd.colorscheme("neotheme")
vim.o.background = "light"

This is separate from palette validation and does not alter any custom colors.

Validation and warnings

Custom palettes follow the same validation rules as built-in customization:

  • Unknown categories fail.
  • Unknown fields fail.
  • Non-string or non-#RRGGBB values fail.
  • A non-nil configurator return value fails.
  • Missing fields produce one warning that lists the missing paths.

See Palette Customization for a description of every role.

Clone this wiki locally