---
title: Chrome and Chromium
pagination_prev: demos/cloud/index
pagination_next: demos/bigdata/index
sidebar_custom_props:
  summary: Export HTML Tables in a Chromium extension
---
import current from '/version.js';
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
The [SheetJS Standalone scripts](/docs/getting-started/installation/standalone)
can be integrated in a Chromium extension.
This demo includes examples for exporting bookmarks from a popup and scraping
tables with a content script and a background script.
[The demo](#demo) includes unpacked extensions for Manifest V2 and Manifest V3.
:::note Tested Deployments
This demo was tested in the following deployments:
| Platform     | Version | Date       |
|:-------------|:--------|:-----------|
| Chromium 140 | V3      | 2025-09-13 |
| Chromium 137 | V2      | 2025-09-13 |
:::
:::caution pass
This demo showcases Manifest V2 and Manifest V3 extensions.
Chrome 138 and later no longer support Manifest V2 extensions.
**New Chrome and Chromium Extensions should use Manifest V3!**
:::
## Loading SheetJS Scripts
SheetJS libraries should be bundled in the extension. For path purposes, it is
strongly recommended to place `xlsx.full.min.js` in the root folder.
#### Popup Pages
In Manifest V2 and Manifest V3 extensions, popup pages can load the standalone
script using a normal `
```
#### Content Scripts
In Manifest V2 and Manifest V3 extensions, the standalone script can be loaded
through the `content_scripts` field:
```js
  /* in manifest.json v2 or v3 */
  "content_scripts": [{
    "matches": [""],
    "js": ["xlsx.full.min.js", "content.js"],
    "run_at": "document_end"
  }],
```
The `XLSX` global will be visible to other content scripts.
#### Background Scripts
In Manifest V2 extensions, if the standalone script is added as a background
script, other background scripts will be able to access the `XLSX` global!
```js
  /* in manifest.json v2 only! */
  "background": {
    "scripts": ["xlsx.full.min.js", "table.js"],
    "persistent": false
  },
```
In Manifest V3 extensions, background service workers can load the standalone
script through `importScripts`:
```js
/* assuming background script is in the same folder as xlsx.full.min.js */
importScripts("./xlsx.full.min.js");
// now XLSX will be available
```
## Relevant Operations
The official documentation covers details including required permissions.
### Generating Downloads
#### Manifest V2
The `writeFile` function works in a Chrome or Chromium extension:
```js
XLSX.writeFile(wb, "export.xlsx");
```
Under the hood, it uses the `chrome.downloads` API.  `"downloads"` permission
should be set in `manifest.json`.
#### Manifest V3
In a background service worker, `URL.createObjectURL` is unavailable. Instead,
`XLSX.write` can generate a Base64 string for a synthetic URL:
```js
/* generate Base64 string */
const b64 = XLSX.write(wb, {bookType: "xlsx", type: "base64"});
chrome.downloads.download({
  /* make a base64 url manually */
  url: `data:application/octet-stream;base64,${b64}`,
  filename: `SheetJSTables.xlsx`
});
```
### Content Script Table Scraping
`table_to_book` and `table_to_sheet` can help build workbooks from DOM tables:
```js
var tables = document.getElementsByTagName("table");
var wb = XLSX.utils.book_new();
for(var i = 0; i < tables.length; ++i) {
  var ws = XLSX.utils.table_to_sheet(tables[i]);
  XLSX.utils.book_append_sheet(wb, ws, "Table" + i);
}
```
## Demo
The demo extension includes multiple features to demonstrate sample usage.
Production extensions should include proper error handling.
  
1) Download the zip for the desired Manifest version:
- [Manifest V2](pathname:///chromium/SheetJSChromiumUnpackedV2.zip)
- [Manifest V3](pathname:///chromium/SheetJSChromiumUnpackedV3.zip)
2) Open `chrome://extensions/` in the browser and enable Developer mode
3) Drag and drop the downloaded zip file into the window.
  
  
1) Create a new extension using `create-chrome-ext`[^1]:
```bash
npm create chrome-ext@latest sheetjs-crx -- --template vanilla-ts
cd sheetjs-crx
npm install
```
2) Edit the highlighted lines in `package.json`:
```js title="package.json" (edit highlighted lines)
{
  "name": "sheetjs-crx",
  // highlight-next-line
  "displayName": "SheetJS Demo",
  "version": "0.0.0",
  "author": "**",
  // highlight-next-line
  "description": "Sample Extension using SheetJS to interact with Chrome",
```
3) Edit `manifest.ts` and add to the `permissions` array:
```ts title="manifest.ts"
  permissions: ['sidePanel', 'storage',
    "activeTab",
    "bookmarks",
    "contextMenus",
    "downloads",
    "tabs"
  ],
```
4) Install the SheetJS dependency and start the dev server:
{`\
curl -o ./public/img/logo-48.png https://docs.sheetjs.com/logo.png
npm i --save https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz
npm run dev`}
The build step will create a `build` subfolder.
5) Replace `src/popup/index.ts` with the following codeblock:
```ts title="src/popup/index.ts"
import { version, utils, writeFileXLSX } from 'xlsx';
import './index.css'
/* recursively walk the bookmark tree */
const recurse_bookmarks = (data, tree, path) => {
  if(tree.url) data.push({Name: tree.title, Location: tree.url, Path:path});
  var T = path ? (path + "::" + tree.title) : tree.title;
  (tree.children||[]).forEach(function(C) { recurse_bookmarks(data, C, T); });
};
const export_bookmarks = () => {
  chrome.bookmarks.getTree(function(res) {
    var data = [];
    res.forEach(function(t) { recurse_bookmarks(data, t, ""); });
    /* create worksheet */
    var ws = utils.json_to_sheet(data, { header: ['Name', 'Location', 'Path'] });
    /* create workbook and export */
    var wb = utils.book_new();
    utils.book_append_sheet(wb, ws, 'Bookmarks');
    writeFileXLSX(wb, "bookmarks.xlsx");
  });
};
document.addEventListener('DOMContentLoaded', () => {
  const root = document.getElementById('app')!
  const xprt = document.createElement("button"); // sjsdownload
  xprt.type = "button"; xprt.innerHTML = "Export Bookmarks";
  root.appendChild(xprt);
  xprt.addEventListener("click", export_bookmarks);
  const vers = document.createElement("a");
  vers.innerHTML = "SheetJS " + version;
  root.appendChild(vers);
  vers.addEventListener("click", () => { chrome.tabs.create({url: "https://sheetjs.com/"}); });
});
```
6) Replace `src/background/index.ts` with the following codeblock:
```ts title="src/background/index.ts"
chrome.runtime.onInstalled.addListener(function() {
  chrome.contextMenus.create({
    type: "normal",
    id: "sjsexport",
    title: "Export Table to XLSX",
    contexts: ["page", "selection"]
  });
  chrome.contextMenus.create({
    type: "normal",
    id: "sj5export",
    title: "Export All Tables in Page",
    contexts: ["page", "selection"]
  });
  chrome.contextMenus.onClicked.addListener(function(info/*, tab*/) {
    var mode = "";
    switch(info.menuItemId) {
      case 'sjsexport': mode = "JS"; break;
      case 'sj5export': mode = "J5"; break;
      default: return;
    }
    chrome.tabs.query({active: true, currentWindow: true}, function(tabs){
      chrome.tabs.sendMessage(tabs[0].id, {Sheet:mode}, sjsexport_cb);
    });
  });
  chrome.contextMenus.create({
    id: "sjsabout",
    title: "About",
    contexts: ["browser_action"]
  });
  chrome.contextMenus.onClicked.addListener(function(info/*, tab*/) {
    if(info.menuItemId !== "sjsabout") return;
    chrome.tabs.create({url: "https://sheetjs.com/"});
  });
});
function sjsexport_cb(wb) {
  if(!wb || !wb.SheetNames || !wb.Sheets) { return alert("Error in exporting table"); }
  const b64 = XLSX.write(wb, {bookType: "xlsx", type: "base64"});
  chrome.downloads.download({
    url: `data:application/octet-stream;base64,${b64}`,
    filename: `SheetJSTables.xlsx`
  })
}
```
7) Replace `src/contentScript/index.ts` with the following codeblock:
```ts title="src/contentScript/index.ts"
import { utils } from 'xlsx';
var coords = [0,0];
document.addEventListener('mousedown', function(mouse) {
  if(mouse && mouse.button == 2) coords = [mouse.clientX, mouse.clientY];
});
chrome.runtime.onMessage.addListener(function(msg, sender, cb) {
  if(!msg || !msg['Sheet']) return;
  if(msg.Sheet == "JS") {
    var elt = document.elementFromPoint(coords[0], coords[1]);
    while(elt != null) {
      if(elt.tagName.toLowerCase() == "table") return cb(utils.table_to_book(elt));
      elt = elt.parentElement;
    }
  } else if(msg.Sheet == "J5") {
    var tables = document.getElementsByTagName("table");
    var wb = utils.book_new();
    for(var i = 0; i < tables.length; ++i) {
      var ws = utils.table_to_sheet(tables[i]);
      utils.book_append_sheet(wb, ws, "Table" + i);
    }
    return cb(wb);
  }
  cb(coords);
});
```
8) Open `chrome://extensions/` in the browser and enable Developer mode
9) Click "Load unpacked" and select the `build` folder within the project.
  
