Documentation: Chrome Side Panel API, Firefox Sidebar Action API, Opera Sidebar Action API.
Promise-based methods for configuring an extension's sidebar, controlling its visibility, and changing its label, icon or badge where the browser supports those operations. Capability checks help callers decide which features to offer. This module does not export sidebar event subscriptions.
- isAvailableSidebar()
- canOpenSidebar()
- canCloseSidebar()
- canSetSidebarTitle()
- canGetSidebarTitle()
- canSetSidebarIcon()
- openSidebar(options)
- closeSidebar(options)
- toggleSidebar()
- getSidebarOptions(tabId?)
- setSidebarOptions(options?)
- getSidebarBehavior()
- setSidebarBehavior(behavior?)
- setSidebarPath(path, tabId?)
- getSidebarPath(tabId?)
- getSidebarState(windowId?)
- isOpenSidebar(windowId?)
- setSidebarTitle(title, tabId?)
- getSidebarTitle(tabId?)
- setSidebarIcon(details)
- setSidebarBadgeText(text, tabId?)
- clearSidebarBadgeText(tabId?)
- getSidebarBadgeText(tabId?)
- setSidebarBadgeTextColor(color, tabId?)
- getSidebarBadgeTextColor(tabId?)
- setSidebarBadgeBgColor(color, tabId?)
- getSidebarBadgeBgColor(tabId?)
For Chromium's Side Panel API, declare the sidePanel permission and an initial local page in the MV3 manifest:
{
"manifest_version": 3,
"permissions": ["sidePanel"],
"side_panel": {
"default_path": "sidebar.html"
}
}For Firefox or Opera Sidebar Action, use the sidebar_action manifest key:
{
"sidebar_action": {
"default_panel": "sidebar.html",
"default_title": "My sidebar",
"default_icon": "icons/sidebar.png"
}
}These are manifest fragments; provide the referenced files and the remaining required manifest fields.
sidebar_action does not use the Chromium sidePanel permission. See the
Firefox manifest reference
and Opera's manifest example.
Call the wrappers from an extension context, such as a background script, service worker, popup or extension page.
| Browser / API | Configuration and visibility | Title, icon and badge |
|---|---|---|
Chrome / Chromium, MV3 sidePanel |
Options, behavior, path, open/close and state query; method support varies by version | No title, icon or badge methods |
Microsoft Edge, MV3 sidePanel |
Uses the Chromium implementation where exposed | No title, icon or badge methods |
Firefox desktop sidebarAction |
Path, open/close, toggle and state query | Title and icon; no sidebar badge methods |
Opera opr.sidebarAction, MV2 |
Path; no legacy open/close, toggle or state query | Title; icon is temporarily disabled by the package; badge methods depend on the platform and exposed API |
| Opera, MV3 | Uses sidePanel for configuration/visibility when exposed; otherwise legacy Sidebar Action where supported |
Title via opr.sidebarAction; icon is temporarily disabled by the package |
| Safari / iOS Safari / Firefox for Android | These sidebar APIs are unavailable | Unavailable |
See the compatibility data for Side Panel
and Sidebar Action.
Chrome introduced sidePanel in 114, open in 116, and close in 141. Use method-level checks rather
than treating namespace presence as support for every operation.
Opera's reference marks badge support as unavailable
on macOS; the package's declarations do not guarantee that a method is exposed or functional on every platform.
The Opera icon limitations apply specifically to changing the icon.
- Options and behavior use
sidePanelonly. Path, open/close and state queries prefersidePanelwhen present. - Title, icon, badge and toggle operations use
sidebarAction:opr.sidebarActiontakes precedence overbrowser.sidebarAction. A selected namespace with a missing method does not trigger a retry on another namespace.
A browser exposing both APIs can therefore use different native namespaces for different operations.
isAvailableSidebar() and the can… checks are synchronous, uncached and silent. They return false when
required API access fails and do not call the operation being checked. They work without a DOM, including in
service workers. A true result does not guarantee valid arguments, a required user gesture or a successful call.
Operations return Promises and preserve failures as rejections. Unsupported operations are not silently skipped.
Callback errors through runtime.lastError, native Promise rejections and synchronous native errors are passed
back to the caller. Only the state helpers deliberately convert unavailable or failed queries into safe results.
import {canSetSidebarTitle, setSidebarTitle} from "@addon-core/browser";
if (canSetSidebarTitle()) {
try {
await setSidebarTitle("Reading list");
} catch (error) {
console.error("Could not update the sidebar title", error);
}
}An exported Error subclass used for explicit unsupported-operation checks, including a blocked Opera
icon change. It is not a wrapper around every possible failure: native errors and API access errors remain
ordinary rejections, and some unavailable configuration/path operations use Error directly.
Do not assume that every Sidebar failure is an instance of SidebarError.
Exported as both a runtime enum and a TypeScript type:
enum SidebarState {
Open = "open",
Closed = "closed",
Unknown = "unknown",
}Use SidebarState.Unknown when the browser cannot establish the panel's state. It is distinct from a confirmed
closed panel. See getSidebarState() for the query scope and Chromium limitation.
Configuration and visibility methods use chrome.sidePanel.PanelOptions, PanelBehavior, OpenOptions
and CloseOptions. These types describe arguments; their availability in TypeScript does not establish runtime support.
setSidebarIcon() retains the existing global type opr.sidebarAction.IconDetails for its Firefox arguments;
the type name does not imply runtime support in Opera:
| Property | Type | Meaning |
|---|---|---|
path |
string | Record<string, string> |
An image path or a map of sizes to paths |
imageData |
ImageData |
Pixel data supplied instead of a path |
tabId |
number |
Optional tab-specific override; omitted for the global icon |
Supply path or imageData. The current wrapper type does not expose Firefox's windowId option or maps of
ImageData objects. Supported Firefox calls receive the original arguments, including all path-map keys,
string paths, pixel data and tabId. The browser validates native arguments; the package does not adapt maps.
IconDetails is not a separate named export from the package.
Badge setters accept a color string or chrome.extensionTypes.ColorArray, an RGBA tuple
[number, number, number, number] with channels from 0 to 255. Badge color getters return that tuple.
isAvailableSidebar(method?: ApiMethodName<typeof chrome.sidePanel | typeof browser.sidebarAction | typeof opr.sidebarAction>): booleanThe optional method is a native function name, for example isAvailableSidebar("open").
Import the public ApiMethodName type from @addon-core/browser for your own typed helpers.
This checks callable presence without invoking the method; permissions and argument support still require handling.
Checks for sidePanel on the API selected by browser(), then for the selected sidebarAction namespace.
Returns false if no namespace is available or accessing the environment throws. It checks namespace
presence only; use the can… functions for the operations they cover.
import {isAvailableSidebar} from "@addon-core/browser";
const hasSidebarApi = isAvailableSidebar();canOpenSidebar(): booleanChecks for a callable sidePanel.open when Side Panel is present, otherwise for sidebarAction.open.
A missing method or access error returns false. This does not check user-gesture requirements.
import {canOpenSidebar} from "@addon-core/browser";
const canShowOpenButton = canOpenSidebar();canCloseSidebar(): booleanChecks for a callable sidePanel.close when Side Panel is present, otherwise for sidebarAction.close.
This is independent of canOpenSidebar(); a browser can support opening without exposing a close method.
import {canCloseSidebar} from "@addon-core/browser";
const canShowCloseButton = canCloseSidebar();canSetSidebarTitle(): booleanChecks for a callable setTitle on the selected sidebarAction. Returns false for an absent or
non-callable method or an access error. sidePanel alone cannot change the title through this helper.
import {canSetSidebarTitle, setSidebarTitle} from "@addon-core/browser";
if (canSetSidebarTitle()) {
await setSidebarTitle("My panel");
}canGetSidebarTitle(): booleanChecks for a callable getTitle on the selected sidebarAction. Reading and setting are checked
independently; support for setTitle does not establish support for getTitle.
import {canGetSidebarTitle, getSidebarTitle} from "@addon-core/browser";
if (canGetSidebarTitle()) {
console.log(await getSidebarTitle());
}canSetSidebarIcon(): booleanUses the same compatibility rule as setSidebarIcon(): the selected sidebarAction must expose a callable
setIcon, and it must not be Opera's opr.sidebarAction. Opera is temporarily blocked in both MV2 and MV3
because its native handler targets the wrong API. This check does not read the manifest.
Returns false for sidePanel alone, absent or non-callable methods, API access errors and the temporary
Opera restriction. true means support considering known restrictions, not success with arbitrary arguments.
The check does not load an image or make a trial icon change. See the Opera restriction.
import {canSetSidebarIcon, setSidebarIcon} from "@addon-core/browser";
if (canSetSidebarIcon()) {
await setSidebarIcon({path: "/icons/sidebar.png"});
}openSidebar(options: chrome.sidePanel.OpenOptions): Promise<void>Forwards options to sidePanel.open when available. Chromium requires at least one of tabId or windowId.
Without Side Panel, calls Firefox's sidebarAction.open() with no arguments: the supplied IDs do not target
another window in that branch. The public wrapper still uses Chromium's options type and requires a target ID.
A missing selected method rejects with SidebarError.
Invoke opening directly in a user-action handler. See the Chromium requirements and Firefox active-window behavior. For example, register this in a background context with an action configured without a popup:
import {canOpenSidebar, onActionClicked, openSidebar} from "@addon-core/browser";
onActionClicked(tab => {
if (canOpenSidebar()) {
void openSidebar({windowId: tab.windowId}).catch(error => {
console.error("Could not open the sidebar", error);
});
}
});closeSidebar(options: chrome.sidePanel.CloseOptions): Promise<void>Forwards options to sidePanel.close; Chromium requires tabId or windowId to identify the target.
Otherwise calls Firefox's sidebarAction.close() without arguments, ignoring the supplied IDs and closing
the extension's sidebar in the active window. A missing selected method rejects with SidebarError.
Firefox requires a user-action handler for this call; see its
close reference.
This alternative action handler closes the panel when the toolbar button is clicked. Configure the action without a popup and register the handler in a background context:
import {canCloseSidebar, closeSidebar, onActionClicked} from "@addon-core/browser";
onActionClicked(tab => {
if (canCloseSidebar()) {
void closeSidebar({windowId: tab.windowId}).catch(error => console.error("Could not close the sidebar", error));
}
});The public wrapper requires a target ID even in Firefox, where the native call ignores it.
toggleSidebar(): Promise<void>Calls the selected sidebarAction.toggle(), supported by Firefox. It does not emulate toggling through
sidePanel.open and sidePanel.close. Missing support rejects with SidebarError.
Firefox toggles the extension's sidebar in the active window and requires a
user-action handler.
import {toggleSidebar} from "@addon-core/browser";
// Firefox extension page with a button whose ID is toggle-sidebar.
document.querySelector<HTMLButtonElement>("#toggle-sidebar")?.addEventListener("click", () => {
void toggleSidebar().catch(error => console.error("Could not toggle the sidebar", error));
});getSidebarOptions(tabId?: number): Promise<chrome.sidePanel.PanelOptions>Reads sidePanel.getOptions({tabId}). Omit tabId for the default configuration or provide it for a tab's
configuration. Returns the native options unchanged, including path and enabled when supplied by the browser.
This is Side Panel only; missing support and native query failures reject.
import {getSidebarOptions} from "@addon-core/browser";
const options = await getSidebarOptions(42);
console.log(options.path, options.enabled);setSidebarOptions(options?: chrome.sidePanel.PanelOptions): Promise<void>Passes the supplied object to sidePanel.setOptions. Omitting it sends {}; this helper does not implement
a reset operation. Configuration does not open the panel. This is Side Panel only and rejects when unavailable.
import {setSidebarOptions} from "@addon-core/browser";
await setSidebarOptions({tabId: 42, path: "sidebar.html", enabled: true});getSidebarBehavior(): Promise<chrome.sidePanel.PanelBehavior>Returns the result of sidePanel.getPanelBehavior(). This is Side Panel only; it has no Sidebar Action fallback.
import {getSidebarBehavior} from "@addon-core/browser";
const behavior = await getSidebarBehavior();
console.log(behavior.openPanelOnActionClick);setSidebarBehavior(behavior?: chrome.sidePanel.PanelBehavior): Promise<void>Calls sidePanel.setPanelBehavior, for example to let an action click toggle the panel.
Omitting behavior sends {} without adding defaults or resetting configuration. This is Side Panel only.
import {setSidebarBehavior} from "@addon-core/browser";
await setSidebarBehavior({openPanelOnActionClick: true});setSidebarPath(path: string, tabId?: number): Promise<void>Sets sidePanel options {tabId, path} when that API is present; otherwise calls
sidebarAction.setPanel({tabId, panel: path}). Omit tabId to update the default/global panel.
The path is passed unchanged, and the browser decides which URLs it accepts. Chromium requires a local
extension resource. Setting a path does not itself open the panel; unsupported operations and native failures reject.
import {setSidebarPath} from "@addon-core/browser";
await setSidebarPath("sidebar.html");
await setSidebarPath("sidebar.html?mode=compact#settings", 42);getSidebarPath(tabId?: number): Promise<string | undefined>Reads sidePanel.getOptions() or the selected sidebarAction.getPanel(). Omit tabId for the default/global
configuration. Unlike getSidebarOptions().path, this helper normalizes the native result:
- This extension's URLs become paths relative to its root, without a leading
/. - Query parameters, fragments and URL encoding are preserved.
- External URLs remain absolute, including their scheme and host.
- Empty or absent native paths produce
undefined.
A failed query rejects; it is not converted to undefined. Reading a configured path does not establish visibility.
import {getSidebarPath, setSidebarPath} from "@addon-core/browser";
await setSidebarPath("sidebar.html?mode=compact#settings", 42);
console.log(await getSidebarPath(42)); // "sidebar.html?mode=compact#settings"getSidebarState(windowId?: number): Promise<SidebarState>Queries runtime.getContexts({contextTypes: ["SIDE_PANEL"]}) when Side Panel is present; otherwise uses
Firefox's sidebarAction.isOpen({windowId}). Without an ID, looks up the last focused browser window on
each call. It checks only that window, which may differ from the calling page's window. An explicit ID,
including 0, bypasses the focus lookup.
Returns SidebarState.Open for a confirmed open panel, SidebarState.Closed for a confirmed closed panel,
and SidebarState.Unknown when the result cannot be established. Missing APIs, API access errors, failed
window lookups and failed queries produce Unknown without rejection, logging or caching.
Opera's legacy Sidebar Action has no state query; an environment with only that API returns Unknown.
Chromium reports panel contexts, which are not a complete visibility signal. A matching window context
produces Open. If none matches but a side-panel context has windowId: -1, the result is Unknown because
that context cannot be assigned to a window. Otherwise the result is Closed, including an empty context list.
The helper does not maintain its own visibility registry. The unassigned window ID is covered by
Chromium's native API test.
import {getSidebarState, SidebarState} from "@addon-core/browser";
const state = await getSidebarState();
if (state === SidebarState.Open) {
console.log("An open panel was confirmed");
} else if (state === SidebarState.Closed) {
console.log("The queried window has no open panel");
} else {
console.log("The panel state is unknown");
}isOpenSidebar(windowId?: number): Promise<boolean>Returns true only when getSidebarState(windowId) returns SidebarState.Open. Both Closed and Unknown
produce false; false does not necessarily mean the panel is closed. Like the state query, this helper
contains errors and does not reject. Use getSidebarState() when the distinction matters.
import {isOpenSidebar} from "@addon-core/browser";
const confirmedOpen = await isOpenSidebar(7);setSidebarTitle(title: string | number, tabId?: number): Promise<void>Calls sidebarAction.setTitle({tabId, title: title.toString()}); omitting tabId changes the global title.
The title labels the browser's sidebar UI, not the sidebar page's HTML document. Firefox uses it in sidebar
UI and Opera describes it as a tooltip. The wrapper awaits Firefox's Promise and handles Opera's synchronous setter.
A missing or non-callable selected method rejects with SidebarError; native failures still reject.
import {canSetSidebarTitle, setSidebarTitle} from "@addon-core/browser";
if (canSetSidebarTitle()) {
await setSidebarTitle("Reading list", 42);
}getSidebarTitle(tabId?: number): Promise<string>Reads the selected sidebarAction.getTitle({tabId}), awaiting Firefox's Promise or Opera's callback.
Omit tabId for the global title. Missing or non-callable support rejects with SidebarError;
access errors, callback errors and native Promise rejections also reject.
import {canGetSidebarTitle, getSidebarTitle} from "@addon-core/browser";
if (canGetSidebarTitle()) {
console.log(await getSidebarTitle(42));
}setSidebarIcon(details: opr.sidebarAction.IconDetails): Promise<void>Changes the selected Sidebar Action icon in Firefox. Uses the same compatibility rule as canSetSidebarIcon():
missing or non-callable support, or Opera in either MV2 or MV3, rejects with SidebarError before any native
icon call. API access errors retain their original rejection. Opera's namespace keeps priority when both
namespaces exist; the wrapper does not retry through Firefox or the Action API.
Firefox receives details unchanged, including all path-map keys, string paths, imageData and tabId: 0.
Neither details nor its path map is modified. The package does not convert sizes, load or resize images.
It waits for Firefox's Promise and propagates native rejections and synchronous argument errors. A successful
capability check does not validate an image or guarantee that a particular call succeeds.
See the argument types and Opera limitations.
import {canSetSidebarIcon, setSidebarIcon} from "@addon-core/browser";
if (canSetSidebarIcon()) {
await setSidebarIcon({path: "/icons/sidebar.png"});
await setSidebarIcon({path: "/icons/sidebar-active.png", tabId: 42});
await setSidebarIcon({
path: {24: "/small.png", 40: "/medium.png", 64: "/large.png"},
tabId: 42,
});
// Firefox receives the original map with all three sizes. Opera skips this block.
}setSidebarBadgeText(text: string | number, tabId?: number): Promise<void>Calls the selected Sidebar Action's setBadgeText with the text converted to a string. This is an Opera
operation where the method is exposed; it has no Side Panel or Firefox equivalent. Omit tabId for the
global badge. Missing support rejects with SidebarError.
import {setSidebarBadgeText} from "@addon-core/browser";
// Opera with badge support.
await setSidebarBadgeText(3, 42);clearSidebarBadgeText(tabId?: number): Promise<void>Calls setSidebarBadgeText("", tabId) and preserves its errors. Clearing does not reset badge colors.
import {clearSidebarBadgeText} from "@addon-core/browser";
// Opera with badge support.
await clearSidebarBadgeText(42);getSidebarBadgeText(tabId?: number): Promise<string>Reads the selected Sidebar Action's getBadgeText through its callback. Omit tabId for the global badge.
A native empty string is returned unchanged; an unavailable method or failed read rejects.
import {getSidebarBadgeText} from "@addon-core/browser";
// Opera with badge support.
console.log(await getSidebarBadgeText(42));setSidebarBadgeTextColor(color: string | chrome.extensionTypes.ColorArray, tabId?: number): Promise<void>Forwards {tabId, color} to the selected Sidebar Action's setBadgeTextColor, without color conversion.
This requires an implementation exposing that Opera-specific method; the general badge API does not
establish support for text-color methods. Missing support rejects with SidebarError.
import {setSidebarBadgeTextColor} from "@addon-core/browser";
// Opera with setBadgeTextColor support.
await setSidebarBadgeTextColor([255, 255, 255, 255], 42);getSidebarBadgeTextColor(tabId?: number): Promise<chrome.extensionTypes.ColorArray>Reads the selected Sidebar Action's getBadgeTextColor through its callback and returns the native RGBA tuple.
Omitting tabId queries the global setting. Unavailable support and native failures reject.
import {getSidebarBadgeTextColor} from "@addon-core/browser";
// Opera with getBadgeTextColor support.
const [red, green, blue, alpha] = await getSidebarBadgeTextColor(42);
console.log(red, green, blue, alpha);setSidebarBadgeBgColor(color: string | chrome.extensionTypes.ColorArray, tabId?: number): Promise<void>Forwards {tabId, color} to the selected Sidebar Action's setBadgeBackgroundColor. Omit tabId for the
global setting. The wrapper does not normalize colors or provide a fallback when the method is unavailable.
import {setSidebarBadgeBgColor} from "@addon-core/browser";
// Opera with badge background-color support.
await setSidebarBadgeBgColor("#2463eb", 42);getSidebarBadgeBgColor(tabId?: number): Promise<chrome.extensionTypes.ColorArray>Reads the selected Sidebar Action's getBadgeBackgroundColor through its callback. Returns an RGBA tuple
rather than a CSS string, and rejects unavailable support or native errors.
import {getSidebarBadgeBgColor} from "@addon-core/browser";
// Opera with badge background-color support.
const background = await getSidebarBadgeBgColor(42);
console.log(background);The package temporarily disables setSidebarIcon() for Opera in both Manifest V2 and Manifest V3, even
when native setIcon exists. canSetSidebarIcon() returns false; a direct setter call rejects with
SidebarError without invoking Opera's method. Title, path and other Sidebar operations keep their existing
behavior. Use sidebar_action.default_icon in the manifest for Opera's initial Sidebar icon.
In Opera 135.0.5973.92 on macOS, the native Sidebar Action handler sends the request to the old
browserAction.setIcon, with different failures in each manifest version:
- MV2: calls can complete successfully while changing the toolbar's Browser Action icon instead of
the Sidebar icon. Visual checks with both
sidebar_actionandbrowser_actiondeclared reproduced this for global and tab-specific calls. A direct native call with valid arguments reproduced the same result without the wrapper. - MV3: the old API is unavailable, so correct native calls fail with
Access to extension API denied.
Changing path-map keys cannot correct this routing defect, so the package does not adapt Opera icon maps. The block also avoids Opera's lost callback on native image loading or decoding failures. No timeout, custom image loader, Action API fallback or silent success is used.
This is a temporary package compatibility restriction, independent of the manifest or browser version. Revisit it only after a browser fix is confirmed by both successful API completion and visual updates of the actual Sidebar icon, globally and for a specific tab.
Earlier Chromium isOpenSidebar() calls without a window ID checked all windows. The helper now checks
the last focused window and returns false for unavailable or failed queries. Use getSidebarState() to
retain the distinction between closed and unknown.
Earlier Firefox/Opera getSidebarPath() results had a leading / and omitted query parameters and fragments.
The current helper preserves those URL components and returns local paths relative to the extension root.
Jest tests exercise capability checks, namespace priority, unchanged Firefox arguments and failure handling against controlled API models. Opera MV2/MV3 tests assert rejection without native icon calls or manifest reads, while title/path tests guard against regressions. Package-consumer checks verify public types and ESM/CJS exports. Browser scenarios run the built package in Firefox and Chromium; they cover icon/title contracts and unsupported Side Panel operations. Separate native checks in Opera 137.0.6036.39 on macOS confirmed the package's rejection of icon changes in MV2 and MV3, including a service worker without a DOM, while title and path operations still completed. These checks verify the package's block, not whether Opera has fixed its native handler. The earlier native Opera investigation and visual verification are the basis for the compatibility restriction above; successful native callbacks alone did not establish correct Sidebar icon updates. These checks do not establish support for every historical browser version or prove that all badge methods work on every Opera platform.