A Neovim plugin that adds smooth, customizable animations to text operations like yank, paste, search, undo/redo, and more.
[!WARNING] This plugin is still in beta. Breaking changes may occur in future updates.
https://github.com/user-attachments/assets/745cb1e3-9904-4718-9804-ac0a4fee8748
Smooth animations for various operations:
Built-in animation styles:
fade - Smooth fade in/out transitionreverse_fade - Reverse fade effect with outBack easingbounce - Bouncing highlight effectleft_to_right - Linear left-to-right sweeppulse - Pulsating highlightrainbow - Rainbow color transitioncustom - Define your own animation logic[!NOTE] Many operations are disabled by default. Enable the animations you want to use in your configuration.
{
"rachartier/tiny-glimmer.nvim",
event = "VeryLazy",
priority = 10, -- Low priority to catch other plugins' keybindings
config = function()
require("tiny-glimmer").setup()
end,
}
use {
"rachartier/tiny-glimmer.nvim",
config = function()
require("tiny-glimmer").setup()
end
}
https://github.com/user-attachments/assets/1bb98834-25d2-4f01-882f-609bec1cbe5c
https://github.com/user-attachments/assets/1578d19f-f245-4593-a28f-b7e9593cbc68
https://github.com/user-attachments/assets/6bc98a8f-8b7e-4b57-958a-74ad5372612f
https://github.com/user-attachments/assets/5938e28c-2ff3-4e97-8707-67c24e61895c
require("tiny-glimmer").setup({
-- Enable/disable the plugin
enabled = true,
-- Disable warnings for debugging highlight issues
disable_warnings = true,
-- Animation refresh rate in milliseconds
refresh_interval_ms = 8,
-- Automatic keybinding overwrites
overwrite = {
-- Automatically map keys to overwrite operations
-- Set to false if you have custom mappings or prefer manual API calls
auto_map = true,
-- Yank operation animation
yank = {
enabled = true,
default_animation = "fade",
},
-- Search navigation animation
search = {
enabled = false,
default_animation = "pulse",
next_mapping = "n", -- Key for next match
prev_mapping = "N", -- Key for previous match
},
-- Paste operation animation
paste = {
enabled = true,
default_animation = "reverse_fade",
paste_mapping = "p", -- Paste after cursor
Paste_mapping = "P", -- Paste before cursor
},
-- Undo operation animation
undo = {
enabled = false,
default_animation = {
name = "fade",
settings = {
from_color = "DiffDelete",
max_duration = 500,
min_duration = 500,
},
},
undo_mapping = "u",
},
-- Redo operation animation
redo = {
enabled = false,
default_animation = {
name = "fade",
settings = {
from_color = "DiffAdd",
max_duration = 500,
min_duration = 500,
},
},
redo_mapping = "<c-r>",
},
},
-- Third-party plugin integrations
support = {
-- Support for gbprod/substitute.nvim
-- Usage: require("substitute").setup({
-- on_substitute = require("tiny-glimmer.support.substitute").substitute_cb,
-- highlight_substituted_text = { enabled = false },
-- })
substitute = {
enabled = false,
default_animation = "fade",
},
},
-- Special animation presets
presets = {
-- Pulsar-style cursor highlighting on specific events
pulsar = {
enabled = false,
on_events = { "CursorMoved", "CmdlineEnter", "WinEnter" },
default_animation = {
name = "fade",
settings = {
max_duration = 1000,
min_duration = 1000,
from_color = "DiffDelete",
to_color = "Normal",
},
},
},
},
-- Override background color for animations (for transparent backgrounds)
transparency_color = nil,
-- Animation configurations
animations = {
fade = {
max_duration = 400, -- Maximum animation duration in ms
min_duration = 300, -- Minimum animation duration in ms
easing = "outQuad", -- Easing function
chars_for_max_duration = 10, -- Character count for max duration
from_color = "Visual", -- Start color (highlight group or hex)
to_color = "Normal", -- End color (highlight group or hex)
},
reverse_fade = {
max_duration = 380,
min_duration = 300,
easing = "outBack",
chars_for_max_duration = 10,
from_color = "Visual",
to_color = "Normal",
},
bounce = {
max_duration = 500,
min_duration = 400,
chars_for_max_duration = 20,
oscillation_count = 1, -- Number of bounces
from_color = "Visual",
to_color = "Normal",
},
left_to_right = {
max_duration = 350,
min_duration = 350,
min_progress = 0.85,
chars_for_max_duration = 25,
lingering_time = 50, -- Time to linger after completion
from_color = "Visual",
to_color = "Normal",
},
pulse = {
max_duration = 600,
min_duration = 400,
chars_for_max_duration = 15,
pulse_count = 2, -- Number of pulses
intensity = 1.2, -- Pulse intensity
from_color = "Visual",
to_color = "Normal",
},
rainbow = {
max_duration = 600,
min_duration = 350,
chars_for_max_duration = 20,
-- Note: Rainbow animation does not use from_color/to_color
},
-- Custom animation example
custom = {
max_duration = 350,
chars_for_max_duration = 40,
color = "#ff0000", -- Custom property
-- Custom effect function
-- @param self table - The effect object with settings
-- @param progress number - Animation progress [0, 1]
-- @return string color - Hex color or highlight group
-- @return number progress - How much of the animation to draw
effect = function(self, progress)
return self.settings.color, progress
end,
},
},
-- Filetypes to disable hijacking/overwrites
hijack_ft_disabled = {
"alpha",
"snacks_dashboard",
},
-- Virtual text display priority
virt_text = {
priority = 2048, -- Higher values appear above other plugins
},
})
Each animation can be customized with from_color and to_color options using highlight group names or hex colors:
require("tiny-glimmer").setup({
animations = {
fade = {
from_color = "DiffDelete", -- Highlight group
to_color = "DiffAdd",
},
bounce = {
from_color = "#ff0000", -- Hex color
to_color = "#00ff00",
},
},
})
[!WARNING] The
rainbowanimation does not usefrom_colorandto_coloroptions.
Available easing functions for fade and reverse_fade animations:
linearinQuad, outQuad, inOutQuad, outInQuadinCubic, outCubic, inOutCubic, outInCubicinQuart, outQuart, inOutQuart, outInQuartinQuint, outQuint, inOutQuint, outInQuintinSine, outSine, inOutSine, outInSineinExpo, outExpo, inOutExpo, outInExpoinCirc, outCirc, inOutCirc, outInCircinElastic, outElastic, inOutElastic, outInElasticinBack, outBack, inOutBack, outInBackinBounce, outBounce, inOutBounce, outInBouncelocal glimmer = require("tiny-glimmer")
-- Control plugin state
glimmer.enable() -- Enable animations
glimmer.disable() -- Disable animations
glimmer.toggle() -- Toggle animations on/off
-- Change animation highlights dynamically
-- @param animation_name string|string[] - Animation name(s) or "all"
-- @param hl table - Highlight configuration { from_color = "...", to_color = "..." }
glimmer.change_hl("fade", { from_color = "#FF0000", to_color = "#0000FF" })
glimmer.change_hl("all", { from_color = "#FF0000", to_color = "#0000FF" })
glimmer.change_hl({"fade", "pulse"}, { from_color = "#FF0000", to_color = "#0000FF" })
-- Search operations (when overwrite.search.enabled = true)
glimmer.search_next() -- Same as "n"
glimmer.search_prev() -- Same as "N"
glimmer.search_under_cursor() -- Same as "*"
-- Paste operations (when overwrite.paste.enabled = true)
glimmer.paste() -- Same as "p"
glimmer.Paste() -- Same as "P"
-- Undo/redo operations (when undo/redo.enabled = true)
glimmer.undo() -- Undo changes
glimmer.redo() -- Redo changes
:TinyGlimmer enable " Enable animations
:TinyGlimmer disable " Disable animations
:TinyGlimmer fade " Switch to fade animation
:TinyGlimmer reverse_fade " Switch to reverse_fade animation
:TinyGlimmer bounce " Switch to bounce animation
:TinyGlimmer left_to_right " Switch to left_to_right animation
:TinyGlimmer pulse " Switch to pulse animation
:TinyGlimmer rainbow " Switch to rainbow animation
:TinyGlimmer custom " Switch to custom animation
Keybinding examples:
vim.keymap.set("n", "<leader>ge", "<cmd>TinyGlimmer enable<cr>", { desc = "Enable animations" })
vim.keymap.set("n", "<leader>gd", "<cmd>TinyGlimmer disable<cr>", { desc = "Disable animations" })
vim.keymap.set("n", "<leader>gt", "<cmd>TinyGlimmer fade<cr>", { desc = "Switch to fade" })
The tiny-glimmer.lib module provides a low-level API for creating custom animations programmatically. This is useful for integrating animations into your own plugins or creating custom keybindings.
local glimmer = require("tiny-glimmer.lib")
-- Animate current line with fade effect
vim.keymap.set("n", "<leader>al", function()
glimmer.cursor_line("fade")
end)
-- Animate visual selection
vim.keymap.set("v", "<leader>av", function()
glimmer.visual_selection("pulse")
end)
-- Create custom animation on specific range
vim.keymap.set("n", "<leader>ac", function()
glimmer.create_animation({
range = glimmer.get_line_range(0),
duration = 500,
from_color = "#ff0000",
to_color = "#00ff00",
effect = "fade",
})
end)
create_animation(opts)Create a simple text animation with full control over parameters.
glimmer.create_animation({
range = {
start_line = 0, -- 0-indexed start line
start_col = 0, -- 0-indexed start column
end_line = 0, -- 0-indexed end line
end_col = 10, -- 0-indexed end column
},
duration = 300, -- Animation duration in ms
from_color = "#ff0000", -- Start color (hex or highlight group)
to_color = "#00ff00", -- End color (hex or highlight group)
effect = "fade", -- Effect type (fade, pulse, bounce, etc.)
easing = "outQuad", -- Easing function (optional)
on_complete = function() -- Callback when done (optional)
print("Animation complete!")
end,
loop = false, -- Whether to loop (optional)
loop_count = 1, -- Number of loops, 0 = infinite (optional)
})
Parameters:
range (AnimationRange, required) - Text range to animateduration (number, required) - Animation duration in millisecondsfrom_color (string, required) - Start color (hex color or highlight group name)to_color (string, required) - End color (hex color or highlight group name)effect (string, optional) - Effect type, defaults to "fade"easing (string, optional) - Easing function, defaults to "linear"on_complete (function, optional) - Callback when animation completesloop (boolean, optional) - Whether to loop the animationloop_count (number, optional) - Number of times to loop (0 = infinite)create_line_animation(opts)Create a line-based animation that highlights entire lines (ignores column positions).
glimmer.create_line_animation({
range = glimmer.get_line_range(1),
duration = 400,
from_color = "DiffAdd",
to_color = "Normal",
effect = "pulse",
})
Parameters are the same as create_animation(), but start_col and end_col are ignored.
create_text_animation(opts)Alias for create_animation() that highlights specific character ranges.
create_named_animation(name, opts)Create a named animation that can be stopped later using its name.
-- Start an infinite rainbow effect
glimmer.create_named_animation("rainbow_loop", {
range = glimmer.get_line_range(0),
duration = 1000,
from_color = "#ff0000",
to_color = "#00ff00",
effect = "rainbow",
loop = true,
loop_count = 0, -- Infinite
})
-- Stop it later
vim.keymap.set("n", "<leader>x", function()
glimmer.stop_animation("rainbow_loop")
end)
Parameters:
name (string, required) - Unique identifier for this animationopts (table, required) - Same options as create_animation()stop_animation(name)Stop a named animation.
glimmer.stop_animation("my_animation_name")
create_effect(opts)Create a custom effect with your own update function.
local effect = glimmer.create_effect({
settings = {
max_duration = 500,
min_duration = 300,
chars_for_max_duration = 10,
custom_color = "#ff00ff",
},
update_fn = function(self, progress)
-- Return color and progress for current frame
-- progress is between 0 and 1
local alpha = math.floor(progress * 255)
local color = string.format("#%02x00ff", alpha)
return color, progress
end,
builder = function(self)
-- Optional: Build initial data
return { initial_state = true }
end,
})
Convenience functions for common animation patterns.
cursor_line(effect, opts)Animate the current cursor line.
-- Simple usage
glimmer.cursor_line("pulse")
-- With custom settings
glimmer.cursor_line("fade", {
max_duration = 600,
from_color = "#ff0000",
loop = true,
loop_count = 3,
})
-- With effect configuration
glimmer.cursor_line({
name = "pulse",
settings = {
max_duration = 800,
pulse_count = 3,
}
})
visual_selection(effect, opts)Animate the current visual selection.
vim.keymap.set("v", "<leader>v", function()
glimmer.visual_selection("bounce", {
max_duration = 500,
})
end)
animate_range(effect, range, opts)Animate a specific range with an effect.
local range = {
start_line = 5,
start_col = 0,
end_line = 10,
end_col = 20,
}
glimmer.animate_range("fade", range, {
from_color = "DiffDelete",
to_color = "Normal",
})
named_animate_range(name, effect, range, opts)Create a named animation for a specific range.
glimmer.named_animate_range("highlight_1", "rainbow", glimmer.get_line_range(5), {
loop = true,
loop_count = 0,
})
-- Stop it later
glimmer.stop_animation("highlight_1")
Functions to get text ranges from various sources.
get_cursor_range()Get the range of the current cursor position (single character).
local range = glimmer.get_cursor_range()
-- Returns: { start_line = 0, start_col = 5, end_line = 0, end_col = 6 }
get_visual_range()Get the range of the current visual selection.
-- In visual mode
local range = glimmer.get_visual_range()
if range then
glimmer.animate_range("fade", range)
end
Returns nil if no visual selection exists.
get_line_range(line)Get the range for a specific line.
-- Get current line (0 or nil)
local current_line = glimmer.get_line_range(0)
-- Get line 5 (1-indexed)
local line_5 = glimmer.get_line_range(5)
Parameters:
line (number) - 1-indexed line number, or 0 for current lineget_yank_range()Get the range from the last yank operation.
local range = glimmer.get_yank_range()
if range then
glimmer.animate_range("pulse", range)
end
Returns nil if no yank operation has occurred.
-- Loop 3 times
glimmer.create_animation({
range = glimmer.get_line_range(0),
duration = 200,
from_color = "#ff0000",
to_color = "#00ff00",
loop = true,
loop_count = 3,
on_complete = function()
print("Looped 3 times!")
end,
})
-- Infinite loop (must be named to stop)
glimmer.create_named_animation("infinite", {
range = glimmer.get_line_range(0),
duration = 500,
from_color = "Visual",
to_color = "Normal",
effect = "pulse",
loop = true,
loop_count = 0, -- 0 = infinite
})
-- Stop it when done
vim.defer_fn(function()
glimmer.stop_animation("infinite")
end, 5000)
-- Animate multiple lines at once
vim.keymap.set("n", "<leader>am", function()
local start_line = vim.api.nvim_win_get_cursor(0)[1]
for i = 0, 4 do
glimmer.create_line_animation({
range = glimmer.get_line_range(start_line + i),
duration = 300 + (i * 50), -- Stagger durations
from_color = "#ff0000",
to_color = "#00ff00",
effect = "fade",
})
end
end)
-- Animate on buffer write
vim.api.nvim_create_autocmd("BufWritePost", {
callback = function()
glimmer.cursor_line("pulse", {
max_duration = 300,
from_color = "DiffAdd",
})
end,
})
-- Animate search results
vim.keymap.set("n", "n", function()
vim.cmd("normal! n")
local pos = vim.api.nvim_win_get_cursor(0)
glimmer.create_animation({
range = glimmer.get_cursor_range(),
duration = 400,
from_color = "IncSearch",
to_color = "Normal",
effect = "pulse",
})
end)
For more examples, see the examples/ directory in the repository.
Add animation support to the substitute plugin:
{
"gbprod/substitute.nvim",
dependencies = { "rachartier/tiny-glimmer.nvim" },
config = function()
require("substitute").setup({
on_substitute = require("tiny-glimmer.support.substitute").substitute_cb,
highlight_substituted_text = {
enabled = false, -- Disable built-in highlight
},
})
end,
}
Then enable it in tiny-glimmer config:
require("tiny-glimmer").setup({
support = {
substitute = {
enabled = true,
default_animation = "fade",
},
},
})
Add yanky.nvim to tiny-glimmer dependencies to ensure proper loading order:
{
"rachartier/tiny-glimmer.nvim",
dependencies = { "gbprod/yanky.nvim" },
event = "VeryLazy",
priority = 10,
config = function()
require("tiny-glimmer").setup()
end,
}
Disable your TextYankPost autocmd that calls vim.highlight.on_yank:
-- Remove or comment out this:
vim.api.nvim_create_autocmd("TextYankPost", {
callback = function()
vim.highlight.on_yank()
end,
})
When using Lazy Vim with search animations enabled, you may need to add the keys property to your plugin specification to ensure proper key mapping:
{
"rachartier/tiny-glimmer.nvim",
event = "VeryLazy",
priority = 10,
keys = {
"n",
"N",
},
config = function()
require("tiny-glimmer").setup({
overwrite = {
search = {
enabled = true,
},
},
})
end,
}
This tells Lazy Vim to load the plugin when the n or N keys are pressed, ensuring the plugin's key mappings take precedence.
Set the transparency_color option to match your background:
require("tiny-glimmer").setup({
transparency_color = "#000000", -- Your background color
})
Define a custom animation in the animations table:
require("tiny-glimmer").setup({
animations = {
my_custom = {
max_duration = 400,
chars_for_max_duration = 10,
custom_property = "value",
effect = function(self, progress)
-- Your animation logic here
return "#ff0000", progress
end,
},
},
overwrite = {
yank = {
enabled = true,
default_animation = "my_custom",
},
},
})
Check these common issues:
overwrite configauto_map = true or set up manual keybindingshijack_ft_disabled:TinyGlimmer enableAdd them to the hijack_ft_disabled list:
require("tiny-glimmer").setup({
hijack_ft_disabled = {
"alpha",
"dashboard",
"neo-tree",
},
})
MIT