Repository navigation
UI Surfaces
Declare contributions in the manifest and the matching ui.surfaces grants. Local IDs belong to your mod; do not use reserved host namespaces. Disabling a mod disposes its registrations.
| Contribution | How it works |
|---|---|
toolbar |
Button references a declared command; ui.setToolbarItemState updates enabled state/title. |
sidebarSections |
Static known-folder or provided-location entries. Scripted lists use locations and location.list. |
contextMenu |
Items or background actions referencing commands. |
columns |
column.values returns one handle-keyed cell per item; batch, cache and honor revision. Set a cell's label when its text is glyphs or symbols, so VoiceOver reads a meaning such as "Large, 12 GB" instead. |
statusBar |
Declared item text updated with ui.setStatusText. Set panel to one of your panel ids and a click opens or closes that panel; command runs too if both are set. Items show as soon as the mod is enabled; the user can hide or reorder them in Settings → Status Bar. |
panels |
Left, right or bottom; docked or slideOut; fill with ui.setPanel. Content taller than the panel scrolls. Every panel is listed in the View menu and the command palette. ui.showPanel opens one from a command the user ran. |
previews |
Extension-matched preview.render produces a title and node tree. |
views |
Alternative list/grid/columns layouts using your declared columns. |
commands |
command.run executes an action and returns a message or operation ticket. |
keybindings |
Chords reference commands. Resolve collisions; bare-key bindings generate warnings. |
locations |
Read-only or capability-backed virtual listings; use provider identity, not fake file URLs. |
searchProviders |
search.query returns bounded result batches. |
settingsPanes |
Native controls rendered from the mod's settings schema; no script evaluation required to draw them. |
themes, iconSets, sounds
|
Packaged data/assets; see Retro Purple. |
The generated scripting type is called UINode; native integrations use Mod UI node representations. This is a data tree, not HTML or an AppKit object. Node types are vstack, hstack, card, text, markdown, icon, button, list, progress, divider, spacer, textField, select. Fields include optional id, text, label, command, placeholder, value, children, items, style, icon, selected, color, size, weight, align, spacing, padding, tinted. A list item has id, title and optional subtitle.
await ui.setPanel("notes", {
type: "vstack",
children: [
{ type: "text", text: "Ready", label: "Panel status" },
{ type: "button", text: "Refresh", label: "Refresh notes", command: "refresh" }
]
});Give meaningful accessibility labels, concise button titles and readable text in both appearances. Every button needs non-blank text or label, and every textField, select and list needs a label (a text field can use its placeholder). A panel update that breaks this is not shown: the panel keeps its last valid content and the app shows an error naming the node. A preview that breaks it is not shown either. Give ids to controls that stay on screen across updates, so keyboard focus and the caret stay put when you add or remove other nodes; ids must be unique within a tree. Button commands must be your declared commands. Do not embed web scripts, arbitrary native controls or filesystem paths that were not granted. There is no PTY control; the terminal example is a bounded command-output panel.
A textField is a single-line input. text is its initial value, placeholder the hint, and id is required if you want to read it back.
await ui.setPanel("notes", {
type: "vstack",
children: [
{ type: "textField", id: "note", placeholder: "Add a note", command: "add-note", label: "New note" },
{ type: "button", text: "Add", label: "Add note", command: "add-note" }
]
});Pressing Return in the field runs its command with args { id, value, fields }: the field's id, its text, and a map of every text field in the panel by id. A button's command receives { fields } with the panel's current values, so a button can submit what was typed. See Folder Notes.
A select is a drop-down. items are its choices, selected the chosen item id (the first item when omitted), and id is required to read it back. Choosing an item runs its command with args { id, value, fields }, where value is the chosen item id. Its current choice is also in fields for every other command in the panel.
{ type: "select", id: "branch", label: "Branch", command: "switch",
selected: "main", items: [{ id: "main", title: "main" }, { id: "dev", title: "dev" }] }A list with a command runs it on double-click with args { value, fields }, where value is the row id.
A text node takes style: body (default), title, secondary or monospaced. Colours and fonts come from the active theme. A button takes icon, an SF Symbol name drawn before its text.
Finer control, all optional and all theme-aware:
| Field | Applies to | Values |
|---|---|---|
color |
text, icon, tinted card
|
primary, secondary, accent, success, warning, danger
|
size |
text, icon
|
point size, 8 to 96 |
weight |
text, icon
|
regular, medium, semibold, bold
|
align |
text, and the children of a vstack or card
|
leading, center, trailing
|
spacing |
vstack, hstack, card
|
gap between children, 0 to 64 |
padding |
vstack, hstack, card
|
inset around children, 0 to 64 |
tinted |
card |
true fills the card with its color instead of the neutral card background |
Three node types exist for layout. A card is a vstack drawn on a rounded background. An icon shows the SF Symbol named in its icon field. A spacer takes up the free room in a stack, so in an hstack it pushes what follows to the far edge. An hstack made only of cards gives them equal widths.
{ type: "card", tinted: true, color: "accent", padding: 16, children: [
{ type: "hstack", spacing: 12, children: [
{ type: "icon", icon: "cloud.sun.fill", size: 40, color: "warning", label: "Partly cloudy" },
{ type: "text", text: "72°", size: 38, weight: "bold", label: "Temperature 72 degrees" },
{ type: "spacer" }
] },
{ type: "text", text: "Austin", style: "title" }
] }Do not rely on colour alone: keep a label or text that says the same thing. See Weather Panel.
ui.showPanel("id") opens one of your panels in the front window if it is closed. It only works inside a command the user ran (a toolbar button, menu item, panel button or status bar click) and fails with noGesture anywhere else, so a mod cannot pop its panel open on its own.
locations.current() returns the front window's location. For a local folder it also returns folder, a FileRef scoped to your grants: with fs.write destination during a user gesture, or fs.read/fs.write selection otherwise. With neither grant it is a reference only and cannot be read or written. See Drop Stack.
Replacement declarations need ui.override in addition to ordinary surface access. Availability depends on the running app's adapters; a schema-valid surface does not prove the app has wired it. See generated manifest for all required fields and hooks for deadlines.
- Home
- Installing
- Building
- Replacing Finder
- Themes
- Keyboard Shortcuts
- FAQ
- Modding
- Getting Started
- Writing Mods in bfscript
- Writing Mods in TypeScript
- Permissions
- Hooks
- UI Surfaces
- Publishing to the Mod Index
- Using the MCP Server
- Troubleshooting
- Examples
- API Reference
- Manifest Reference
- Diagnostic Codes
- CLI Reference
- Reference Home
- Getting Started bfscript
- Getting Started TypeScript
- Getting Started JavaScript