Depth of field for your line numbers.
Line numbers far from the cursor are blended toward the background in discrete steps, so the ones you are about to jump to stay legible and the rest recedes.
That is all it does. Signs, folds and the layout of the number column stay with
whoever owns your 'statuscolumn'.
The same buffer at the same cursor position, with the fade on and off:
:Bokeh on |
:Bokeh off |
|---|---|
![]() |
![]() |
- Neovim 0.10+
'termguicolors'(the fade is computed as 24-bit colour)- Optionally statuscol.nvim
With lazy.nvim:
{ "delphinus/bokeh.nvim", opts = {} }There are three ways in, in order of how much bokeh.nvim takes over.
require("bokeh").hl returns nothing but a %#BokehFadeN# highlight item.
Put it in front of a segment that renders line numbers and keep that renderer's
behaviour — statuscol.nvim's thousands separator, relculright, sign-in-number
column and so on all keep working.
local bokeh = require "bokeh"
bokeh.setup()
local builtin = require "statuscol.builtin"
require("statuscol").setup {
relculright = true,
segments = {
{ text = { builtin.foldfunc }, click = "v:lua.ScFa" },
{ text = { bokeh.hl, builtin.lnumfunc }, click = "v:lua.ScLa" },
},
}require("bokeh").segment is a drop-in statuscol.nvim text segment. It
matches the built-in number column cell for cell: 'numberwidth' counts the
separating space, so the number is right-aligned in one column less than that,
and with 'relativenumber' set the cursor line's absolute number goes to the
left. Use hl in front of another renderer if you want a different layout —
statuscol.nvim's relculright or its thousands separator, say.
segments = {
{ text = { builtin.foldfunc }, click = "v:lua.ScFa" },
{ text = { require("bokeh").segment }, click = "v:lua.ScLa" },
}No other plugin involved: bokeh.nvim sets 'statuscolumn' to the fold column,
the sign column and a faded number column.
require("bokeh").setup { standalone = true }Defaults:
require("bokeh").setup {
bands = 5, -- number of fade steps
distance = 12, -- distance at which the deepest band starts
amount = 0.7, -- how far the deepest band is blended, 0..1
curve = "linear", -- "linear" | "ease_in" | "ease_out" | fun(t: number): number
target = nil, -- colour to fade toward; nil means the `Normal` background
from = "LineNr", -- highlight group the fade starts from, or { above = …, below = … }
standalone = false, -- set 'statuscolumn' ourselves
enabled = true, -- start enabled
redraw = "auto", -- keep the fade in sync without 'relativenumber'
}A deeper, shorter fade:
require("bokeh").setup { bands = 8, distance = 6, amount = 0.85, curve = "ease_in" }curve shapes how the steps are distributed. ease_in keeps lines near the
cursor crisp and drops off late; ease_out fades hard right next to the cursor.
Pass your own fun(t: number): number if neither fits — t runs 0..1 across the
bands and the result scales amount.
v:relnum is a distance and says nothing about direction, so by default lines
above and below the cursor fade alike. Name a group per direction to split them:
require("bokeh").setup {
from = { above = "LineNrAbove", below = "LineNrBelow" },
}That builds BokehFadeAbove1 … and BokehFadeBelow1 … instead of the plain
BokehFadeN, and the fade starts from each direction's own colour. Reading the
cursor line costs about 57ns per drawn line, so a full screen pays around 3µs.
hl is the whole integration surface, so any column you can write in
'statuscolumn' can carry the fade. Absolute and relative side by side, with
the fade split by direction:
local bokeh = require "bokeh"
vim.api.nvim_set_hl(0, "GutterAbove", { fg = "#7b9ac7" })
vim.api.nvim_set_hl(0, "GutterBelow", { fg = "#6aa781" })
vim.api.nvim_set_hl(0, "GutterAbsolute", { fg = "#6b7089" })
bokeh.setup { from = { above = "GutterAbove", below = "GutterBelow" } }
function _G.Gutter()
-- Each option folds its own column, and with both off there is nothing left.
local nu, rnu = vim.wo.number, vim.wo.relativenumber
if not (nu or rnu) then return "" end
local absolute_width = nu and #tostring(vim.api.nvim_buf_line_count(0)) or 0
local relative_width = rnu and 4 or 0
if vim.v.virtnum ~= 0 then return (" "):rep(absolute_width + relative_width + 1) end
local column = ""
if nu then
local absolute = tostring(vim.v.lnum)
local hl = vim.v.relnum == 0 and "%#CursorLineNr#" or "%#GutterAbsolute#"
column = hl .. (" "):rep(absolute_width - #absolute) .. absolute
end
if rnu then
-- Blank rather than "0" on the cursor line, unless it is the only number left.
local relative = vim.v.relnum > 0 and tostring(vim.v.relnum) or (nu and "" or "0")
local hl = bokeh.hl()
if hl == "" and vim.v.relnum == 0 then hl = "%#CursorLineNr#" end
column = column .. hl .. (" "):rep(relative_width - #relative) .. relative
end
return column .. " "
end
vim.o.statuscolumn = "%!v:lua.Gutter()"None of that layout is bokeh's, and none of it needs to be: hl() returns an
empty string on the cursor line and while the fade is off, so the column keeps
its shape either way. The full file is demo/example.lua.
hl() only colours — whether a number is drawn at all stays with your renderer,
and that includes honouring 'number' and 'relativenumber'. Neovim opens help
windows with both off, and ftplugins commonly do the same for quickfix and
terminal windows. segment(), standalone mode and statuscol.nvim's
builtin.lnumfunc all check for you; a hand-written column has to check for
itself.
Check them one at a time, not as a single or. The built-in number column
treats them as two independent switches — 'number' alone shows absolute
numbers, 'relativenumber' alone shows distances with 0 on the cursor line —
so a column that only asks "is either one on?" will keep drawing both of its
own columns and make :set nornu look like it does nothing. Fold each column
away with the option that owns it, as above.
:Bokeh " toggle
:Bokeh off
:Bokeh onrequire("bokeh").disable()
require("bokeh").enable()
require("bokeh").toggle() -- returns the state after flipping
require("bokeh").is_enabled()Per scope, checked on every drawn line:
vim.g.bokeh_disable = true -- everywhere
vim.b[buf].bokeh_disable = true -- one buffer
vim.w[win].bokeh_disable = true -- one windowIn standalone mode, switching off restores the 'statuscolumn' that was in
force before setup(). Otherwise the line numbers keep rendering, just with
LineNr instead of the fade bands — bokeh.nvim cannot unhook itself from
someone else's statuscolumn.
BokehFade1 … BokehFade{bands}, where band 1 is nearest the cursor and
bands is the most faded. With a directional from they become
BokehFadeAbove1 … and BokehFadeBelow1 … instead. They are derived from
from (LineNr by default), keeping its other attributes, and are recomputed
on every ColorScheme so they track your colorscheme instead of freezing at
startup.
Override them after the fact if you want colours the blend cannot produce:
vim.api.nvim_create_autocmd("ColorScheme", {
callback = function()
vim.api.nvim_set_hl(0, "BokehFade5", { fg = "#2a2f45", italic = true })
end,
})The cursor line is never touched, so it keeps CursorLineNr.
'statuscolumn' is only re-evaluated on cursor movement when
'relativenumber' is set — see :help 'statuscolumn'. With only 'number'
on, the fade would keep pointing at the line the cursor used to be on, so
bokeh.nvim forces the redraw itself in exactly that case. Set redraw = false
to opt out, or redraw = true to force it unconditionally.
:checkhealth bokeh reports this along with 'termguicolors' and the state of
the fade bands.
- Rendering absolute and relative numbers side by side. bokeh.nvim only colours — write the column yourself (see Building your own column) or use line-numbers.nvim.
- Marks, folds, signs, wrapped-line indicators. All of those belong to the statuscolumn, not to the fade.
MIT



