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.
malloc() and free(), it works (Linux, macOS, Windows).#RGB, #RRGGBB, #RRGGBBAA, #AARRGGBB QML, 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.sp), and virtualtext (inline or end-of-line).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 },
},
})
default keyThe 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", -- string or list: "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
},
disable_document_color = true, -- true (all LSPs) | false | { lsp_name = true, ... }
},
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. This
can cause duplicate highlights — for example, background highlighting on hex
codes even when colorizer is set to virtualtext mode.
Colorizer automatically disables vim.lsp.document_color on buffer attach via
display.disable_document_color (default true). This applies to all LSP
servers, not just Tailwind. No manual LspAttach autocmd is needed.
disable_document_color accepts three forms:
| Value | Behavior |
|---|---|
true |
Disable document color for all LSP servers (default) |
false |
Keep vim.lsp.document_color active |
{ lsp_name = true, ... } |
Disable only for the listed servers |
Additionally, when tailwind.lsp is active, the Tailwind-specific
tailwind.lsp.disable_document_color (also default true) handles the case
where the Tailwind LSP attaches after colorizer.
To keep the built-in feature active alongside colorizer:
require("colorizer").setup({
options = {
display = {
disable_document_color = false, -- keep vim.lsp.document_color active
},
},
})
To disable only for specific LSP servers:
require("colorizer").setup({
options = {
display = {
disable_document_color = { cssls = true, html = true },
},
},
})
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.
display.mode accepts a list to apply multiple modes simultaneously:
require("colorizer").setup({
options = {
display = {
mode = { "background", "virtualtext" }, -- colored background + color swatch
},
},
})
Non-virtualtext modes (background, foreground, underline) merge into a
single extmark since their highlight attributes don't overlap. virtualtext
always gets its own extmark. Any combination of the four modes is valid.
Note:
backgroundandforegroundboth set thefgattribute. When combined,backgroundwins (auto-contrast text is needed for readability). Usebackground+underlineif you want both effects.
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:
--alias: var(--base) chains are followedvar(--name, fallback) syntax (highlights using the definition)@import declarations to resolve variables from imported CSS filescss_var automatically follows @import declarations to resolve variables
defined in other files. All standard import syntaxes are supported:
@import url("variables.css");
@import url('tokens.css');
@import "theme.css";
Import paths are resolved relative to the current file. Buffer-local definitions always take precedence over imported ones.
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:
hsluv, xcolor, css_var_rgb, css_var,
debounce_ms, hex.hash_aarrggbb, hex.no_hash) are
only available via the structured options format.options and user_default_options are provided, options wins.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