Skip to content
MeanderingProgrammer edited this page Nov 12, 2024 · 4 revisions

Colors

I've found that there is generally some confusion for how colors work in neovim. In particular when it comes to plugin users wanting to modify some specific part of the UI. While there is some background and complexity involved here, what's great is there is nothing plugin specific to how all this works. Once you have a handle of the concepts it will empower you to change the color, of everything you see.

The goal of this document is to explain these concepts from the ground up making as few assumptions as possible of the reader's prior knowledge on this topic.

If you find anything inaccurate please let me know!

Highlight Groups

The first import topic to cover is how colors are defined. Rather than directly using RGB values like #ff0000 for red and applying these to ranges of text the colors are abstracted behind what is called a Highlight Group, see :h highlight-groups.

A Highlight Group is a simple string that is mapped to a variety of display properties. These properties include the RGB for the foreground & background, as well as whether the text should be bold, italicized, etc., see :h nvim_set_hl() for a complete list of properties. Highlight Groups can also be linked to other Highlight Groups, which simply means use the value of this other one instead, this linking is really powerful and will be covered in more detail later.

You can run the :highlight command to see all the Highlight Groups currently defined in neovim along with their values. These definitions will come largely from your color scheme as well as individual plugins.

The :h treesitter-highlight doc does an excellent job giving an end to end example of how ranges of code end up assigned to specific Highlight Groups with a concrete example. The only additional information I would add is that in the most common case parsers for languages and highlights.scm being on your runtimepath is done by nvim-treesitter, and is not something you manage directly, though you definitely could.

Once we have ranges of text assigned to Highlight Groups we move on to the part that makes color schemes work. The key part is that Highlight Group names are somewhat standardized and refer to the same concept across languages, see :h treesitter-highlight-groups. So the @comment Highlight Group will be used for code comments in python, rust, etc. As a result if a color scheme implements all the Highlight Groups defined in the treesitter document it should provide a consistent experience across all languages. You can imagine if these names Highlight Group names were languages specific the amount of work required by color scheme authors would be much higher. As a result these highlight.scm files end up being very important and key to the whole experience.

Plugins

While we've covered Highlight Groups in the context of adding color to code they are used for everything else as well. Plugins need to use Highlight Groups to add color to the UI as well, there is no other mechanism.

Plugin authors will typically follow a common practice of defining their own highlight groups for most things that have color. However they will not define the RGB values directly as the UI would effectively ignore your preferences and use a static coloring for everyone.

Instead they will create "default links" under some unique prefix. What this means is they will create highlights by calling the following API:

vim.api.nvim_set_hl(0, 'MyPluginBackground', { link = 'Normal', default = true })

This default being true means if this Highlight Group already exists then don't change it, keep its current value. In practice this means that individual color schemes can implement specific behaviors for individual plugins if they want to and do not like the result out of the box with their color scheme. Since they should be loaded before other plugins their custom values will be kept. However if no value is defined then link to this other Highlight Group, which is one of the standard set and should be defined by all color schemes. The Highlight Groups defined for the plugin should be clearly visible in the documentation and described, but that's up to the author.

This might sound overly complex, why not just use the link value Normal directly, why wrap it? Well changing the value of the Normal Highlight Group to make one plugin look nicer would be kind of absurd. This linking lets the impact of the change be targeted, rather than impacting how everything looks.

Customization

With that context built up we can now work on changing the color of something. Lets say we want to change the color of the background generated by a plugin.

Lets also say that value is something you can provide via its configuration, i.e. the default looks like:

{
    'PluginAuthor/my-plugin.nvim',
    config = function() 
        require('my-plugin').setup({ background = 'MyPluginBackground' })
    end,
}

And MyPluginBackground links to Normal by default.

Figuring this information out per plugin is not consistent. Ideally something like the README would have the default configuration and Highlight Groups spelled out with descriptions but this is not always the case. This also may not always be the case and a plugin is doing something else, but one of the approaches below should work.

To change the color there are 2 things that can be done, change the highlight group being used, or change the value of the highlight group.

1) Change the Highlight Group

First you'll need to find a highlight group with a color that you want to use instead, use the :highlight command to help you find one you like for this particular use case. From there update the configuration, lets say we liked Visual in this case:

{
    'PluginAuthor/my-plugin.nvim',
    config = function() 
        require('my-plugin').setup({ background = 'Visual' })
    end,
}

This requires the plugin to expose the highlight group as an option you can set.

It is also good to note that if this is a common highlight group changing color schemes will change the color, which could be a good or bad thing depending.

2) Change the Value

To do this use the vim.api.nvim_set_hl API to change the value associated with MyPluginBackground. With this approach you don't modify the configuration but you do need to run the logic at a specific point in time depending on whether the plugin sets default to true when creating Highlight Groups:

  • Yes: Before calling setup so value gets picked up
  • No: After calling setup so you override their value

You also have several types of value you can set:

1) Hard Coded

This involves setting a concrete RGB value for the Highlight Group.

This color will not change when the color scheme is changed.

{
    'PluginAuthor/my-plugin.nvim',
    config = function() 
        vim.api.nvim_set_hl(0, 'MyPluginBackground', { bg = '#ff0000' })
        require('my-plugin').setup({})
        -- Move here if default is not set
    end,
}

2) Link

This is similar to changing the Highlight Group in the configuration but works even if there is no option provided. It is also functionally equivalent in that the color will change based on the color scheme.

{
    'PluginAuthor/my-plugin.nvim',
    config = function() 
        vim.api.nvim_set_hl(0, 'MyPluginBackground', { link = 'Visual' })
        require('my-plugin').setup({})
        -- Move here if default is not set
    end,
}

3) Underlying Link Value

This involves overriding the Highlight Group being linked to. This is not recommended as it will likely have far reaching impact beyond a specific plugin but is given as an option for completeness.

{
    'PluginAuthor/my-plugin.nvim',
    config = function() 
        vim.api.nvim_set_hl(0, 'Normal', { bg = '#ff0000' })
        require('my-plugin').setup({})
        -- Non color scheme plugins should never set values for default
        -- Highlight Groups so the value of default does not matter
    end,
}
Clone this wiki locally