What Are Neovim Code Snippets?
Code snippets are pre-defined templates of source code that expand into larger blocks of text with just a few keystrokes. In Neovim, a snippet system allows you to type a short trigger word (like for or useEffect) and have it instantly expand into a complete code structureβoften with tab-stoppable placeholders that let you fill in variable names, types, and other dynamic values without breaking your flow.
Unlike simple abbreviations or macro recordings, modern snippet engines support:
- Placeholder jumping β press Tab to move between editable fields
- Variable interpolation β dynamically insert environment variables, file names, dates
- Choice menus β select from predefined options at a placeholder position
- Transformations β apply regex substitutions to placeholder values
- Nested expansion β expand one snippet inside another
The most popular snippet engine for Neovim today is LuaSnip, a Lua-based snippet plugin that integrates seamlessly with completion engines like nvim-cmp and auto-completion sources. It supports both its own powerful Lua snippet format and VSCode-style JSON snippets, giving you access to thousands of community-maintained snippets out of the box.
Why Snippets Matter for Developer Productivity
Snippets eliminate repetitive typing, reduce typos, and enforce consistent code patterns across your projects. Here's why they're indispensable in a professional workflow:
Speed and Accuracy
Typing useEffect and pressing Tab expands into a complete React hook with dependency array, cleanup function placeholder, and tab stops for the callback bodyβall in under a second. This is dramatically faster than manually typing the boilerplate and ensures you never forget the dependency array or the return cleanup function.
Consistency Across Codebases
Team-wide snippet configurations ensure everyone uses the same patterns. A snippet for a new API endpoint might enforce logging, error handling, and response formatting that matches your organization's standards. No more "I forgot the try/catch" code review comments.
Reducing Cognitive Load
Memorizing verbose framework boilerplate (think Redux reducers, Angular component decorators, or Kubernetes resource YAML) consumes mental bandwidth. Snippets externalize that knowledge, letting you focus on business logic rather than syntax recall.
Context-Aware Expansion
Advanced setups with nvim-cmp can show snippets alongside LSP completions. When you type try, you might see both the language server's completion for try and a snippet for a full try/catch blockβchoose whichever serves your immediate need.
Setting Up Snippet Support in Neovim
Here is a complete, production-ready configuration using LuaSnip with nvim-cmp. This setup gives you both custom Lua snippets and community snippets from the friendly-snippets collection (which includes VSCode-style snippets for dozens of languages).
Step 1: Install Required Plugins
Using a modern plugin manager like lazy.nvim:
-- ~/.config/nvim/lua/plugins/snippets.lua
return {
-- Snippet engine
{
"L3MON4D3/LuaSnip",
version = "v2.*",
build = "make install_jsregexp", -- enables regex transforms
dependencies = {
"rafamadriz/friendly-snippets", -- community snippets collection
},
config = function()
require("luasnip").setup({
history = true,
update_events = "TextChanged,TextChangedI",
delete_check_events = "TextChanged,InsertLeave",
ext_opts = {
[require("luasnip.util.types").choiceNode] = {
active = { hl_group = "LuaSnipChoiceNodeActive" },
visited = { hl_group = "LuaSnipChoiceNodeVisited" },
},
},
})
-- Load friendly-snippets
require("luasnip.loaders.from_vscode").lazy_load()
-- Optionally load custom Lua snippets from a directory
require("luasnip.loaders.from_lua").load({ paths = "~/.config/nvim/lua/snippets" })
end,
},
-- Completion engine (optional but recommended)
{
"hrsh7th/nvim-cmp",
dependencies = {
"hrsh7th/cmp-nvim-lsp",
"hrsh7th/cmp-path",
"hrsh7th/cmp-buffer",
"saadparwaiz1/cmp_luasnip", -- snippet completion source
},
config = function()
local cmp = require("cmp")
local luasnip = require("luasnip")
cmp.setup({
snippet = {
expand = function(args)
luasnip.lsp_expand(args.body)
end,
},
sources = cmp.config.sources({
{ name = "nvim_lsp" },
{ name = "luasnip" }, -- snippet source
{ name = "path" },
{ name = "buffer" },
}),
mapping = cmp.mapping.preset.insert({
["<Tab>"] = cmp.mapping(function(fallback)
if cmp.visible() then
cmp.select_next_item()
elseif luasnip.expand_or_locally_jumpable() then
luasnip.expand_or_jump()
else
fallback()
end
end, { "i", "s" }),
["<S-Tab>"] = cmp.mapping(function(fallback)
if cmp.visible() then
cmp.select_prev_item()
elseif luasnip.locally_jumpable(-1) then
luasnip.jump(-1)
else
fallback()
end
end, { "i", "s" }),
["<CR>"] = cmp.mapping.confirm({ select = true }),
}),
})
end,
},
}
Step 2: Verify the Setup
Open a TypeScript file and type useEffect. You should see the snippet appear in the completion menu (if using nvim-cmp). Press Tab to select and expand it. The snippet expands with tab-stoppable placeholders. Press Tab again to jump to the next placeholder, Shift-Tab to go back.
Step 3: Key Mapping Reference
Here's a standalone key mapping setup for LuaSnip without nvim-cmpβuseful if you prefer manual snippet expansion:
-- ~/.config/nvim/lua/config/snippets-keymaps.lua
local ls = require("luasnip")
vim.keymap.set({ "i", "s" }, "<C-k>", function()
if ls.expand_or_jump() then
return
end
vim.api.nvim_feedkeys(vim.keycode("<C-k>"), "n", false)
end, { desc = "Expand snippet or jump to next placeholder" })
vim.keymap.set({ "i", "s" }, "<C-j>", function()
if ls.jump(-1) then
return
end
vim.api.nvim_feedkeys(vim.keycode("<C-j>"), "n", false)
end, { desc = "Jump to previous placeholder" })
vim.keymap.set({ "i", "s" }, "<C-l>", function()
if ls.choice_active() then
ls.change_choice(1)
end
end, { desc = "Cycle through snippet choices" })
Creating Your First Snippets
LuaSnip supports two snippet formats: a Lua DSL that offers maximum power and flexibility, and VSCode-style JSON snippets for simpler, portable definitions. Let's explore both.
Lua Format (Recommended for Custom Snippets)
Create a file in your snippets directory (e.g., ~/.config/nvim/lua/snippets/). The file name typically matches the language filetype. Here's a complete example for TypeScript React:
-- ~/.config/nvim/lua/snippets/typescriptreact.lua
local ls = require("luasnip")
local s = ls.snippet
local t = ls.text_node
local i = ls.insert_node
local f = ls.function_node
local c = ls.choice_node
local d = ls.dynamic_node
local sn = ls.snippet_node
local fmt = require("luasnip.ext.fmt")
return {
-- Basic functional component
s("fc", fmt([[
function {}() {{
return (
<div>
{}
</div>
);
}}
export default {};
]], {
i(1, "MyComponent"), -- component name
i(2, "Hello World"), -- JSX content
i(3, "MyComponent"), -- export name (mirrors first by default)
})),
-- useEffect hook with cleanup
s("useef", fmt([[
useEffect(() => {{
{}
return () => {{
{}
}};
}}, [{}]);
]], {
i(1, "// effect logic"),
i(2, "// cleanup logic"),
i(3, "dependency"),
})),
-- useState hook with type parameter
s("uset", fmt([[
const [{}, set{}] = useState<{}>({});
]], {
i(1, "state"),
f(function(args)
-- Capitalize first letter of state name
local state_name = args[1][1] or "State"
return state_name:sub(1,1):upper() .. state_name:sub(2)
end, { 1 }),
i(2, "string"),
i(3, '""'),
})),
-- Arrow function export with TypeScript
s("afexp", fmt([[
export const {} = ({}: {}) => {} => {{
{}
}};
]], {
i(1, "functionName"),
i(2, "param"),
i(3, "ParamType"),
c(4, {
t("void"),
t("Promise<void>"),
t("JSX.Element"),
}),
i(5, "// function body"),
})),
-- Simple console.log with dynamic filename
s("clog", fmt([[
console.log("[{}:{}] {}:", {});
]], {
f(function()
return vim.fn.expand("%:t") -- current filename
end, {}),
f(function()
return vim.fn.line(".") -- current line number
end, {}),
i(1, "debug"),
i(2, "variable"),
})),
}
VSCode-Style JSON Format
For portability or when contributing to community collections, use JSON snippets. Place them in a directory structure like ~/.config/nvim/snippets/package.json or use the friendly-snippets collection's format. Here's a standalone JSON snippet file example:
{
"Print to console": {
"prefix": "log",
"body": ["console.log('${1:variable}:', ${1});", "${0}"],
"description": "Log variable to console"
},
"For Loop": {
"prefix": "for",
"body": [
"for (let ${1:i} = 0; ${1:i} < ${2:array}.length; ${1:i}++) {",
"\t${3:// body}",
"}",
"${0}"
],
"description": "For loop over array"
},
"React Functional Component": {
"prefix": "rfc",
"body": [
"import React from 'react';",
"",
"const ${1:ComponentName}: React.FC = () => {",
"\treturn (",
"\t\t<div>",
"\t\t\t${2:content}",
"\t\t</div>",
"\t);",
"};",
"",
"export default ${1:ComponentName};",
"${0}"
],
"description": "React functional component"
}
}
To load these JSON snippets, register the directory in your LuaSnip config:
require("luasnip.loaders.from_vscode").load({
paths = "~/.config/nvim/snippets/custom-json-snippets"
})
Advanced Snippet Features
LuaSnip's real power lies in its advanced node types. Here are the most useful ones in production snippet authoring.
Function Nodes: Dynamic Content
Function nodes execute Lua code at expansion time to generate dynamic content. They receive the values of referenced insert nodes as arguments.
-- Snippet that generates a timestamped log statement
s("tlog", {
t("// Log created at: "),
f(function()
return os.date("%Y-%m-%d %H:%M:%S")
end, {}),
t({ "", "console.log(" }),
i(1, '"message"'),
t(");"),
})
-- Snippet with transformation: capitalize first letter
s("capvar", {
f(function(args)
local str = args[1][1] or "variable"
return str:sub(1,1):upper() .. str:sub(2)
end, { 1 }),
i(1, "variable"),
})
Choice Nodes: Multiple Options at a Placeholder
Choice nodes present a list of alternatives the user can cycle through with a keymap (typically Ctrl+L or similar).
s("httpmethod", {
c(1, {
t("GET"),
t("POST"),
t("PUT"),
t("PATCH"),
t("DELETE"),
}),
t(" "),
i(2, "/api/endpoint"),
})
-- Choice node with snippet nodes (complex choices)
s("component-type", {
c(1, {
sn(nil, { t("React.FC"), t("<"), i(1, "Props"), t(">") }),
sn(nil, { t("React.Component"), t("<"), i(1, "Props"), t(", "), i(2, "State"), t(">") }),
t("React.ForwardRefExoticComponent"),
}),
})
Dynamic Nodes: Context-Aware Nested Content
Dynamic nodes re-evaluate their content based on changes in other insert nodes. They're perfect for generating repeated patterns.
s("table-fields", {
i(1, "field_count"),
t({ "", "fields: {" }),
d(2, function(args)
local count = tonumber(args[1][1]) or 0
local nodes = {}
for i = 1, count do
table.insert(nodes, t({ "", "\t" }))
table.insert(nodes, i(i + 1, "field" .. i))
table.insert(nodes, t(": "))
table.insert(nodes, i(i + count + 1, "type" .. i))
if i < count then
table.insert(nodes, t(","))
end
end
return sn(nil, nodes)
end, { 1 }),
t({ "", "}" }),
})
Transform Nodes: Regex Substitutions
Transform nodes apply regular expression replacements to placeholder content. They require the jsregexp build step from LuaSnip installation.
-- Convert kebab-case to camelCase
s("kebab-to-camel", {
i(1, "kebab-case-string"),
t(" => "),
f(function(args)
local str = args[1][1] or ""
-- Manual transformation without jsregexp
return str:gsub("%-(%w)", function(c)
return c:upper()
end)
end, { 1 }),
})
-- Using jsregexp transform syntax (requires build)
s("camel-transform", fmt("const {} = require('{}');", {
f(function(args)
return args[1][1]:gsub("%-(%w)", function(c) return c:upper() end)
end, { 1 }),
i(1, "my-module-name"),
}))
Auto-Snippet: Trigger on Pattern Match
Auto-snippets expand automatically when a specific pattern is typed, without requiring an explicit expansion key. Useful for automatic bracket pairing or small utility expansions.
s("autopair", {
t("("), i(1), t(")"),
}, { condition = function()
-- Only expand when preceded by specific characters
local line = vim.fn.getline(".")
local col = vim.fn.col(".")
local char_before = line:sub(col - 1, col - 1)
return char_before:match("[%s%(%)%[%]{}]") ~= nil
end, show_condition = function()
return true
end, type = "autosnippet" })
Built-in Snippet Sources and Frameworks
Writing every snippet from scratch is unnecessary. The ecosystem provides extensive pre-built collections.
friendly-snippets
This collection contains VSCode-compatible snippets for over 60 languages and frameworks. It includes everything from Python docstrings to React hooks, Docker Compose files, and LaTeX math environments. Once installed (as shown in the setup above), snippets load automatically for matching filetypes.
-- The friendly-snippets collection is loaded automatically
-- via require("luasnip.loaders.from_vscode").lazy_load()
-- No additional configuration needed.
-- You can extend it with your own overrides:
require("luasnip.loaders.from_vscode").lazy_load({
paths = "~/.config/nvim/snippets/my-overrides",
-- my-overrides snippets with same prefix take precedence
})
vim-snippets (Legacy)
The classic honza/vim-snippets collection works with LuaSnip via its snipmate loader. If you're migrating from an older setup:
require("luasnip.loaders.from_snipmate").lazy_load({
paths = "~/.config/nvim/snipmate-snippets"
})
Language-Specific Collections
Many Neovim users maintain language-specific snippet repositories. For example, nvim-lua-snippets provides snippets for Neovim plugin development itself. You can load them alongside friendly-snippets:
-- Load multiple snippet sources
require("luasnip.loaders.from_vscode").lazy_load() -- friendly-snippets
require("luasnip.loaders.from_lua").load({
paths = {
"~/.config/nvim/lua/snippets", -- your custom Lua snippets
"~/projects/nvim-lua-snippets/lua", -- community Lua snippets
}
})
Best Practices for Organizing Snippets
1. Filetype-Scoped Files
Place snippets in files named after the Neovim filetype. For React with TypeScript, use typescriptreact.lua or typescriptreact.json. For plain JavaScript, use javascript.lua. This ensures snippets only appear in relevant buffers.
~/.config/nvim/lua/snippets/
βββ lua.lua # snippets for Lua development
βββ typescriptreact.lua # React + TSX snippets
βββ python.lua # Python snippets
βββ rust.lua # Rust snippets
βββ markdown.lua # Markdown snippets
βββ all.lua # Global snippets (available everywhere)
2. Use Descriptive Prefixes
Prefixes should balance brevity with clarity. fc for "functional component" is terse but memorable. useef for "useEffect" is a good abbreviation. Avoid single-letter prefixes that collide with common words.
-- Good prefixes
s("fc", ...) -- functional component
s("uset", ...) -- useState
s("useref", ...) -- useRef
s("tryc", ...) -- try/catch block
s("afexp", ...) -- arrow function export
-- Avoid
s("f", ...) -- too ambiguous
s("a", ...) -- meaningless
3. Document Snippets with Descriptions
Both Lua and JSON formats support descriptions. These appear in completion menus and help other developers (or your future self) understand snippet purpose.
s("uset", fmt("const [{}, set{}] = useState<{}>({});", {
i(1, "state"),
f(function(args)
return (args[1][1] or "state"):sub(1,1):upper() .. (args[1][1] or "state"):sub(2)
end, { 1 }),
i(2, "type"),
i(3, "initialValue"),
}), { desc = "React useState hook with setter" })
4. Version Control Your Snippets
Treat your snippet collection like any other code artifact. Store it in a Git repository, share it across machines via dotfiles management, and iterate on snippets based on real usage.
# Typical dotfiles structure
~/.dotfiles/
βββ nvim/
β βββ lua/
β β βββ plugins/
β β β βββ snippets.lua # plugin config
β β βββ snippets/
β β βββ typescriptreact.lua
β β βββ python.lua
β β βββ lua.lua
β βββ init.lua
βββ setup.sh # symlink script
5. Use Placeholder Mirroring
When the same value appears multiple times in a snippet, mirror insert nodes so typing once populates all instances:
-- The same insert node index (1) mirrors the value
s("testcomp", fmt([[
describe('{}', () => {{
it('should {}', () => {{
const {} = render(<{} />);
expect({}).toBeInTheDocument();
}});
}});
]], {
i(1, "MyComponent"), -- this value appears in all {1} positions
i(2, "render correctly"),
i(1), -- mirrors the component name
i(1), -- mirrors again
i(1), -- and again
}))
6. Leverage Environment-Specific Context
Create environment-aware snippets that adapt to your project structure. Function nodes can read buffer variables, file paths, or even git branch names:
-- Snippet that imports from the current file's relative path
s("import-self", {
t("import { "),
i(1, "Component"),
t(' } from "./'),
f(function()
local path = vim.fn.expand("%:.:r") -- relative path without extension
return path
end, {}),
t('";'),
})
7. Test Snippets Before Heavy Use
Open a scratch buffer, set the filetype to your target language (:set ft=typescriptreact), and test snippet expansion, placeholder jumping, and edge cases. LuaSnip's :LuaSnipListAvailable command shows all active snippets for the current buffer.
Conclusion
Neovim code snippets, powered by LuaSnip, transform repetitive coding patterns into fluid, single-keystroke operations. By combining the speed of snippet expansion with the intelligence of LSP completions through nvim-cmp, you create a development environment where boilerplate writes itself and your focus stays squarely on solving problems. The investment in learning the LuaSnip DSL pays dividends: from simple text expansions to complex dynamic nodes that generate entire scaffolding based on a single input, the system scales with your needs. Start with the friendly-snippets collection for immediate productivity, then gradually build your own library of context-aware, project-specific snippets. Treat your snippet collection as living codeβversion it, refine it, and let it evolve alongside your skills and your codebase's patterns.