Commands, panels and widgets
Build native tools that people can place in their browser bars.
Developer preview · macOS · Python SDK 1.1Commands
from browse import command, tabs
@command("Pin this page", id="pin-page", symbol="pin", placement="top")
async def pin_page():
tab = await tabs.current()
if tab:
await tab.pin()
return "Pinned"command(title, subtitle=None, symbol=None, id=None, icon=None, placement=None) adds a command. Returning a string displays a message. Keep explicit id values stable when changing a title. A command without placement remains available in the command bar and customisation palette.
A complete reading-list panel
id = "dev.yourname.reading-list"
name = "Reading List"
version = "1.0.0"
permissions = ["tabs", "sidebar"]from browse import panel, storage, tabs, ui
async def save_current():
tab = await tabs.current()
if tab and tab.url:
items = await storage.get("items", [])
items.append({"title": tab.title, "url": tab.url})
await storage.set("items", items)
@panel("Reading List", id="reading", symbol="book")
async def reading():
items = await storage.get("items", [])
return ui.column(
ui.button("Save this tab", on_click=save_current),
ui.list([
ui.item(item["title"], subtitle=item["url"], url=item["url"])
for item in items
], empty="No saved pages yet."),
)Panels return a browse.ui widget tree. Browse renders it natively. Callbacks may be ordinary or async functions; the panel renders again after a callback. Use panel.refresh() for all panels or panel.refresh("reading") for the explicit panel ID after other changes.
Compact widgets and placement
from browse import on, panel, tabs, ui, widget
@widget("Tab count", id="tab-count", symbol="square.on.square", placement="bottom")
async def counter():
return ui.text(f"{len(await tabs.all())} tabs", style="caption")
@on.tab_opened
@on.tab_closed
def tabs_changed(tab):
panel.refresh("tab-count")panel and widget accept title, symbol, icon, id and placement. Placement is sidebar, top or bottom. Panels default to sidebar; compact widgets default to top. View → Customise Browse… lets the person move or hide them. Their saved choice takes precedence over the plugin’s suggestion.
Commands are limited to 50 and panels/widgets share a limit of 5 per extension. A tree can contain up to 2,000 widgets and be nested 16 levels deep. Compact widget content has 28 points of height in horizontal bars and 50 in the sidebar; use a panel for larger tools.
Widget reference
| Function | Options |
|---|---|
| ui.text(value, style="body", color=None) | Styles: title, headline, body, caption, mono. Colours: secondary, accent, red, green, orange or #RRGGBB. |
| ui.button(label, on_click=None, symbol=None, icon=None, style="plain") | Styles: plain, prominent, destructive. |
| ui.row(*children, spacing=8) / ui.column(*children, spacing=6) | Compose a layout. |
| ui.list(items, empty=None) | A list of rows and an optional empty-state message. |
| ui.item(title, subtitle=None, symbol=None, url=None, badge=None, on_click=None) | Calls the callback, or opens url when no callback is set. |
| ui.toggle(label, value, on_change=None) | Callback receives a bool. |
| ui.field(placeholder="", value="", on_submit=None) | Callback receives text when Return is pressed. |
| ui.progress(value=None, label=None) | 0–1 progress, or a spinner for None. |
| ui.chart(data, kind="bar", height=120) | A dictionary or label/value pairs. kind is bar or line. |
| ui.symbol(name, color=None) / ui.image(path, label="", size=20) | An SF Symbol or packaged raster image. Image size is clamped to 12–64 points. |
| ui.link(label, url) | Open a new tab when clicked. |
| ui.divider() / ui.spacer() | Separate or space out content. |
Pass optional arguments by keyword. Opening an item or link URL through an explicit click needs no tabs permission. Programmatic tab access still does.
Custom icons
Use a package-relative icon path on commands, panels, widgets or buttons. Set icon in browse.toml for the Settings icon. SF Symbol names remain available through symbol and act as fallbacks. Include every referenced asset in the package.
Supported images are PNG, JPEG, WebP and the first GIF frame, up to 512 KiB and 4096 × 4096 pixels. Browse downsamples browser-bar icons to 128 pixels. Remote URLs, paths outside the extension folder, escaping symlinks, invalid images and SVG are not supported. Browser bars use native widgets, not arbitrary HTML.