Custom Snippets and Templates in VS Code
A comprehensive guide to creating custom code snippets and project templates in VS Code for faster development with variables, transforms, and shared snippet libraries.
Custom Snippets and Templates in VS Code
Typing the same boilerplate code repeatedly is a waste of developer time. VS Code's snippet system handles everything from simple text expansion to complex templates with dynamic variables, cursor positioning, and regex transforms. Once you learn the snippet syntax, you can encode your team's patterns into reusable templates that new developers pick up instantly.
I maintain snippet libraries for every project I work on. The ten minutes spent creating a good snippet saves hours across the team over its lifetime. This guide covers every snippet feature, from basic tab triggers to advanced variable transforms.
Prerequisites
- VS Code installed
- Familiarity with the VS Code command palette
- Basic understanding of JSON syntax
- Experience with the language you are creating snippets for
Creating Your First Snippet
Open the command palette (Ctrl+Shift+P) and select "Preferences: Configure User Snippets". Choose the language or create a global snippet file.
{
"Console Log": {
"prefix": "cl",
"body": "console.log($1);$0",
"description": "console.log statement"
}
}
Type cl and press Tab to expand. The cursor lands at $1 (inside the parentheses). Press Tab again to move to $0 (the final cursor position, after the semicolon).
Snippet File Locations
- User snippets:
~/.config/Code/User/snippets/(Linux),~/Library/Application Support/Code/User/snippets/(macOS) - Project snippets:
.vscode/*.code-snippetsin your project root - Extension snippets: Bundled inside extension packages
Project snippets are committed to version control. This is how you share patterns with your team.
Tab Stops and Placeholders
Tab stops ($1, $2, etc.) control where the cursor jumps when you press Tab. Placeholders provide default values.
{
"Express Route": {
"prefix": "route",
"body": [
"app.${1|get,post,put,delete,patch|}(\"${2:/api/resource}\", function(req, res) {",
"\t${3:res.json({ status: \"ok\" });}",
"});"
],
"description": "Express route handler"
}
}
Breaking down the syntax:
$1— Tab stop 1 (cursor goes here first)${1|get,post,put,delete,patch|}— Choice placeholder (dropdown menu)${2:/api/resource}— Placeholder with default text${3:res.json(...)}— Placeholder with complex default$0— Final cursor position (implicit at end if not specified)\t— Tab character for indentation
Nested Placeholders
Placeholders can nest inside each other:
{
"Try-Catch": {
"prefix": "tc",
"body": [
"try {",
"\t$1",
"} catch (${2:err}) {",
"\t${3:console.error(${2:err}.message);}",
"}"
]
}
}
Notice ${2:err} appears twice. When you change the variable name at the first occurrence, the second one updates simultaneously. This is a mirrored placeholder.
Variables
VS Code provides built-in variables for dynamic content:
{
"File Header": {
"prefix": "header",
"body": [
"/**",
" * ${TM_FILENAME_BASE}",
" * ",
" * Created: ${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE}",
" * Author: ${1:Shane}",
" * ",
" * $2",
" */"
],
"description": "File header comment"
}
}
Available Variables
TM_SELECTED_TEXT Selected text or empty string
TM_CURRENT_LINE Current line content
TM_CURRENT_WORD Word under cursor
TM_LINE_INDEX Zero-based line number
TM_LINE_NUMBER One-based line number
TM_FILENAME Current filename (e.g., app.js)
TM_FILENAME_BASE Filename without extension (e.g., app)
TM_DIRECTORY Directory of current file
TM_FILEPATH Full file path
RELATIVE_FILEPATH Relative path from workspace root
CLIPBOARD Clipboard contents
CURRENT_YEAR 4-digit year (2026)
CURRENT_YEAR_SHORT 2-digit year (26)
CURRENT_MONTH 2-digit month (02)
CURRENT_MONTH_NAME Full month (February)
CURRENT_MONTH_NAME_SHORT Short month (Feb)
CURRENT_DATE 2-digit day (13)
CURRENT_DAY_NAME Day name (Thursday)
CURRENT_HOUR 24h hour
CURRENT_MINUTE Minute
CURRENT_SECOND Second
CURRENT_SECONDS_UNIX Unix timestamp
RANDOM 6 random digits
RANDOM_HEX 6 random hex chars
UUID UUID v4
BLOCK_COMMENT_START Block comment start (e.g., /*)
BLOCK_COMMENT_END Block comment end (e.g., */)
LINE_COMMENT Line comment (e.g., //)
Practical Variable Usage
{
"Module Boilerplate": {
"prefix": "mod",
"body": [
"/**",
" * ${TM_FILENAME_BASE} module",
" * @module ${TM_FILENAME_BASE}",
" */",
"",
"var ${TM_FILENAME_BASE} = (function() {",
"\t\"use strict\";",
"",
"\tfunction ${1:init}() {",
"\t\t$2",
"\t}",
"",
"\treturn {",
"\t\t${1:init}: ${1:init}",
"\t};",
"})();",
"",
"module.exports = ${TM_FILENAME_BASE};"
],
"description": "CommonJS module with revealing pattern"
},
"Unique ID": {
"prefix": "uid",
"body": "var ${1:id} = \"${UUID}\";",
"description": "Variable with UUID"
},
"Timestamped Log": {
"prefix": "tlog",
"body": "console.log(\"[${CURRENT_HOUR}:${CURRENT_MINUTE}:${CURRENT_SECOND}]\", $1);$0",
"description": "Console log with timestamp"
}
}
Variable Transforms
Transforms modify variable values using regex. The syntax is ${variable/regex/replacement/flags}.
{
"Component from Filename": {
"prefix": "comp",
"body": [
"// ${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/} component",
"",
"var ${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/} = {",
"\tname: \"${TM_FILENAME_BASE}\",",
"",
"\tinit: function() {",
"\t\t$1",
"\t}",
"};",
"",
"module.exports = ${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/};"
],
"description": "Component from filename"
}
}
In a file called user-profile.js, this expands to:
// UserProfile component
var UserProfile = {
name: "user-profile",
init: function() {
}
};
module.exports = UserProfile;
Transform Modifiers
${variable/regex/${1:/upcase}/} UPPERCASE
${variable/regex/${1:/downcase}/} lowercase
${variable/regex/${1:/capitalize}/} Capitalize first letter
${variable/regex/${1:/camelcase}/} camelCase
${variable/regex/${1:/pascalcase}/} PascalCase
Advanced Transform Examples
{
"Getter/Setter": {
"prefix": "getset",
"body": [
"function get${1/(.*)/${1:/pascalcase}/}() {",
"\treturn this._${1:property};",
"}",
"",
"function set${1/(.*)/${1:/pascalcase}/}(${1:property}) {",
"\tthis._${1:property} = ${1:property};",
"}"
],
"description": "Getter and setter pair"
},
"Constant from Variable": {
"prefix": "const",
"body": "var ${1/(.*)/${1:/upcase}/} = ${2:value};",
"description": "Uppercase constant"
},
"Snake to Camel": {
"prefix": "s2c",
"body": "${TM_SELECTED_TEXT/(_)(\\w)/${2:/upcase}/g}",
"description": "Convert snake_case selection to camelCase"
}
}
Multi-Line and Complex Snippets
Express Application Template
{
"Express App": {
"prefix": "expressapp",
"body": [
"var express = require(\"express\");",
"var path = require(\"path\");",
"",
"var app = express();",
"var port = process.env.PORT || ${1:3000};",
"",
"// Middleware",
"app.use(express.json());",
"app.use(express.urlencoded({ extended: true }));",
"app.use(express.static(path.join(__dirname, \"public\")));",
"",
"// Routes",
"app.get(\"/\", function(req, res) {",
"\tres.json({ status: \"ok\", timestamp: new Date().toISOString() });",
"});",
"",
"app.get(\"/health\", function(req, res) {",
"\tres.json({ healthy: true });",
"});",
"",
"$0",
"",
"// Error handler",
"app.use(function(err, req, res, next) {",
"\tconsole.error(err.stack);",
"\tres.status(500).json({ error: \"Internal server error\" });",
"});",
"",
"app.listen(port, function() {",
"\tconsole.log(\"Server running on port \" + port);",
"});"
],
"description": "Complete Express application boilerplate"
}
}
Test File Template
{
"Test Suite": {
"prefix": "testfile",
"body": [
"var ${TM_FILENAME_BASE/(.+)\\.test$/$1/} = require(\"./${TM_FILENAME_BASE/(.+)\\.test$/$1/}\");",
"",
"describe(\"${TM_FILENAME_BASE/(.+)\\.test$/${1:/pascalcase}/}\", function() {",
"\tvar instance;",
"",
"\tbeforeEach(function() {",
"\t\t${1:// setup}",
"\t});",
"",
"\tafterEach(function() {",
"\t\t${2:// cleanup}",
"\t});",
"",
"\tdescribe(\"${3:method}\", function() {",
"\t\tit(\"should ${4:do something}\", function() {",
"\t\t\t${5:// arrange}",
"",
"\t\t\t${6:// act}",
"",
"\t\t\t${7:// assert}",
"\t\t\texpect(${8:result}).toBe(${9:expected});",
"\t\t});",
"",
"\t\tit(\"should handle errors\", function() {",
"\t\t\texpect(function() {",
"\t\t\t\t$10",
"\t\t\t}).toThrow();",
"\t\t});",
"\t});",
"});"
],
"description": "Test file with describe/it blocks"
}
}
For a file named userService.test.js, the import resolves to require("./userService") and the describe title becomes UserService.
API Endpoint Snippet
{
"REST Endpoint": {
"prefix": "endpoint",
"body": [
"// ${1|GET,POST,PUT,DELETE,PATCH|} ${2:/api/resource}",
"router.${1/(.*)/${1:/downcase}/}(\"${2:/api/resource}\", ${3|,authenticate\\, |}function(req, res) {",
"\ttry {",
"\t\t$4",
"\t\tres.json({",
"\t\t\tsuccess: true,",
"\t\t\tdata: ${5:result}",
"\t\t});",
"\t} catch (err) {",
"\t\tconsole.error(\"${1|GET,POST,PUT,DELETE,PATCH|} ${2:/api/resource} error:\", err.message);",
"\t\tres.status(${6|400,404,500|}).json({",
"\t\t\tsuccess: false,",
"\t\t\terror: err.message",
"\t\t});",
"\t}",
"});"
],
"description": "REST API endpoint with error handling"
}
}
Project Snippet Files
Create .vscode/project.code-snippets in your project root for shared team snippets:
{
"Project Logger": {
"scope": "javascript",
"prefix": "plog",
"body": [
"var logger = require(\"../utils/logger\");",
"",
"logger.${1|info,warn,error,debug|}(\"${2:message}\"${3:, { $4 \\}});$0"
],
"description": "Project-specific logger call"
},
"Database Query": {
"scope": "javascript",
"prefix": "dbq",
"body": [
"var db = require(\"../db\");",
"",
"function ${1:queryName}(${2:params}) {",
"\tvar sql = \"${3:SELECT * FROM ${4:table} WHERE ${5:id} = $1}\";",
"\tvar values = [${6:params.id}];",
"",
"\treturn db.query(sql, values).then(function(result) {",
"\t\treturn result.rows;",
"\t});",
"}",
"",
"module.exports = { ${1:queryName}: ${1:queryName} };"
],
"description": "Database query function"
},
"API Model": {
"scope": "javascript",
"prefix": "model",
"body": [
"var db = require(\"../db\");",
"",
"var ${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}Model = {",
"\tgetAll: function() {",
"\t\treturn db.query(\"SELECT * FROM ${TM_FILENAME_BASE}s ORDER BY id\");",
"\t},",
"",
"\tgetById: function(id) {",
"\t\treturn db.query(\"SELECT * FROM ${TM_FILENAME_BASE}s WHERE id = $1\", [id]);",
"\t},",
"",
"\tcreate: function(data) {",
"\t\tvar sql = \"INSERT INTO ${TM_FILENAME_BASE}s (${1:name}) VALUES ($1) RETURNING *\";",
"\t\treturn db.query(sql, [data.${1:name}]);",
"\t},",
"",
"\tupdate: function(id, data) {",
"\t\tvar sql = \"UPDATE ${TM_FILENAME_BASE}s SET ${1:name} = $1 WHERE id = $2 RETURNING *\";",
"\t\treturn db.query(sql, [data.${1:name}, id]);",
"\t},",
"",
"\tremove: function(id) {",
"\t\treturn db.query(\"DELETE FROM ${TM_FILENAME_BASE}s WHERE id = $1\", [id]);",
"\t}",
"};",
"",
"module.exports = ${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}Model;"
],
"description": "CRUD model for database table"
}
}
Snippet Scope Control
Restrict snippets to specific languages, file types, or contexts:
{
"HTML Template": {
"scope": "html,pug,ejs",
"prefix": "page",
"body": [
"<!DOCTYPE html>",
"<html lang=\"en\">",
"<head>",
"\t<meta charset=\"UTF-8\">",
"\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">",
"\t<title>${1:Page Title}</title>",
"</head>",
"<body>",
"\t$0",
"</body>",
"</html>"
]
},
"SQL Migration": {
"scope": "sql",
"prefix": "migration",
"body": [
"-- Migration: ${1:description}",
"-- Date: ${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE}",
"",
"BEGIN;",
"",
"-- Up",
"${2:CREATE TABLE ${3:table_name} (",
"\tid SERIAL PRIMARY KEY,",
"\t${4:column_name} ${5:TEXT} NOT NULL,",
"\tcreated_at TIMESTAMP DEFAULT NOW()",
");}",
"",
"COMMIT;",
"",
"-- Down",
"-- ${6:DROP TABLE ${3:table_name}};"
]
},
"Dockerfile": {
"scope": "dockerfile",
"prefix": "nodeapp",
"body": [
"FROM node:${1|20,18|}-alpine",
"",
"WORKDIR /app",
"",
"COPY package*.json ./",
"RUN npm ci --production",
"",
"COPY . .",
"",
"EXPOSE ${2:3000}",
"",
"HEALTHCHECK --interval=30s --timeout=3s \\",
"\tCMD wget -qO- http://localhost:${2:3000}/health || exit 1",
"",
"CMD [\"node\", \"${3:src/index.js}\"]"
]
}
}
Building a Snippet Library Extension
Package your snippets as a VS Code extension for distribution:
{
"name": "my-team-snippets",
"displayName": "My Team Snippets",
"description": "Shared code snippets for our team's Node.js projects",
"version": "1.0.0",
"engines": { "vscode": "^1.80.0" },
"categories": ["Snippets"],
"contributes": {
"snippets": [
{
"language": "javascript",
"path": "./snippets/javascript.json"
},
{
"language": "sql",
"path": "./snippets/sql.json"
},
{
"language": "dockerfile",
"path": "./snippets/dockerfile.json"
}
]
}
}
Common Issues and Troubleshooting
Snippet does not appear in IntelliSense
The scope or language mismatch prevents the snippet from showing:
Fix: Check that the snippet file matches the language. User snippet files are language-specific (e.g., javascript.json). Project .code-snippets files need the scope field.
Special characters break the snippet
Backslashes, quotes, and dollar signs need escaping in JSON:
{
"body": [
"var regex = /\\d+/g;",
"var str = \"Hello \\\"world\\\"\";",
"var price = \"\\$${1:9.99}\";"
]
}
Double backslash (\\) becomes a single backslash. \\\" becomes \". Use \\$ to insert a literal $.
Tab stops skip or jump to wrong position
Duplicate tab stop numbers cause confusing behavior. Each number should represent a single logical position:
Fix: Use unique numbers for independent positions. Use the same number only when you want mirrored editing (typing in one updates the other).
Snippet expands when you do not want it to
A short prefix like c or f triggers accidentally:
Fix: Use more specific prefixes. Configure editor.snippetSuggestions to "top", "inline", or "bottom" to control where snippets appear in IntelliSense.
Best Practices
- Use meaningful prefixes. Short enough to type quickly, long enough to avoid accidental triggers. Two to four characters is ideal.
- Place the cursor at the most likely edit point. Tab stop
$1should be where the developer needs to type first. - Provide sensible defaults in placeholders.
${1:defaultValue}saves keystrokes. Users can Tab through to accept defaults. - Use choice placeholders for constrained values.
${1|option1,option2,option3|}prevents typos and shows valid options. - Commit project snippets to version control.
.vscode/*.code-snippetsfiles are how you share patterns with your team. - Group related snippets by domain. One file for route snippets, one for model snippets, one for test snippets. This makes maintenance easier.
- Add descriptions to every snippet. Users browsing IntelliSense need to know what each snippet does before expanding it.
- Use transforms to derive names from context. Filename-based transforms eliminate the need to type the same name in multiple places.