Build a browser extension that makes flashcards
In this tutorial you build a browser extension that sends the text you select on any page to Anki. Select a sentence and a small export button appears beside it; one click and the card is in your collection with a link back to the page. The toolbar popup does the same with an editable card and a deck list. It uses WXT for the extension, Base UI for the interface, and the TypeScript SDK for the Ankinize API, so it works with Anki closed and syncs to every device.
What you will build#
Three parts. A selection widget is a round export button that appears beside any text you select; clicking it adds a card to your starting deck. A background script sends the card to Ankinize. The popup opens from the toolbar, reads the selection of the current tab, and shows a form with the front, an optional back, a link switch and a deck list. The options page stores your server, your API key and a starting deck. The cards are Basic notes, so fields has two keys, front and back.
The finished project, with every file and a README, is in the ankinize-examples repository. Each step below shows a file from it, unchanged.
- Bun and a Chromium browser (or Firefox).
- An Ankinize account with an API key that has the
writescope, from API Keys & Devices. Make a key for this extension alone. - At least one deck in your collection.
Build it#
Create the project
WXT scaffolds the extension. Choose the React template, then add the SDK and Base UI. Base UI ships components with behaviour and accessibility but no styles, which suits an extension: you write the few rules you need and nothing else is bundled.
web-extis what letsbun run devopen the browser for you; without it WXT only prints where the build is.Terminal This tutorial keeps the code under
src/(thesrcDiroption in the config, step 2). A WXT project finds its pages insrc/entrypoints: a folder with anindex.htmlnamedpopupbecomes the toolbar popup, and one namedoptionsthe settings page. WXT writes the manifest from them, so you only declare what they cannot say.Declare the permissions
activeTabwithscriptinglets the popup read the selection of the tab you are on when you open the popup, and only that tab. The widget is a content script, which the manifest lists from itsmatchesin the next steps; it needs no permission of its own.host_permissionsnames the Ankinize server, which lets the popup call it without CORS. The optional permission is for self-hosters: the options page asks for your own server's origin when you save.wxt.config.ts Store the settings
WXT's
storagewraps the extension storage with types and a fallback. Using thelocal:area keeps the key on this device and out of Chrome sync.src/lib/settings.ts Read the selection
When the popup opens, run a one-line function in the active tab and return
window.getSelection(). A page's own scripts never see the API key, because the key only ever lives in the popup. Pages that cannot be scripted (the browser's own settings pages, the web store) throw, so the popup falls back to an empty selection and you can type the card by hand.src/lib/selection.ts Turn text into card fields
A note's
fieldsare an object keyed by each field's name in camelCase, so Basic'sFrontandBackarefrontandback. Card fields hold HTML, and a selection or a page title can contain markup, so everything is escaped before it goes in, and onlyhttp(s)addresses become links. Keeping this in its own module makes it easy to test.src/lib/card.ts Make the form controls with Base UI
Fieldconnects a label, a hint and an error message to its control, andField.Controlrenders a textarea when you pass one torender.Selectgives a keyboard-driven list with a trigger, a popup and items, and you style them through the class names anddata-*attributes it sets, such asdata-highlighted. The stylesheet is in the repository.src/ui/Field.tsx src/ui/DeckSelect.tsx Write the options page
A form with three fields. When the server is not
ankinize.com, saving first asks the browser for access to that origin. The request has to start from the click, so it comes before anything else in the handler. Base UI'sFormchecks the required fields before it calls you.src/entrypoints/options/App.tsx Send the card to Ankinize
One function adds a Basic card, and the popup and the widget both use it. The
idempotencyKeymakes a double click or a retry safe: the second request returns the first answer instead of adding another card. Errors from the API arrive as anAnkinizeErrorwith a readable message, such as a deck that does not exist or a key without the write scope.src/lib/export.ts Add the background script
A page's scripts must never see your API key, and a content script's requests are subject to the page's CORS rules, so the widget cannot call the API itself. It sends the selection to the extension's service worker, which holds the key and has the host permission, and answers with the result. Returning
truefrom the listener keeps the channel open for that later answer.src/entrypoints/background.ts The message types are the small
src/lib/messages.tsin the repository.Add the selection widget
A content script runs inside pages.
createShadowRootUimounts our React tree in a shadow root, andcssInjectionMode: "ui"puts the stylesheet inside it, so the page cannot restyle the button and the button cannot leak styles into the page. Matching<all_urls>means the script is loaded on every page, so it only watches for a selection and reads nothing else.src/entrypoints/selection-widget.content/index.tsx The widget shows the button after you finish a selection (on mouse up or a key up), puts it under the end of the selection, and hides it when the selection clears. Pressing the button does not clear the selection, because the container cancels
mousedown. The button is Base UI'sButton, a plain<button>with the disabled state handled for you; the icon changes to a check when the card is added, or a cross with the reason when it is not. Its stylesheet,style.css, is in the repository.src/entrypoints/selection-widget.content/SelectionWidget.tsx Write the popup
On open it loads the settings and the selection together, then lists your decks with
listDecks. Pressing Add card callsaddCardwith the front and back you edited and the deck you chose. Use the popup when you want to reword the card or pick another deck; use the widget when the selection is already the card.src/entrypoints/popup/App.tsx The popup's
main.tsx, the twoindex.htmlfiles andsrc/lib/api.ts(which points the SDK at the saved server) are a few lines each; copy them from the repository.Run it
bun run devopens a browser with the extension loaded and reloads it as you edit. Open the settings from the extension's menu, enter your key, select some text on a page and click the export button beside it (reload tabs that were open before you installed the extension). The card appears in your collection at once, and on your other devices at their next sync. The toolbar popup works too.Terminal
Make it your own#
- Other note types. Use
getNoteTypeto list a type's field keys, then changenotetypeNameandfieldsto match. - A right-click menu. Add the
contextMenuspermission and a background script that callscreateNotestraight from the selection. - Check for duplicates. Before adding, search with
listNotesand aqbuilt withq.fieldto see whether the front already exists.
Next#
Let an agent do the choosing: AI agents and Ankinize. Every endpoint is in the API reference.