Skip to content

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:

Download TabType

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:

Onboarding first screen

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

Granting accessibility permission in System Settings

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:

Onboarding complete

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:

  1. Add 3 personal snippets you use daily (sig, thanks, standup).
  2. In a repo, type ;;@git and browse branches/remotes.
  3. Type ;;@file readme to jump to common file paths.
  4. 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:

VariableDescriptionExample 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 v4550e8400-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.

KeyExpansionDescription
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

When the popup is open, @ enters scope mode:

  • ;;@ opens the full scope picker.
  • ;;@partial filters the scope picker by alias/title.
  • ;;@scope query runs scoped search (@git feature and @git/feature both work).
  • ;;@@query searches 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.
  • ;;@gi to filter to Git-related scopes before selecting one.
  • ;;@git feat to find branches/remotes quickly.
  • ;;@file conf to find file paths.
  • ;;@skill perf to find skill docs.
  • ;;@your-provider query for your custom provider.

Scope reference

Project sources:

ScopeShortWhat it shows
@file@fFiles in the project (fuzzy path search)
@git@gGit branches, tags, and remotes
@branch@bGit refs (branches and tags)
@tag@tGit refs (branches and tags)
@remote@rGit remotes only
@npm@nnpm scripts from package.json
@make@mMakefile, Justfile, and Taskfile targets
@env@eEnvironment variables from .env files
@skill@sAI agent skills (SKILL.md files)
@doc@dProject docs (CLAUDE.md, AGENTS.md, README.md)
@{scope}Custom provider results (scope defaults to provider id)

Manual sources:

ScopeShortWhat it shows
@user@uYour manual snippets from config
@app@aApp-scoped snippets for the current app

Special filters:

ScopeWhat it does
@url, @link, @lShows 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_patterns replaces the global array entirely rather than merging.
  • Custom providers: project providers with the same id replace 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.

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

ScanLive
RunsOn project switch or manual refresh (Cmd+R)On every keystroke (debounced)
Gets the query?NoYes, via TABTYPE_QUERY env var
CachingTTL-based (default 30s)No cache
Timeout10 seconds3 seconds
Use whenData is relatively stableResults 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.

SettingDefaultDescriptionProject?
case_sensitivefalseCase-sensitive matchingYes
scan_depth5Max directory depth for file scanningYes
ignore_patterns["node_modules", ".git", "dist", "build"]Directories to skip during scanYes
max_branch_age_days90Hide 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-centerNo
font_size16Popup text sizeNo
font_color"#FFFFFF"Hex color for popup text. Ignored while Glass is active.No
row_padding7Vertical padding per row in pixelsNo
row_height32Row height in pixelsNo
badge_palette"minimal"Source badge colors: "pastel", "saturated", or "minimal"No
popup_widthPopup width in pixelsNo
detail_width270Detail panel width in pixelsNo
material"glass" on macOS 26+, "solid" on older versionsPopup 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 Glass to use Liquid Glass when available.
  • Pick Dark or Light for 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_color only 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.