Skip to main content

Icon guidelines

Configuring entry and UI icons for a mini tool

"Icons" in Finch are actually two independent systems, tied to two different surfaces. It's easy to conflate them when building a mini tool, so let's separate them first:

Entry icon (icon.png)UI icon (IconRef)
Shown whereToolbox list / detail page / community recommendation cardComposer toolbar button, button dropdown items
RepresentsThe mini tool itselfA specific interaction point (a button, a menu item)
FormatA PNG fileA string reference
Provided byA file in the package rootManifest declaration + code registering SVGs on demand

1. Entry icon (icon.png)

Spec

  • The filename must be exactly icon.png (case-sensitive), placed in the package root next to package.json.
  • Must be PNG format.
  • The community catalog requires square, 128×128 to 300×300 pixels (inclusive). Local private installs aren't size-checked, but to stay crisp across display sizes (a 40px small icon / a 64px card image), aim for this spec anyway.
  • Prefer a transparent or solid background — avoid gradients or photo-like content covering the whole image; the icon ends up displayed around 40px, where detail gets lost.

Two loading paths

  • Installed: the Finch main process reads <extension-dir>/icon.png directly from disk, served to the renderer through the internal finch-ext-icon:// protocol. It only recognizes this one file in the package root — no subdirectories, no other filenames.
  • Not installed (shown in the community catalog): Finch constructs https://unpkg.com/<npm-package>@<version>/icon.png directly — which means icon.png must actually be included in the npm-published tarball. Confirm with npm pack --dry-run before publishing; if it only exists in your git repo and wasn't included in npm publish, the community card falls back to a default placeholder.
  • With no icon at all, the fallback is a neutral square icon (lucide-react's Blocks) — no error, just a less polished experience.

2. UI icon (IconRef)

Composer buttons and their dropdown menu items reference icons via a string (similar to VS Code's ThemeIcon), rather than embedding a component or image directly. This string is called IconRef, with three forms:

FormMeaning
"settings" or "Settings"A built-in Finch Lucide icon name — kebab-case or PascalCase both resolve
"lucide:settings"Same as above, with an explicit prefix (recommended)
"ext:<packId>/<iconId>"References an icon from an SVG icon pack you registered at runtime

Used when declaring a button slot in the manifest:

"contributes": {
  "composerActions": [
    { "id": "my-btn", "icon": "GitBranch", "tooltip": "..." }
  ]
}

Code can also return an IconRef dynamically via getIcon() or a menu item's iconName, overriding the static manifest declaration.

The built-in Lucide set is a fixed allowlist

It's not the entire Lucide icon library — Finch maintains a central registry (BUILTIN_ICONS), and only names on it resolve to an icon. Anything else is treated as plain text (i.e. an unregistered icon name renders as that literal string, not an error and not some Lucide icon). The current registry leans toward common linear UI icons, e.g.:

settings · star · sparkles · zap · timer · filter · list
git-branch · git-commit-horizontal · clipboard · clipboard-check · clipboard-list
file · file-text · message-circle · log-in · puzzle · check
toggle-left · toggle-right · wand-sparkles · zoom-in · zoom-out
folder · hash · rocket · shield · cloud · calendar · users · bookmark …

This list keeps growing — don't guess or assume a name outside it works. Before integrating, run your button in Finch to confirm the icon actually renders, rather than degrading into text. If the icon you need genuinely isn't in the built-in set, register a runtime icon pack instead.

When the built-in set doesn't have what you need: register a runtime SVG icon pack

Two steps, the same "manifest static declaration + code dynamic registration" pattern as ComposerAction buttons:

1. Declare the icon pack namespace in the manifest (just declares that the pack exists — no actual graphics here):

"contributes": {
  "iconPacks": [{ "id": "my-icons", "label": "My Icons" }]
}

2. Register the actual SVGs in activate():

ctx.subscriptions.push(
  ctx.icons.register('my-icons', {
    rocket: { svg: '<svg viewBox="0 0 24 24">...</svg>' },
  }),
);

Once registered, reference it as ext:my-icons/rocket; if you're referencing your own icon pack from inside the same extension, you can also shorten it to ext:rocket.

Hard constraints when designing SVGs

For security, Finch treats an icon strictly as "a pure vector image" — no scripts, animations, or external images/links are supported. Anything that doesn't comply is silently dropped at render time, with no error shown. Follow these rules so an icon renders correctly the first time you register it:

  • Use basic vector shapes only: paths, circles, rectangles, polygons — no <script>, <image>, animation tags, or references to external images/links (including http(s):// URLs).
  • Don't hardcode colors. Just fill/stroke normally — Finch automatically makes the icon follow the current theme (light/dark), so there's no need to handle color yourself.
  • Use a 24×24 canvas. You don't need to worry about specific pixel units — the icon scales with the current font size automatically, as long as your artwork is proportioned for a 24×24 viewBox.
  • Keep the file small. A simple linear icon should be a few KB at most — that's never a problem.

Actual rendering size

Composer toolbar button icons and dropdown menu item icons currently render at 14px, strokeWidth: 1.8, mixed inline with Finch's built-in Lucide icons. To visually match the built-ins with a custom SVG icon pack:

  • Use a 24×24 canvas, with the main artwork occupying roughly an 18–20px visible area (matching Lucide's default padding).
  • Match stroke weight to Lucide's strokeWidth: 1.8–2 — too thick or thin looks inconsistent with other icons at 14px.
  • Prefer a stroke style (fill="none" + stroke="currentColor") — large filled blocks tend to blur together at 14px.

3. Best practices

  • Prefer the built-in Lucide set — don't build a custom icon pack for every mini tool just for looks. The built-in set already covers most Composer scenarios (settings, filters, files, git, toggles...); only register a runtime icon pack when there's genuinely no matching semantic in the built-in set.
  • icon.png should be recognizable at 40px — avoid complex gradients, text, or photos; simple monochrome/duotone artwork works best.
  • Don't hardcode colors in an SVG, unless the icon genuinely needs multi-color semantics (like a status light) — otherwise let currentColor inherit the theme color.
  • Register icon packs on demand, not preemptively — every icon registered via ctx.icons.register() gets sanitized and cached, so only register what you actually use.
  • Use kebab-case naming: keep packId and iconId consistent with your mini tool's own id style, for easier debugging and reuse.

Checklist

[ ] icon.png is in the package root, filename matches exactly (case-sensitive)
[ ] icon.png is PNG, square, 128×128–300×300 pixels
[ ] npm pack --dry-run confirms icon.png is in the publish manifest
[ ] Composer button / menu icon names are checked against the built-in Lucide allowlist first, to avoid falling back to text
[ ] Custom SVG icon packs follow the 24×24 canvas, currentColor, no-external-references constraints
[ ] Custom SVG icon stroke weight visually matches Lucide (strokeWidth ~1.8–2)