A high-performance color highlighter for Neovim with no external dependencies. Written in performant Luajit.
Screenshot tests — CI-generated visual tests for
every parser and display mode. If something looks off, click the [N] link
next to the test to report an issue.
- Fast: Handwritten trie-based parser with byte-level dispatch. Only visible lines are processed.
- Zero dependencies: As long as you have
malloc()andfree(), it works (Linux, macOS, Windows). - Broad format support: Hex (
#RGB,#RRGGBB,#RRGGBBAA,#AARRGGBBQML,0xAARRGGBB), CSS functions (rgb(),hsl(),hwb(),lab(),lch(),oklch(),color()), CSS custom properties (var(--name)), named colors, xterm/ANSI 256, Tailwind CSS, Sass variables, and custom parsers — in any filetype. - Display modes: Background (with auto-contrast text), foreground, underline (colored via
sp), and virtualtext (inline or end-of-line). - Higher priority than treesitter: Uses
vim.hl.priorities(diagnostics/user) so colorizer highlights always win over treesitter syntax colors.
Requires Neovim >= 0.10.0 and set termguicolors
-- lazy.nvim
{
"catgoose/nvim-colorizer.lua",
event = "BufReadPre",
opts = {},
}-- Enable all CSS color formats
require("colorizer").setup({
options = { parsers = { css = true } },
})
-- CSS functions only, with virtualtext display
require("colorizer").setup({
options = {
parsers = { css_fn = true },
display = {
mode = "virtualtext",
virtualtext = { position = "after" },
},
},
})
-- Preset with individual override
require("colorizer").setup({
options = {
parsers = { css = true, rgb = { enable = false } },
},
})
-- Per-filetype overrides
require("colorizer").setup({
filetypes = {
"*",
"!markdown",
html = { mode = "foreground" },
cmp_docs = { always_update = true },
},
})The default key in parsers.hex sets the default value for all format
keys (rgb, rgba, rrggbb, rrggbbaa, aarrggbb). Any format key you
don't set explicitly inherits from default. Keys you set explicitly always
take priority.
-- Enable all hex formats
hex = { default = true }
-- Enable all hex formats except 8-digit (#RRGGBBAA)
hex = { default = true, rrggbbaa = false }
-- Disable all hex formats
hex = { default = false }
-- Only enable 6-digit hex
hex = { rrggbb = true }
-- Equivalent to the above (default is already false)
hex = { default = false, rrggbb = true }Note: Other parsers (
names,tailwind,sass) useenableas a simple on/off switch. Thedefaultkey is unique tohexbecause it is the only parser with multiple boolean format sub-keys.
require("colorizer").setup({
filetypes = { "*" }, -- filetypes to highlight, "*" for all
buftypes = {}, -- buftypes to highlight
user_commands = true, -- enable user commands (ColorizerToggle, etc.)
lazy_load = false, -- lazily schedule buffer highlighting
options = {
parsers = {
css = false, -- preset: enables names, hex, rgb, hsl, oklch, css_var
css_fn = false, -- preset: enables rgb, hsl, oklch
names = {
enable = true, -- enable named colors (e.g. "Blue")
lowercase = true, -- match lowercase names
camelcase = true, -- match CamelCase names (e.g. "LightBlue")
uppercase = false, -- match UPPERCASE names
strip_digits = false, -- ignore names with trailing digits (e.g. "blue3")
custom = false, -- custom name-to-hex mappings; table|function|false
extra_word_chars = "-", -- extra chars treated as part of color name
},
hex = {
default = true, -- default value for unset format keys (see above)
rgb = true, -- #RGB (3-digit)
rgba = true, -- #RGBA (4-digit)
rrggbb = true, -- #RRGGBB (6-digit)
rrggbbaa = false, -- #RRGGBBAA (8-digit)
hash_aarrggbb = false, -- #AARRGGBB (QML-style, alpha first)
aarrggbb = false, -- 0xAARRGGBB
no_hash = false, -- hex without '#' at word boundaries
},
rgb = { enable = false }, -- rgb()/rgba() functions
hsl = { enable = false }, -- hsl()/hsla() functions
oklch = { enable = false }, -- oklch() function
hwb = { enable = false }, -- hwb() function (CSS Color Level 4)
lab = { enable = false }, -- lab() function (CIE Lab)
lch = { enable = false }, -- lch() function (CIE LCH)
css_color = { enable = false }, -- color() function (srgb, display-p3, a98-rgb, etc.)
tailwind = {
enable = false, -- parse Tailwind color names
update_names = false, -- feed LSP colors back into name parser (requires both enable + lsp.enable)
lsp = { -- accepts boolean, true is shortcut for { enable = true, disable_document_color = true }
enable = false, -- use Tailwind LSP documentColor
disable_document_color = true, -- auto-disable vim.lsp.document_color on attach
},
},
sass = {
enable = false, -- parse Sass color variables
parsers = { css = true }, -- parsers for resolving variable values
variable_pattern = "^%$([%w_-]+)", -- Lua pattern for variable names
},
xterm = { enable = false }, -- xterm 256-color codes (#xNN, \e[38;5;NNNm)
xcolor = { enable = false }, -- LaTeX xcolor expressions (e.g. red!30)
hsluv = { enable = false }, -- hsluv()/hsluvu() functions
css_var_rgb = { enable = false }, -- CSS vars with R,G,B (e.g. --color: 240,198,198)
css_var = {
enable = false, -- resolve var(--name) references to their defined color
parsers = { css = true }, -- parsers for resolving variable values
},
custom = {}, -- list of custom parser definitions
},
display = {
mode = "background", -- "background"|"foreground"|"underline"|"virtualtext"
background = {
bright_fg = "#000000", -- text color on bright backgrounds
dark_fg = "#ffffff", -- text color on dark backgrounds
},
virtualtext = {
char = "■", -- character used for virtualtext
position = "eol", -- "eol"|"before"|"after"
hl_mode = "foreground", -- "background"|"foreground"
},
priority = {
default = 150, -- extmark priority for normal highlights
lsp = 200, -- extmark priority for LSP/Tailwind highlights
},
},
hooks = {
should_highlight_line = false, -- function(line, bufnr, line_num) -> bool
should_highlight_color = false, -- function(rgb_hex, parser_name, ctx) -> bool
transform_color = false, -- function(rgb_hex, ctx) -> string
on_attach = false, -- function(bufnr, opts)
on_detach = false, -- function(bufnr)
},
always_update = false, -- update highlights even in unfocused buffers
debounce_ms = 0, -- debounce highlight updates (ms); 0 = no debounce
},
})Tailwind colors can be parsed from the bundled color data (enable) or via textDocument/documentColor from the Tailwind LSP (lsp). Both can be used together.
| Option | Behavior |
|---|---|
enable = true |
Parse standard Tailwind color names |
lsp = true |
Use Tailwind LSP document colors |
Both true |
Combine both sources |
lsp accepts a boolean shorthand or a table for fine-grained control:
require("colorizer").setup({
options = {
parsers = {
tailwind = { enable = true, lsp = true },
},
},
})require("colorizer").setup({
options = {
parsers = {
tailwind = {
enable = true,
lsp = {
enable = true,
disable_document_color = true, -- default
},
update_names = true,
},
},
},
})With lsp.update_names = true and both enable + lsp.enable active, LSP
results are fed back into the name parser's color table. Name-based parsing is
instant (works in cmp windows, new buffers, etc.) but uses bundled color data.
The LSP is slower (requires server response) but reads custom colors from
tailwind.config.{js,ts}. By combining both, buffers are painted immediately
with name-based matches, then LSP results correct the colors and update the
name table so subsequent name-based highlights use accurate values.
Neovim 0.12+ has built-in textDocument/documentColor support via
vim.lsp.document_color that is enabled by default on LspAttach. When
colorizer's tailwind.lsp is active, disable_document_color (default true)
automatically calls vim.lsp.document_color.enable(false, bufnr) to prevent
duplicate highlights. No manual LspAttach autocmd is needed.
To keep the built-in feature active alongside colorizer:
tailwind = {
enable = true,
lsp = { enable = true, disable_document_color = false },
},Or use the built-in feature instead and disable colorizer's LSP integration:
-- Let Neovim handle LSP colors, colorizer handles everything else
require("colorizer").setup({
options = {
parsers = {
tailwind = { enable = true, lsp = false },
},
},
})The built-in vim.lsp.document_color.enable() supports style options:
'background' (default), 'foreground', 'virtual', or a custom string/function.
See :help vim.lsp.document_color.enable() for details.
Note: This only applies to Neovim 0.12+. Neovim 0.10 and 0.11 do not have this feature and are unaffected.
Colorizer uses extmark priorities from display.priority to control which
highlights win when multiple sources target the same range:
| Key | Default | Based on | Purpose |
|---|---|---|---|
default |
150 | vim.hl.priorities.diagnostics |
Normal parser-based highlights |
lsp |
200 | vim.hl.priorities.user |
Tailwind LSP highlights |
These defaults are higher than treesitter (100) and semantic tokens (125), so colorizer highlights always win over syntax highlighting. The LSP priority is higher than default so Tailwind LSP results take precedence over parser-based matches on the same range.
Neovim's built-in vim.lsp.document_color sets no explicit priority on its
extmarks (effectively 0), so if both are active on the same buffer you get
duplicate highlights rather than a priority conflict. This is why
disable_document_color defaults to true — it prevents the duplicates
entirely.
To customize priorities:
require("colorizer").setup({
options = {
display = {
priority = {
default = 50, -- lower than treesitter, color highlights lose
lsp = 300, -- higher than default user priority
},
},
},
})Register custom parsers to highlight application-specific color patterns:
require("colorizer").setup({
options = {
parsers = {
custom = {
{
name = "android_color",
prefixes = { "Color." },
parse = function(ctx)
local m = ctx.line:match('^Color%.parseColor%("#(%x%x%x%x%x%x)"%)', ctx.col)
if m then
return #'Color.parseColor("#xxxxxx")', m:lower()
end
end,
},
},
},
},
})Each custom parser supports: name, parse(ctx), prefixes, prefix_bytes, setup(ctx), teardown(ctx), state_factory(). See the full documentation for details.
should_highlight_line is called before each line is parsed. Return true to highlight, false to skip:
require("colorizer").setup({
options = {
hooks = {
should_highlight_line = function(line, bufnr, line_num)
return string.sub(line, 1, 2) ~= "--"
end,
},
},
})should_highlight_color is called after a color is parsed. Return false to skip that color:
hooks = {
should_highlight_color = function(rgb_hex, parser_name, ctx)
-- Skip black and white
return rgb_hex:lower() ~= "000000" and rgb_hex:lower() ~= "ffffff"
end,
}transform_color remaps the color before display:
hooks = {
transform_color = function(rgb_hex, ctx)
-- Desaturate: convert everything to grayscale
local r = tonumber(rgb_hex:sub(1, 2), 16)
local g = tonumber(rgb_hex:sub(3, 4), 16)
local b = tonumber(rgb_hex:sub(5, 6), 16)
local gray = math.floor(0.299 * r + 0.587 * g + 0.114 * b)
return string.format("%02x%02x%02x", gray, gray, gray)
end,
}on_attach and on_detach are called when colorizer attaches to or detaches from a buffer:
hooks = {
on_attach = function(bufnr, opts)
vim.notify("Colorizer attached to buffer " .. bufnr)
end,
on_detach = function(bufnr)
vim.notify("Colorizer detached from buffer " .. bufnr)
end,
}The css_var parser resolves var(--name) references by scanning the buffer
for --name: <color> definitions. Any color format recognized by the configured
parsers (hex, rgb, hsl, etc.) works in definitions.
require("colorizer").setup({
options = {
parsers = {
css = true, -- also enables css_var via the css preset
},
},
})Or enable it explicitly without the full css preset:
require("colorizer").setup({
options = {
parsers = {
hex = { default = true },
css_var = { enable = true, parsers = { css = true } },
},
},
})Features:
- Resolves aliased variables:
--alias: var(--base)chains are followed - Handles
var(--name, fallback)syntax (highlights using the definition) - Re-scans definitions on every text change
require("colorizer").attach_to_buffer(0, {
parsers = { css = true },
display = { mode = "foreground" },
})
require("colorizer").detach_from_buffer(0)| Command | Description |
|---|---|
| ColorizerAttachToBuffer | Attach to the current buffer |
| ColorizerDetachFromBuffer | Stop highlighting the current buffer |
| ColorizerReloadAllBuffers | Reload all highlighted buffers |
| ColorizerToggle | Toggle highlighting of the current buffer |
The flat user_default_options format is fully supported and automatically
translated to the new structured format internally. No migration is required.
Important:
- The legacy option set is frozen — no new options will be added to it.
New features (e.g.
hsluv,xcolor,css_var_rgb,css_var,debounce_ms,hex.hash_aarrggbb,hex.no_hash) are only available via the structuredoptionsformat. - If both
optionsanduser_default_optionsare provided,optionswins.
require("colorizer").setup({
user_default_options = {
names = true,
RGB = true,
RRGGBB = true,
css = false,
mode = "background",
tailwind = false,
},
})See :help colorizer.config and the
full documentation for the
legacy-to-new translation mapping.
make test
make test-file FILE=tests/test_config.lua:help colorizer- Full API docs

