# How Zed's Snippet System Works: A Complete Guide to Adding Custom Snippets

> Discover how Zed's snippet system works. Learn to add custom snippets using JSON templates and interactive tabstops for dynamic code expansion.

- Repository: [Zed Industries/zed](https://github.com/zed-industries/zed)
- Tags: deep-dive
- Published: 2026-03-01

---

**Zed implements snippets as JSON-formatted template files stored in `~/.config/zed/snippets` that the core `snippet` crate parses into structured templates with interactive tabstops, supporting LSP-style `${1:placeholder}` syntax and choice lists for dynamic code expansion.**

The zed-industries/zed repository provides a sophisticated snippet architecture that bridges user-defined JSON configurations with runtime code expansion. Understanding how Zed's snippet system works enables developers to create reusable code templates that integrate seamlessly with the editor's completion engine and language server protocol support.

## Storage Architecture and File Discovery

Snippets reside in `~/.config/zed/snippets` (resolved via `paths::snippets_dir()`). The system distinguishes between **global snippets** and **language-specific snippets** through a scope-based file naming convention implemented in [`crates/snippets_ui/src/snippets_ui.rs`](https://github.com/zed-industries/zed/blob/main/crates/snippets_ui/src/snippets_ui.rs).

Global snippets live in [`snippets.json`](https://github.com/zed-industries/zed/blob/main/snippets.json) (defined by the constant `GLOBAL_SCOPE_FILE_NAME`), while language-specific snippets use `<language-id>.json` where the ID corresponds to the language's LSP identifier (`language.lsp_id()`). When you trigger the **Snippets → Configure Snippets** command (actions `ConfigureSnippets` and `OpenFolder` in [`snippets_ui.rs`](https://github.com/zed-industries/zed/blob/main/snippets_ui.rs)), the UI scans the directory to build `existing_scopes` and presents them in a fuzzy picker via `ScopeSelectorDelegate::new` (lines 60-74).

The `ScopeName` to filename conversion happens through the `From<ScopeName> for ScopeFileName` implementation (lines 37-44), which appends `.json` via `ScopeFileName::with_extension` (lines 28-32).

## Parsing Engine and Tabstop Resolution

When completion triggers, the `snippet` crate (specifically [`crates/snippet/src/snippet.rs`](https://github.com/zed-industries/zed/blob/main/crates/snippet/src/snippet.rs)) processes raw snippet strings through `Snippet::parse` (lines 17-41). The parser implements a state machine that:

1. **Collects raw source** and iterates characters in `parse_snippet`
2. **Detects tabstops** when encountering `$`, delegating to `parse_tabstop`
3. **Handles escapes** for `\`, `$`, and `}` per LSP specifications (lines 58-66)
4. **Processes choice lists** via `parse_choices` (lines 45-78) for syntax like `${1|a,b,c|}`
5. **Manages nested placeholders** and final tabstop `$0` insertion

The resulting `Snippet` struct contains `text` (the expanded code with placeholders removed) and an ordered `tabstops` vector defining cursor positions and choice lists. The editor uses this to position the caret, link mirrored placeholders, and render choice dropdowns.

## How to Add Custom Snippets in Zed

### Creating Global Snippets

To add a global snippet available across all file types:

1. Open the snippets folder via the command palette (**Snippets → Open Folder**) or action `snippets::OpenFolder`
2. Create or edit [`snippets.json`](https://github.com/zed-industries/zed/blob/main/snippets.json):

```json
{
  "Print Debug": {
    "prefix": "dbg",
    "body": ["println!(\"Debug: ${1:msg}\");", "$0"],
    "description": "Quick debug print"
  }
}

```

The `prefix` triggers completion, `$1` defines the first tabstop with default text "msg", and `$0` marks the final cursor position.

### Adding Language-Specific Snippets

For TypeScript or JavaScript snippets, create [`javascript.json`](https://github.com/zed-industries/zed/blob/main/javascript.json) (which also covers JSX):

```json
{
  "React Component": {
    "prefix": "rcomp",
    "body": [
      "export const ${1|Component,Container|} = () => {",
      "  return <div>$0</div>;",
      "};"
    ],
    "description": "Skeleton for a functional React component"
  }
}

```

The `${1|Component,Container|}` syntax creates a dropdown choice list parsed by `parse_choices`, allowing users to select between alternatives at expansion time.

## Programmatic Snippet Parsing

For developers extending Zed's functionality, the snippet parser exposes a clean Rust API:

```rust
use snippet::Snippet;

fn expand_snippet(source: &str) -> anyhow::Result<String> {
    // Parse the raw snippet definition
    let parsed = Snippet::parse(source)?;
    // `parsed.text` is the snippet with placeholders removed
    Ok(parsed.text)
}

// Example
let result = expand_snippet(r#"console.log("${1:msg}")${0}"#)?;
assert_eq!(result, r#"console.log("")"#);

```

This matches the internal implementation used when the completion engine receives snippet-type completions from language servers or Zed's *simple-completion-language-server*.

## Advanced Snippet Syntax

Zed supports the full LSP snippet specification implemented in the parsing functions:

- **Tabstops**: `$1`, `$2` define cursor positions
- **Placeholders**: `${1:default}` provide default text
- **Choice lists**: `${1|opt1,opt2|}` render UI dropdowns
- **Final tabstop**: `$0` or implicit end-of-snippet positioning (lines 24-40 in [`snippet.rs`](https://github.com/zed-industries/zed/blob/main/snippet.rs))
- **Escaping**: `\\$` and `\\}` render literal characters

The parser validates nested structures and automatically inserts an implicit final tabstop if `$0` is omitted.

## Summary

- **Storage**: Snippets live in `~/.config/zed/snippets` as JSON files, with global snippets in [`snippets.json`](https://github.com/zed-industries/zed/blob/main/snippets.json) and language-specific ones in `<lsp_id>.json`
- **UI Layer**: The `snippets_ui` crate provides `ConfigureSnippets` and `OpenFolder` actions for file discovery and editing, mapping scopes to filenames via `ScopeFileName::with_extension`
- **Parsing**: The `snippet` crate's `Snippet::parse` function processes LSP-style syntax including tabstops, placeholders, and choice lists into structured `TabStop` data
- **Expansion**: Parsed snippets yield `text` and `tabstops` vectors that drive cursor positioning, linked placeholder updates, and interactive choice selection in the editor

## Frequently Asked Questions

### Where does Zed store custom snippets?

Zed stores snippets in `~/.config/zed/snippets` (resolved via `paths::snippets_dir()`). Global snippets use [`snippets.json`](https://github.com/zed-industries/zed/blob/main/snippets.json), while language-specific files follow the naming convention `<language-lsp-id>.json` as determined by the language server's identifier and the `ScopeFileName` mapping in [`snippets_ui.rs`](https://github.com/zed-industries/zed/blob/main/snippets_ui.rs).

### What snippet syntax does Zed support?

Zed supports standard LSP snippet syntax including numbered tabstops (`$1`), placeholders with defaults (`${1:value}`), choice lists (`${1|a,b|}`), and the final tabstop (`$0`). The parser in [`crates/snippet/src/snippet.rs`](https://github.com/zed-industries/zed/blob/main/crates/snippet/src/snippet.rs) handles escaping for special characters like `$` and `}` according to LSP specifications.

### How do I create a snippet with multiple choice options?

Use the choice list syntax `${1|option1,option2,option3|}` in the snippet body. When expanded, Zed renders a dropdown at that tabstop position allowing selection between alternatives. This is parsed by the `parse_choices` function (lines 45-78) in the snippet crate.

### How do snippet tabstops work during editing?

When you insert a snippet, Zed places the cursor at the first tabstop (`$1`). Pressing `Tab` advances to `$2`, and so on, until reaching `$0` (the final position). Linked placeholders with the same index update simultaneously, and choice lists display a dropdown for selection. This behavior is driven by the `TabStop` ranges vector generated during `Snippet::parse` in [`crates/snippet/src/snippet.rs`](https://github.com/zed-industries/zed/blob/main/crates/snippet/src/snippet.rs).