TabType Reference
TabType is a macOS menu bar app that gives you universal text completion. Type ;; in any app — your terminal, editor, browser, chat — and a popup appears with completions drawn from your projects and config. Pick one with Tab or Enter, and TabType types it for you.
Why people use TabType
People usually adopt TabType for one or more of these:
- Stop retyping common messages, commands, and links.
- Complete project-aware items like git branches, npm scripts, and docs from any app.
- Keep one consistent completion workflow across terminal, editor, browser, and chat.
- Bring custom internal data into completion with custom providers.
Install
Download the latest release and drag TabType to your Applications folder:
TabType requires macOS 15 (Sequoia) or later.
Getting started
Grant accessibility permission
TabType needs accessibility access to detect ;; keystrokes and insert completions across apps. No keystrokes are recorded or stored — TabType only activates after the trigger.
On first launch, TabType opens an onboarding window. The first thing you’ll see is a permission card:

Click Grant Permission to open the macOS accessibility prompt. Then navigate to System Settings → Privacy & Security → Accessibility and enable the toggle next to TabType:

TabType detects the change automatically — no restart needed. If you skip this step, the menu bar will show a Finish Setup item so you can come back to it anytime.
Walk through the onboarding
After granting permission, walk through the remaining onboarding steps. The onboarding teaches you the ;; trigger, shows example completions across different apps, and lets you try it in a live input field. When you reach this screen, you’re all set:

