← Back to DevBytes

Neovim Code Snippets: Complete Guide

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:

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.

πŸš€ Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started β€” $23.99/mo
← Back to all articles