Browser StudioBrowser Studio
October 18, 2026·7 min read·Userscripts

Prototype your Chrome extension idea as a userscript first

Test the DOM part of a Chrome extension idea in a day with a userscript, then move it into a content script. A worked example, and what carries over.


Most Chrome extension ideas are, at their core, a change to a web page. Add a button here, extract that table, hide this panel, reformat that list. The manifest, the service worker, the options page and the Web Store listing all come later, and they take days. The part that decides whether the idea works at all is the DOM code, and you can test that in an afternoon with a userscript.

This post walks through that approach: why it saves time, a worked example, what carries over when you graduate to an extension, and what doesn't.

Why prototype as a userscript

A userscript is one JavaScript file with a short header, run by a manager such as Tampermonkey or Violentmonkey. Compared with an unpacked extension, it removes almost every step between writing code and seeing it on the page:

  • No package. No manifest.json, no folder, no reload button on chrome://extensions.
  • Edit and refresh. Save the script in the manager's editor, reload the page, see the change.
  • Real pages, real data. You're testing against the live site you care about, signed in, with your own data.
  • Easy to share. Send a .user.js link to three people who'd use the extension and watch whether they keep it on.

What you learn in that first day is the expensive knowledge: whether the site's structure is stable enough to target, whether the data you need is on the page or loaded later, whether the feature is actually useful when you have it.

A worked example: copy as Markdown

Say your extension idea is a button that copies the current article's title and link as a Markdown link, for people who keep notes in Obsidian. Here's the whole prototype.

// ==UserScript==
// @name         Copy as Markdown link
// @namespace    https://example.com/your-name
// @version      0.1.0
// @description  Adds a button that copies the page title and URL as a Markdown link
// @match        https://news.example.com/articles/*
// @grant        GM_setClipboard
// ==/UserScript==

(function () {
  const heading = document.querySelector("article h1")
  if (!heading) return

  const button = document.createElement("button")
  button.textContent = "Copy as Markdown"
  button.style.marginLeft = "12px"
  button.addEventListener("click", () => {
    const title = heading.textContent.trim()
    GM_setClipboard(`[${title}](${location.href})`, "text")
    button.textContent = "Copied"
    setTimeout(() => (button.textContent = "Copy as Markdown"), 1500)
  })

  heading.append(button)
})()

Twenty lines, and you already know the answer to the important questions. Does article h1 find the heading on every article? Does the title need cleaning, for example removing a site name suffix? Is a button next to the heading where people expect it, or would a keyboard shortcut be better?

Two free tools help at this stage. The userscript header generator writes a correct header and checks every @match line as you type. The CSS selector tester lets you paste a piece of the page's HTML and try selectors against it, which is handy when the site's markup is messy.

A checklist for prototype day

Give the prototype one day and use it to answer specific questions, not to polish:

  1. Can you find the elements? Write the selectors and load ten different pages of the site. If any page breaks them, you've learned something you'd otherwise discover from one-star reviews.
  2. Is the data on the page? Some sites load the interesting part later from an API. If the content arrives late, you'll need a MutationObserver in the extension too, so put it in the prototype now.
  3. Does it survive navigation? Single-page apps change the URL without reloading. Click around and check the feature reappears where it should.
  4. Is it fast? Open the Performance panel once. A feature that slows the page down won't survive as an extension either.
  5. Would people keep it on? Share the .user.js link with a few target users and ask after a week whether they still have it enabled.

If the answers are good, you have a tested core and a list of edge cases. If they're bad, you've saved yourself a manifest, an icon set, a listing and a review cycle.

What carries over to a content script

When the prototype has earned its keep, most of it moves into an extension's content script almost unchanged.

The DOM code carries over. querySelector, creating elements, event listeners, MutationObserver: content scripts run in the page's document just like a userscript does, in their own isolated world.

@match becomes matches. Your header line turns into the content_scripts entry in the manifest:

"content_scripts": [
  {
    "matches": ["https://news.example.com/articles/*"],
    "js": ["content.js"]
  }
]

The pattern syntax is the same, because userscript managers adopted Chrome's match-pattern rules. If you tested your patterns in the @match tester, they're already right.

@run-at maps to run_at. document-start, document-end and document-idle become document_start, document_end and document_idle, with underscores in the manifest. They mean the same thing in both. Idle is the default in Chrome and in Tampermonkey; other managers may differ.

What doesn't carry over

The GM_ functions are the main thing to replace. They're the manager's API, not Chrome's.

  • GM_setValue and GM_getValue become chrome.storage.local.set and chrome.storage.local.get. Note that the Chrome versions are asynchronous.
  • GM_xmlhttpRequest becomes a fetch from the content script, if the target allows it, or a message to your service worker, which can fetch from any origin in your host_permissions.
  • GM_setClipboard becomes navigator.clipboard.writeText, which works from a content script after a user gesture such as a click.
  • GM_registerMenuCommand becomes an item in your action popup, or a chrome.contextMenus entry registered from the service worker.
  • GM_addStyle becomes a CSS file listed under css in the content script entry, or a style element you inject.

You'll also add the things a userscript never needed: an icon set, a listing, a privacy disclosure, and a decision about which permissions to request. Every host pattern you list shows up in the install prompt, so keep them as narrow as your prototype proved they could be.

When to graduate

Keep it a userscript while you're still learning what the feature should do. Move to an extension when one of these becomes true:

  • you need something only an extension can do, such as a side panel, a toolbar popup, background work, or reacting to tab events;
  • the people who'd use it don't have a userscript manager and won't install one;
  • you want a Web Store listing, with search traffic, ratings and automatic updates;
  • you want to charge for it, which is far easier with an extension and a proper account system.

Many ideas never need to graduate. A tool you and a few colleagues use on one internal site is perfectly happy as a userscript forever.

Generating the first draft

The slowest part of the prototype is usually the first version of the selectors: opening DevTools, finding the right element, discovering that the class names are generated and change every release. This is where AI helps, with one caveat. A chat assistant asked to "write a userscript for news.example.com" has never seen the page, so it guesses the structure and the script often finds nothing.

ScriptPanda, an AI userscript builder that we build, works from the page you're on. You describe the feature in the side panel, it reads a trimmed outline of the page's structure, writes the userscript, installs it and runs it, so you see straight away whether it does what you meant. You can open the generated code in its editor, which makes it a quick way to get a working first draft that you then move into a content script yourself.

Whichever way you write it, the principle holds: spend the first day on the part that can fail, the page, and only then build the package around it.

Keep going

Know why your users uninstall.

One line of code. Structured feedback. Drop-off funnels.

Start free