Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions examples/side-panel/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
name: Side Panel
description: Cross-browser side panel that works on both Chrome and Firefox
apis:
- browser.sidePanel
- browser.sidebarAction
- browser.tabs.query
- browser.storage.local
permissions:
- storage
- tabs
---

```sh
pnpm i
pnpm dev
```

A port of MDN's [annotate-page](https://github.com/mdn/webextensions-examples/tree/main/annotate-page) example to WXT, demonstrating how to use Chrome's `sidePanel` API and Firefox's `sidebarAction` API with a single codebase.

## Features

- Shows current tab info (title and URL)
- Per-page notes saved to `browser.storage.local`
- Works on both Chrome (MV3) and Firefox (MV2)

## Cross-Browser Notes

- **Chrome**: Uses `browser.sidePanel.setPanelBehavior({ openPanelOnActionClick: true })` to open the panel on icon click
- **Firefox**: Uses `browser.sidebarAction.toggle()` on `browserAction.onClicked` to toggle the sidebar
- Tab queries use `windowId` instead of `currentWindow` for Firefox compatibility
18 changes: 18 additions & 0 deletions examples/side-panel/entrypoints/background.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { browser } from "wxt/browser";

interface BrowserWithSidebar {
sidebarAction?: {
toggle(): Promise<void>;
};
}

export default defineBackground(() => {
if (browser.sidePanel) {
browser.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });
} else {
const { sidebarAction } = browser as typeof browser & BrowserWithSidebar;
if (sidebarAction) {
browser.browserAction.onClicked.addListener(() => sidebarAction.toggle());
}
}
});
34 changes: 34 additions & 0 deletions examples/side-panel/entrypoints/sidepanel/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Cross-Platform Side Panel</title>
<meta
name="manifest.default_icon"
content='{"16":"/icon/16.png","32":"/icon/32.png","48":"/icon/48.png"}'
/>
<meta name="manifest.open_at_install" content="false" />
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<div class="container">
<div class="section">
<h2>Current Tab</h2>
<div id="tab-info" class="info-box">
<p>Loading...</p>
</div>
</div>
<div class="section">
<h2>Notes</h2>
<textarea
id="notes"
placeholder="Write notes about this page..."
></textarea>
<button id="save-btn">Save Note</button>
<p id="status" class="status"></p>
</div>
</div>
<script type="module" src="./main.ts"></script>
</body>
</html>
65 changes: 65 additions & 0 deletions examples/side-panel/entrypoints/sidepanel/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { browser } from "wxt/browser";

let windowId: number;
const tabInfoEl = document.getElementById("tab-info")!;
const notesEl = document.getElementById("notes") as HTMLTextAreaElement;
const statusEl = document.getElementById("status")!;

init();

async function init() {
const win = await browser.windows.getCurrent();
windowId = win.id!;

await updateTabInfo();

document.getElementById("save-btn")!.addEventListener("click", saveNotes);
notesEl.addEventListener("keydown", (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
e.preventDefault();
saveNotes();
}
});

browser.tabs.onActivated.addListener(updateTabInfo);
browser.tabs.onUpdated.addListener((_, info) => {
if (info.url || info.title) updateTabInfo();
});
}

async function getActiveTab() {
const [tab] = await browser.tabs.query({ windowId, active: true });
return tab;
}

async function updateTabInfo() {
const tab = await getActiveTab();
if (tab) {
tabInfoEl.innerHTML = `
<p><strong>Title:</strong> ${escape(tab.title || "Untitled")}</p>
<p><strong>URL:</strong> <span class="url">${escape(tab.url || "N/A")}</span></p>
`;
const key = `notes:${tab.url}`;
const result = await browser.storage.local.get(key);
notesEl.value = (result[key] as string) || "";
}
}

async function saveNotes() {
const tab = await getActiveTab();
if (tab?.url) {
await browser.storage.local.set({
[`notes:${tab.url}`]: notesEl.value,
});
statusEl.textContent = "Saved!";
setTimeout(() => {
statusEl.textContent = "";
}, 2000);
}
}

function escape(text: string): string {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
111 changes: 111 additions & 0 deletions examples/side-panel/entrypoints/sidepanel/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}

body {
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
sans-serif;
background: #f5f5f5;
color: #333;
line-height: 1.5;
}

.container {
padding: 16px;
}
h2 {
font-size: 1rem;
margin-bottom: 8px;
color: #444;
}
.section {
margin-bottom: 20px;
}

.info-box {
background: #fff;
border: 1px solid #ddd;
border-radius: 8px;
padding: 12px;
font-size: 0.875rem;
}

.info-box p {
margin-bottom: 4px;
}
.info-box p:last-child {
margin-bottom: 0;
}
.info-box .url {
color: #0066cc;
word-break: break-all;
}

textarea {
width: 100%;
height: 150px;
padding: 12px;
border: 1px solid #ddd;
border-radius: 8px;
font-family: inherit;
font-size: 0.875rem;
resize: vertical;
margin-bottom: 8px;
}

textarea:focus {
outline: none;
border-color: #0066cc;
box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.2);
}

button {
background: #0066cc;
color: #fff;
border: none;
padding: 10px 20px;
border-radius: 6px;
font-size: 0.875rem;
cursor: pointer;
}

button:hover {
background: #0052a3;
}

.status {
margin-top: 8px;
font-size: 0.875rem;
color: #28a745;
min-height: 1.5em;
}

@media (prefers-color-scheme: dark) {
body {
background: #1a1a1a;
color: #e0e0e0;
}
h2 {
color: #ccc;
}
.info-box {
background: #2a2a2a;
border-color: #444;
}
textarea {
background: #2a2a2a;
border-color: #444;
color: #e0e0e0;
}
textarea:focus {
border-color: #4d94ff;
box-shadow: 0 0 0 2px rgba(77, 148, 255, 0.3);
}
}
20 changes: 20 additions & 0 deletions examples/side-panel/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "side-panel",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "wxt",
"dev:firefox": "wxt -b firefox",
"build": "wxt build",
"build:firefox": "wxt build -b firefox",
"zip": "wxt zip",
"zip:firefox": "wxt zip -b firefox",
"compile": "tsc --noEmit",
"postinstall": "wxt prepare"
},
"devDependencies": {
"typescript": "^5.9.3",
"wxt": "^0.20.18"
}
}
Binary file added examples/side-panel/public/icon/128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/side-panel/public/icon/16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/side-panel/public/icon/32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/side-panel/public/icon/48.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/side-panel/public/icon/96.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions examples/side-panel/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "./.wxt/tsconfig.json"
}
18 changes: 18 additions & 0 deletions examples/side-panel/wxt.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { defineConfig } from "wxt";

const name = "Cross-Platform Side Panel";
const icons = { 16: "icon/16.png", 32: "icon/32.png", 48: "icon/48.png" };

export default defineConfig({
manifest: ({ browser }) => ({
name,
description: "A side panel extension that works on Chrome and Firefox",
permissions: ["storage", "tabs"],
...(browser === "firefox" && {
browser_action: {
default_icon: icons,
default_title: "Toggle Side Panel",
},
}),
}),
});
Loading