VIN Highlighter
- Extensions and automation
- Browser Extensions
- 2026
- Open source
WXT, React, TypeScript
VIN Highlighter finds 17-character vehicle identification numbers anywhere on a page and turns each one into a link to a VIN console. It is a Manifest V3 extension built with WXT, React and TypeScript, with builds for Chrome and Firefox.
The whole behavior is one content script of roughly seventy lines. It stores nothing, sends nothing anywhere, and keeps no state between page loads. That narrowness is the point.
# content script running on an inventory listingwalkAndHighlight(document.body), one pass1HGBH41JXMN109186 -> linked5YJ3E1EA7JF006588 -> linkedWBA8E9G59GNT84064 -> linkedrows rendered after that pass stay unlinkedpermissions in use: activeTab, scripting
6 lines
- Chapter
- Extensions and automation
- Category
- Browser Extensions
- Year
- Availability
- Open source
- Last commit
- WXT
- React
- TypeScript
The copy paste loop
Working through inventory listings, the VIN is always the next step and always a manual one. Select the string, copy it, switch tabs, paste it into the console, come back. Each round trip is a few seconds, it happens all day, and none of it needs a person. The tedium is also where transcription mistakes come from. A seventeen character alphanumeric string typed by hand goes wrong quietly, and a wrong VIN looks exactly like a right one.
Walking the text
entrypoints/vinHighlighter.content.ts matches every URL and calls walkAndHighlight(document.body) once. The walk recurses through element, document and fragment nodes, and does its actual work on text nodes. Matches come from \b[A-HJ-NPR-Z0-9]{17}\b, a character set that excludes I, O and Q exactly as the standard does, so a look-alike string is not mistaken for one. A matching text node is rebuilt as a DocumentFragment: the text before the match, an anchor wrapping a span with a yellow background, then the remainder, then parent.replaceChild. Surrounding text survives intact. The link target is one template string in that file, so pointing the extension elsewhere is an edit rather than a rewrite.
The permission surface
The manifest asks for activeTab and scripting and nothing else. No storage permission, because there is nothing to store. There is no host permission list either, and that is the part worth reading closely. The content script matches *://*/*, so WXT registers it against every page the browser loads. The prompt a user sees stays short while the script itself runs everywhere. Nothing leaves the page in any case. The extension makes no network request of its own, and the only outbound step is a user clicking a link they can see.
Decisions and their limits
- One regex and no check digit validation. Anything seventeen characters long in the right alphabet gets a link. The script stays tiny and catches VINs on pages that format them oddly. It also links the occasional hash or token that happens to fit.
- A single pass at load, with no
MutationObserver. It never fights the page for the DOM. On a client-rendered inventory dashboard most rows arrive after that pass and never get linked. The README claims it catches new nodes as the page mutates. The code does not, and that is the first thing I would fix. - The walk skips nothing by tag, including
script,styleandtextarea. Those rarely hold a bare seventeen character token. When they do, the script rewrites markup never meant to be read. - Inline styles rather than an injected stylesheet. Two property assignments cover the whole appearance, and no CSS of mine can leak onto the host page. The highlight cannot adapt to a dark page, and it inherits the site’s link color.
Building and loading
pnpm install runs wxt prepare afterwards. pnpm dev starts a Chrome development build and pnpm dev:firefox the Firefox one. pnpm build and pnpm build:firefox write unpacked extensions into .output/, and pnpm zip packages one for a store. Loading it by hand means Developer mode and Load unpacked under chrome://extensions, or about:debugging in Firefox. pnpm compile runs tsc --noEmit and is the only check besides the build. CI runs both on pushes and pull requests to release, and a v* tag packages Chrome and Firefox zips into a GitHub release. A husky pre-commit hook runs Prettier over staged files.
Boilerplate still attached
Three files are still the WXT starter. entrypoints/content.ts is a second content script matching every page with an empty main(). entrypoints/background.ts logs a greeting and does nothing else. The popup is the unmodified WXT and React example with a counter button, so the toolbar icon opens that rather than anything about VINs. package.json carries the boilerplate name and a description reading manifest.json description. None of it breaks the extension, and all of it ships. It is MIT licensed and stays small because its job is specific: recognise a VIN, connect it to the next action.