### Bookmark Exporter
  Testing (click to hide)
0) Open https://sheetjs.com in the browser and create a bookmark.
1) Click the Extensions icon (puzzle icon to the right of the address bar) and
select "SheetJS Demo".
2) If a small popup is not displayed, click on the SheetJS icon
3) Click "Export Bookmarks" and click "Save". Open the downloaded file!
 
```mermaid
sequenceDiagram
  actor U as User
  participant P as Popup
  participant A as Chromium
  U->>P: click icon
  P->>A: `chrome.bookmarks.getTree`
  A->>P: bookmark tree
  Note over P: walk tree
  Note over P: make workbook
  P->>U: `XLSX.writeFile`
```
`chrome.bookmarks` API enables bookmark tree traversal.  The "Export Bookmarks"
button in the extension pop-up recursively walks the bookmark tree, pushes the
bookmark URLs into a data array, and exports into a simple spreadsheet.
```js
/* walk the bookmark tree */
function recurse_bookmarks(data, tree) {
  if(tree.url) data.push({Name: tree.title, Location: tree.url});
  (tree.children||[]).forEach(function(child) { recurse_bookmarks(data, child); });
}
/* get bookmark data */
chrome.bookmarks.getTree(function(res) {
  /* load into an array */
  var data = [];
  res.forEach(function(t) { recurse_bookmarks(data, t); });
  /* create worksheet */
  var ws = XLSX.utils.json_to_sheet(data, { header: ['Name', 'Location'] });
  /* create workbook and export */
  var wb = XLSX.utils.book_new();
  XLSX.utils.book_append_sheet(wb, ws, 'Bookmarks');
  XLSX.writeFile(wb, "bookmarks.xlsx");
});
```
### Table Exporter
:::caution pass
**Due to security restrictions, V2 table export does not work in Chrome 131!**
The Manifest V3 table exporter does work in Chrome 131.
:::
  Testing (click to hide)