TabType now runs quietly in your menu bar. Type ;; in any app to start.
Your first snippet
Everything starts with a config file at ~/.config/tabtype/config.json. TabType creates one with a couple of example snippets on first launch, but here’s the idea:
{
"snippets": [
{
"key": "sig",
"expand": "Best regards,\nYour Name"
}
]
}
Type ;;sig anywhere. The popup shows your snippet, you press Tab, and TabType replaces ;;sig with the expansion text. That’s the core loop.
The trigger defaults to ;; but you can change it in your global config with the trigger field:
{
"trigger": "///"
}
Try this next
If you just installed TabType, these are quick wins:
- Add 3 personal snippets you use daily (
sig,thanks,standup). - In a repo, type
;;@gitand browse branches/remotes. - Type
;;@file readmeto jump to common file paths. - Add one custom provider (bookmarks or Docker) and test
;;@your-scope.
Snippet types
There are two kinds of snippets: text and URL.
A text snippet inserts text at the cursor. You’ve already seen one — key plus expand:
{ "key": "addr", "expand": "123 Main St, Springfield, IL 62701" }
A URL snippet opens a link in your default browser instead of inserting text. Use url instead of expand:
{ "key": "docs", "url": "https://docs.example.com" }
URL snippets support custom schemes like obsidian://, slack://, and vscode:// — not just https.
Every snippet must have exactly one of expand or url — not both.
Two optional fields work on either type:
label— a display name shown in the popup instead of the key. Useful when the key is short but you want something more descriptive.description— extra context shown in the detail panel when the snippet is highlighted.
{
"key": "gh",
"url": "https://github.com/myorg/myapp",
"label": "GitHub Repo",
"description": "Opens the main repository page"
}
Template variables
Text snippets can include template variables that expand when the snippet is accepted:
| Variable | Description | Example output |
|---|---|---|
{{date}} | Current date (yyyy-MM-dd) | 2026-02-20 |
{{date:FORMAT}} | Current date with custom format | {{date:MM/dd/yyyy}} → 02/20/2026 |
{{time}} | Current time (HH:mm) | 14:30 |
{{time:FORMAT}} | Current time with custom format | {{time:HH:mm:ss}} → 14:30:45 |
{{clipboard}} | Current clipboard text | (clipboard contents) |
{{uuid}} | Random UUID v4 | 550e8400-e29b-41d4-a716-446655440000 |
Format strings use Swift DateFormatter tokens (yyyy-MM-dd, HH:mm:ss). All date/time variables in a single snippet share the same timestamp, so they’re always consistent.
Snippet composition
Snippets can reference other snippets by key using the same {{key}} syntax. Built-in variables always take priority over snippet references. Composition resolves recursively up to 10 levels deep; circular references are preserved literally.
{
"snippets": [
{ "key": "name", "expand": "Alice" },
{ "key": "sig", "expand": "Best regards,\n{{name}}" }
]
}
Accepting sig produces: Best regards,\nAlice.
Built-in snippets
TabType ships with five built-in snippets available to all users. Define a snippet with the same key in your config to override any built-in.
| Key | Expansion | Description |
|---|---|---|
date | {{date:yyyy-MM-dd}} | Today’s date (ISO format) |
now | {{date:yyyy-MM-dd HH:mm}} | Current date and time |
uuid | {{uuid}} | Random UUID v4 |
paste | {{clipboard}} | Current clipboard text |
shrug | ¯\_(ツ)_/¯ | Shrug emoticon |
Scoped search
When the popup is open, @ enters scope mode:
;;@opens the full scope picker.;;@partialfilters the scope picker by alias/title.;;@scope queryruns scoped search (@git featureand@git/featureboth work).;;@@querysearches snippets for a literal@query(escape hatch).
Unknown @... input stays in scope-picker filtering mode by default (it does not fall back to normal snippet search unless you use @@).
Common examples:
;;@to open all available scopes.;;@gito filter to Git-related scopes before selecting one.;;@git featto find branches/remotes quickly.;;@file confto find file paths.;;@skill perfto find skill docs.;;@your-provider queryfor your custom provider.
Scope reference
Project sources:
| Scope | Short | What it shows |
|---|---|---|
@file | @f | Files in the project (fuzzy path search) |
@git | @g | Git branches, tags, and remotes |
@branch | @b | Git refs (branches and tags) |
@tag | @t | Git refs (branches and tags) |
@remote | @r | Git remotes only |
@npm | @n | npm scripts from package.json |
@make | @m | Makefile, Justfile, and Taskfile targets |
@env | @e | Environment variables from .env files |
@skill | @s | AI agent skills (SKILL.md files) |
@doc | @d | Project docs (CLAUDE.md, AGENTS.md, README.md) |
@{scope} | — | Custom provider results (scope defaults to provider id) |
Manual sources:
| Scope | Short | What it shows |
|---|---|---|
@user | @u | Your manual snippets from config |
@app | @a | App-scoped snippets for the current app |
Special filters:
| Scope | What it does |
|---|---|
@url, @link, @l | Shows only URL snippets across all sources |
Project configuration
When you’re working in a specific project, you can add a .tabtype.json file at the project root. This file also acts as a project marker — TabType uses it to recognize the directory as a project worth scanning.
Project config merges with your global config. You only need to include the fields you want to override:
{
"snippets": [
{ "key": "api", "expand": "https://api.staging.example.com" }
],
"settings": {
"scan_depth": 3
}
}
The merge rules are straightforward:
- Snippets: if a project snippet has the same key as a global one, the project version wins. Unique snippets from both sides are kept.
- Settings: project values override global values per-field. Anything you don’t specify inherits from global. One exception:
ignore_patternsreplaces the global array entirely rather than merging. - Custom providers: project providers with the same
idreplace global ones.
Project config also supports include_paths — an array of directories to include in @file search even if they’re gitignored:
{
"include_paths": ["build/generated", ".cache/assets"]
}
App-scoped snippets
Sometimes you want snippets that only appear when a specific app is focused. Add an app_snippets array to your global config:
{
"app_snippets": [
{
"bundle_id": "com.apple.Safari",
"app_name": "Safari",
"snippets": [
{ "key": "jira", "url": "https://jira.example.com" },
{ "key": "figma", "url": "https://figma.com/files/team" }
]
}
]
}
Each entry needs a bundle_id and app_name. You can find an app’s bundle ID with:
mdls -name kMDItemCFBundleIdentifier /Applications/Safari.app
You can also set popup_position per app to control where the popup appears. Values: auto, top-left, top-right, bottom-left, bottom-right, center, bottom-center.
Provide your own data
If built-in sources are not enough, you can add your own provider with custom_providers.
Each provider gives you:
- Its own browser category.
- Its own scope (for example
@bookmarks). - Snippets generated from any shell command output.
This is useful for things like:
- bookmarks and internal links
- Docker/container commands
- SSH hosts
- internal API search
- project-specific helper commands
Example: bookmarks from a TSV file
If you keep shortcuts in ~/.bookmarks.tsv, expose them directly in TabType:
docs Documentation https://docs.example.com Project docs
jira Jira Board https://jira.example.com Team issues
fig Figma File https://figma.com/file/abc123 Design source
Format shown above is: key<TAB>label<TAB>expansion<TAB>description.
{
"custom_providers": [
{
"id": "bookmarks",
"label": "Bookmarks",
"command": "cat ~/.bookmarks.tsv",
"mode": "scan",
"ttl": 300
}
]
}
You can then browse the Bookmarks category or type ;;@bookmarks.
Custom scopes also appear in ;;@ and are filterable with ;;@bo....
Example: Docker helpers
Turn running containers into ready-to-use commands:
{
"custom_providers": [
{
"id": "docker",
"label": "Docker",
"command": "docker ps --format '{{.Names}}\tdocker exec -it {{.Names}} bash\t{{.Image}} ({{.Status}})'",
"mode": "scan",
"ttl": 30
}
]
}
Each row becomes a snippet you can insert in terminal, chat, or docs.
Example: live API search
Use live mode when results should depend on what you typed:
{
"custom_providers": [
{
"id": "api-search",
"label": "API Search",
"command": "curl -s \"https://api.internal/search?q=$TABTYPE_QUERY\" | jq -c '[.results[] | {key: .id, label: .name, expansion: .id, description: .summary}]'",
"mode": "live",
"debounce": 300
}
]
}
Type ;;@api-search your-query to run it.
Scan mode vs live mode
| Scan | Live | |
|---|---|---|
| Runs | On project switch or manual refresh (Cmd+R) | On every keystroke (debounced) |
| Gets the query? | No | Yes, via TABTYPE_QUERY env var |
| Caching | TTL-based (default 30s) | No cache |
| Timeout | 10 seconds | 3 seconds |
| Use when | Data is relatively stable | Results depend on what the user typed |
Both modes receive PROJECT_ROOT as an environment variable. Live mode also gets TABTYPE_QUERY, TABTYPE_APP_BUNDLE_ID, TABTYPE_APP_NAME, and TABTYPE_CASE_SENSITIVE ("0" or "1").
Provider output formats (reference)
Provider output is auto-detected. TSV (one snippet per line, tab-separated):
label
label expansion
label expansion description
key label expansion description
Column behavior:
- 1 col:
key=label=expansion - 2 col:
key=label,expansion - 3 col:
key=label,expansion,description - 4 col:
key,label,expansion,description
JSON (starts with [ or {):
[
{ "label": "deploy", "expansion": "deploy --env staging", "description": "Deploy to staging" },
{ "key": "stg-deploy", "label": "Deploy to Staging", "expansion": "deploy --env staging" },
{ "label": "docs", "url": "https://docs.example.com" }
]
A global semaphore limits concurrent shell commands to 2, so keep commands fast. Use scan mode with a reasonable TTL for stable data, and reserve live mode for when results genuinely depend on the query.
Reference appendix
Settings reference
All settings live under the settings key. Theme settings are global-only — project configs can’t override them.
| Setting | Default | Description | Project? |
|---|---|---|---|
case_sensitive | false | Case-sensitive matching | Yes |
scan_depth | 5 | Max directory depth for file scanning | Yes |
ignore_patterns | ["node_modules", ".git", "dist", "build"] | Directories to skip during scan | Yes |
max_branch_age_days | 90 | Hide git branches older than this (0 = show all) | Yes |
file_insertion_mode | "projectRelative" | How @file paths are inserted: "projectRelative" or "filenameOnly" | No |
popup_position | "auto" | Popup anchor: auto, top-left, top-right, bottom-left, bottom-right, center, bottom-center | No |
font_size | 16 | Popup text size | No |
font_color | "#FFFFFF" | Hex color for popup text. Ignored while Glass is active. | No |
row_padding | 7 | Vertical padding per row in pixels | No |
row_height | 32 | Row height in pixels | No |
badge_palette | "minimal" | Source badge colors: "pastel", "saturated", or "minimal" | No |
popup_width | — | Popup width in pixels | No |
detail_width | 270 | Detail panel width in pixels | No |
material | "glass" on macOS 26+, "solid" on older versions | Popup material: "glass" or "solid" | No |
appearance | "dark" | Solid popup appearance: "dark" or "light" | No |
Appearance
In Preferences, Appearance is a single picker with Glass, Dark, and Light.
- Pick
Glassto use Liquid Glass when available. - Pick
DarkorLightfor a solid popup style. - If Glass is unavailable (older macOS or Reduce Transparency enabled), TabType keeps your Glass preference and shows a small note explaining why it’s not active right now.
font_coloronly applies to the solid styles.
Equivalent config example:
{
"settings": {
"material": "glass",
"appearance": "dark"
}
}
Browser categories
When you open the popup with an empty query, it shows a browser with sidebar categories. The providers array in your config controls which categories appear and in what order:
{
"providers": ["recent", "files", "skills", "git", "scripts", "manual"]
}
The default order is: recent, files, app, manual, skills, git, docs, scripts. Remove a category ID to hide it, or reorder the array to change order. Unknown IDs are silently ignored. Custom provider IDs are auto-appended if missing.
Disabling a category is execution-gating, not just UI hiding: disabled sources are skipped during provider build, scan, watcher setup/flush, and refresh paths.