| name | muxy-extension |
| description | Best-practice guide for authoring a Muxy extension — how it should look and behave so it reads as a native part of the app. Covers theming (follow the theme, never hardcode colors), the sizing scale, and which surface to use. Mechanics (manifest fields, permissions, the window.muxy API) live in the linked docs. |
Muxy Extension Guide
A Muxy extension is an npm + Vite project in ~/.config/muxy/extensions/<name>/: source under src/, vite build emits dist/, and Muxy installs and reads dist/. The manifest is the "muxy" object in package.json.
This skill is the guidance layer — how an extension should look and behave. For the API and manifest mechanics (every field, the permission strings, the full window.muxy surface, events, scripts), read the reference docs:
https://github.com/muxy-app/muxy/tree/main/docs/extensions
The goal of everything below: an extension should be indistinguishable from a native Muxy surface. Match the theme and match the scale, and it will be.
Pick the right surface
- Showing something to the user → a UI page (tab, panel, or popover). Page scripts get the full
window.muxy API.
- Reacting to events or running shell commands headlessly → a
background.js script. Most extensions don't need one.
- One-shot logic from the palette → a
runScript command, not a hidden tab.
Don't open a hidden tab to run logic, and don't put event-driven work in tab JS where closing the tab loses it.
Theme — follow it, never hardcode
Muxy ships paired light/dark themes and a user-chosen accent. Every extension webview inherits CSS custom properties on document.documentElement that track the live theme and update automatically when the user switches it.
Rules:
- No hex literals for chrome. Use
var(--muxy-…) for every color. The only exception is decorative art meant to be theme-independent.
- The variables already invert for light/dark — never sniff the color scheme to pick a color. Only branch on
muxy.theme.colorScheme for things a variable can't express (e.g. swapping a logo image).
--muxy-accent is the only saturated color. Use it sparingly — primary action, focus ring, one key number — so it stays distinctive. Text on an accent fill should be --muxy-background to stay legible in both themes.
- Depth comes from
--muxy-surface + --muxy-border + --muxy-hover, not from new colors. Cards, inputs, code blocks, and buttons all share the one surface color.
- Re-read the theme for JS-drawn color. Canvas/SVG that doesn't pick up CSS variables must redraw in
muxy.onThemeChange(theme => …).
- Popovers leave the body transparent (
body { background: transparent; }) — they sit over native macOS popover material that is already light/dark-aware. Tabs and panels do paint --muxy-background on the body.
The variables (the complete injected set):
| Variable | Use for |
|---|
--muxy-background | Page background |
--muxy-foreground | Primary text |
--muxy-foreground-muted | Secondary text, labels, captions |
--muxy-surface | Cards, inputs, code blocks, buttons |
--muxy-border | 1px borders and dividers |
--muxy-hover | Hover state for buttons / rows |
--muxy-accent | Primary action, links, focus rings |
--muxy-accent-soft | Translucent accent for badges/highlights |
--muxy-diff-add / --muxy-diff-remove / --muxy-diff-hunk | Diff / success / error / hunk colors |
--muxy-topbar-height | The app's tab-bar height (see Sizing) |
(muxy.theme.colorScheme gives "light"/"dark" in JS; there is no --muxy-color-scheme CSS var.)
Sizing — match the app's scale
Muxy's native views are built from one scale of values, and all of them scale with the user's interface-scale setting (Settings → Interface). Pick from this scale rather than inventing numbers, so your surface tracks scale changes the way native views do. These are the base (100%) values in px:
Spacing (padding, gap, margin) — 2 · 4 · 6 · 8 · 10 · 12 · 16 · 20 · 24 · 32. No in-between values. Panel rows and content pad 10px left/right; an icon-and-label gap is 8px; adjacent icon buttons sit 4px apart.
Font sizes — 10 caption · 11 footnote/section labels (often uppercased) · 12 body (paths, row text) · 13 controls · 14 titles (weight 600) · 16+ headings. Body is 12, not 13. Use the system font for UI; "SF Mono", Menlo, monospace for code, counts, and hashes.
Icons — 12–14px glyphs at weight 600 (a thinner default weight is the most common reason an extension's icons look foreign). Custom SVG strokes are 1.5px, round caps/joins.
Controls — an icon button is a 24×24 hit target wrapping a 13–14px glyph; text buttons are 28px tall with 10px horizontal padding.
Radii — 4 chips/badges · 6 buttons/inputs · 8 cards/panels · 10 large containers. Buttons are 4–6, not 5.
Topbar height is the exception — never hardcode it. It scales with interface scale and is injected pre-scaled as --muxy-topbar-height. A tab fills its whole region, so render your own topbar to match native tabs (so split panes line up): use that variable for the height and keep box-sizing: content-box so the 1px border-bottom lands on the same line as native tabs. Omit the topbar for edge-to-edge content.
Declare the scale once at the top of your stylesheet and reference it everywhere, so there are no stray magic numbers:
:root {
--s1:2px; --s2:4px; --s3:6px; --s4:8px; --s5:10px;
--s6:12px; --s7:16px; --s8:20px; --s9:24px; --s10:32px;
--font-caption:10px; --font-footnote:11px; --font-body:12px;
--font-emphasis:13px; --font-title:14px;
--icon-sm:12px; --icon:14px; --control:24px;
--radius:6px; --radius-card:8px; --row-height:34px;
}
Behavior
- Least privilege. Declare a permission only when you add the call that needs it.
- Use
muxy.git for repository work (status, diff, log, branches, PRs, tags, checkout/cherryPick/revert, worktrees incl. worktree.switchTo and pr.checkoutWorktree) instead of shelling out via muxy.exec — it's the app's own git core and returns structured data. Reads need git:read; writes need git:write and prompt for consent. Available to tabs/panels/popovers, not background scripts. See Git.
- Use
muxy.files for workspace filesystem work (list, read, stat, write, mkdir, rename, move, delete) instead of muxy.exec — paths are sandboxed to the active worktree root and returned relative to it. Reads need files:read; writes need files:write and prompt for consent. Pair with the file.changed event to stay reactive (e.g. a file tree). See Files.
- Use
muxy.modal.open for a list picker (the native Quick Open overlay) instead of building your own — pass { items: [{ id, title, subtitle? }], placeholder? } and await the selected item (or null if dismissed). Muxy owns the search, navigation, and open/close. No permission needed. Has no shortcut of its own: bind a palette command with a defaultShortcut, listen for its event in background.js, then open the modal. See Modal.
- Make hover and active states visible in both light and dark —
background: var(--muxy-hover); border-color: var(--muxy-accent); is the standard pattern.
- Respect
prefers-reduced-motion — Muxy users opt into Reduce Motion at the OS level; avoid long transitions, large translations, autoplay.
- No hardcoded
~/.config/muxy paths from inside the extension — rely on the working directory Muxy sets, or pass cwd to exec.
Checklist