Save the sites you always keep pinned, sort them into categories, and reopen them all in one click, pinned and without duplicates. Manifest V3, 100% local storage, no account, no tracking.
- Features
- Installation
- Usage
- Privacy & permissions
- Backup format
- Development
- Architecture
- Releasing
- Support · License
- One-click launch: open every saved site as a pinned tab, or only one category.
- No duplicates: a site already open is left alone; open but unpinned, it is pinned
instead of reopened. Matching ignores tracking parameters, parameter order,
http/httpsandwww.(so a tab still matches after the site redirected it), but keeps what identifies a page (?v=on YouTube,?q=,?id=, single-page-app routes like#/inbox…). The same address cannot be saved twice. - Pin the current tab from the popup, into the category of your choice.
- Categories: rename them and pick an emoji icon; close all pinned tabs of a category at once (with confirmation). Tabs of a removed category move to the first one.
- Reorder tabs by drag and drop, or with the ↑ / ↓ buttons (keyboard friendly).
- Import / export your configuration as JSON.
- 14 languages: Arabic, Chinese, Dutch, English, French, German, Hindi, Indonesian, Italian, Japanese, Korean, Portuguese, Russian, Spanish.
- Accessible: full keyboard use, focus kept in place after actions, screen-reader labels and announced messages, right-to-left layout for Arabic, dark mode, high-contrast and reduced-motion preferences respected.
| Browser | Store | Minimum version |
|---|---|---|
| Firefox | addons.mozilla.org | 115 (ESR) |
| Chrome / Chromium | Chrome Web Store | 104 |
Each GitHub release
also provides the Firefox and Chrome packages as .zip files.
- Click the Tabs Pin icon in the toolbar.
- Pin current tab saves the page you are on (you choose its category), or open Options to add, edit and reorder sites.
- Open N tabs opens everything; click a category to open only its sites.
- The Close button of a category closes, after confirmation, the pinned tabs of the current window whose site (domain) matches one of the category's saved sites.
| Permission | Why |
|---|---|
tabs |
Read the addresses of open tabs, to avoid duplicates and pin / close them |
storage |
Keep your sites, categories and settings in the browser's local storage |
activeTab |
Read the current tab when you pin it |
- 100% local storage: your sites, categories and settings are stored only in your
browser (
storage.local) and never sent anywhere: no account, no sync server, no analytics. - Site icons (display only, nothing is stored remotely): to show the favicon of a saved site, the popup and options page request it from Google's and DuckDuckGo's favicon services, then from the site itself. These requests contain the site's domain name (not the full address).
Options → Export downloads a JSON file:
{
"tabs": [
{ "id": "tab_…", "url": "https://example.com/", "title": "Example",
"category": "work", "enabled": true, "order": 0, "dateAdded": "2026-01-01T00:00:00.000Z" }
],
"categories": [{ "id": "work", "name": "Work", "icon": "💼" }],
"settings": { "lastOpened": "2026-01-01T00:00:00.000Z", "categoriesLocalized": true },
"exportDate": "…",
"version": "1.4.2"
}On import, the file replaces the current tabs and categories. It is validated first:
entries without an http(s) address and unnamed categories are dropped, a missing icon
becomes 📁, tabs of an unknown category go to the first one, duplicate IDs are fixed,
only a valid past lastOpened date is kept from the settings, and the number of skipped
tabs is reported.
Requirements: Node.js ≥ 20.11 (see .nvmrc), Firefox and/or Chromium.
git clone https://github.com/alexandre-leng/Tabs-Pin-Manager-Extension.git
cd Tabs-Pin-Manager-Extension
npm ci| Command | What it does |
|---|---|
npm run dev:firefox |
Runs Firefox with the extension, reloaded on every change |
npm run dev:chrome |
Stages build/chrome/: load it as an unpacked extension (run again after changes) |
npm run build |
Builds both packages into web-ext-artifacts/ (build:firefox / build:chrome for one) |
npm test |
Unit tests (Jest) |
npm run test:e2e |
Interface tests (Playwright): loads the real extension in Chromium, drives the popup and options page, runs an axe-core accessibility audit. Run npx playwright install chromium once first |
npm run lint |
web-ext lint (Firefox add-on rules) |
npm run lint:js / npm run lint:css |
ESLint / Stylelint |
manifest.json is shared and never modified: every command copies the extension into
build/<browser>/ with the browser-specific manifest (scripts/prepare-manifest.js).
CI runs all the checks above on every push and pull request.
Native ES modules, no bundler: each context has one entry module and every
dependency is an explicit import.
| Context | Entry | Modules |
|---|---|---|
| Background (Firefox event page / Chrome service worker) | background/background.js |
controller.js (events, messages), data-store.js (saved data, one change at a time), tab-actions.js (open / pin / close tabs), import-sanitizer.js, log.js |
| Popup | popup/main.js |
popup.js (PopupManager), category-list.js, category-order.js, category-selection.js, tab-actions.js |
| Options page | options/main.js |
options.js (OptionsManager), tab-cards.js, tab-editor.js, tab-ordering.js, category-editor.js, icon-picker.js, import-export.js |
| Shared | lib/ |
browser-api.js, storage-manager.js, url-utils.js, tab-utils.js, domain-utils.js, punycode.js (display of international domains), time-format.js, ui-utils.js, i18n-helper.js, default-categories.js, mixins.js |
- The background script owns the data: pages change it only through messages
(
openAllTabs,openCategoryTabs,countCategoryPinnedTabs,closeCategoryTabs,saveTab,deleteTab,reorderTabs,saveCategories,importAllData) and read it withgetTabsData(the popup reads storage directly only if the background does not answer). The background broadcasts adataChangedmessage after each change. - On install, default categories are created in the browser language; on update,
migrate()translates them once (settings.categoriesLocalized), fills missing tab fields and moves tabs of unknown categories to the first one. - Page features are plain objects of methods composed into the page class with
mixin(), which throws if two features define the same method. tests/module-graph.test.jschecks that every import resolves and no module is unused;tests/locales.test.jschecks that every translation key used exists in every language.
- Bump the version in
package.jsonandmanifest.json(a test checks they match). - Move the
Unreleasednotes ofCHANGELOG.mdunder## [x.y.z] - date. - Push to
main, then run the Release workflow (Actions → Release → Run workflow): it tests, builds both packages and publishes the GitHub release with the changelog section as notes.
- Bugs and ideas: GitHub Issues
- Email: dev.alexandre.git [@] gmail.com
- Changes: CHANGELOG.md
GPL-3.0. Developed by Alexandre.