1) Open https://sheetjs.com/demo/table in the browser.
2) Right-click anywhere in the page and select "SheetJS Demo" > "Export All Tables in Page"
3) Save and open the downloaded file!
 
The background script configures a context menu with the option to export data.
The flow diagrams show the data flow when the user chooses to export. They
differ in the denouement
```mermaid
sequenceDiagram
  actor U as User
  participant P as Background Script
  participant A as Content Script
  U->>P: Context Click > "Export"
  Note over P: Query for active tab
  P->>A: Ask active tab for data
  Note over A: `table_to_sheet`
  Note over A: generate workbook
  A->>P: workbook object
  Note over U,A: ... different denouement for Manifest V2 / V3 extensions ...
```
#### Manifest V2
For Manifest V2 extensions, `XLSX.writeFile` just works:
```mermaid
sequenceDiagram
  actor U as User
  participant P as Background Script
  Note over P,U: ... background script received workbook ...
  P->>U: `XLSX.writeFile` download
```
#### Manifest V3
For Manifest V3 extensions, since `URL.createObjectURL` is not available in
background service workers, a synthetic URL is created:
```mermaid
sequenceDiagram
  actor U as User
  participant P as Background Script
  Note over P,U: ... background script received workbook ...
  Note over P: `XLSX.write` Base64
  Note over P: Create Data URL
  P->>U: `chrome.downloads.download`
```
[^1]: See the [`create-chrome-ext` package](https://github.com/guocaoyi/create-chrome-ext) for more details.