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

How to write a Tampermonkey userscript, with an example

Write your first Tampermonkey userscript: install it, write the header, build a working script that hides an element and adds a button, and debug it.


A userscript is the fastest way to change a website you use every day. You write a few lines of JavaScript, tell a manager which pages to run them on, and the change is there every time the page loads. This tutorial goes from nothing to a working script: installing Tampermonkey, writing the header, a complete example that hides an element and adds a button, handling pages that load content late, and debugging when nothing happens.

Step 1: Install Tampermonkey

Tampermonkey is a userscript manager, a Chrome extension that runs your scripts on the pages they ask for. Install it from the Chrome Web Store. As of October 2026 its listing describes it as a way to "change the web at will with userscripts", with an editor, quick on and off switches, sync and automatic updates.

There's one more step that catches almost everyone. Since Manifest V3, Chrome only lets extensions run user-supplied scripts through its chrome.userScripts API, and the user has to allow it:

  1. Open chrome://extensions.
  2. Find Tampermonkey and click Details.
  3. Turn on Allow User Scripts.

On Chrome versions before 138, the switch was Developer mode at the top of chrome://extensions instead. If your scripts never run, check this first.

Step 2: Understand the header

Click the Tampermonkey icon and choose Create a new script. The editor opens with a template. The comment block at the top is the header:

// ==UserScript==
// @name         My first script
// @namespace    https://example.com/your-name
// @version      0.1.0
// @description  Hides the sidebar and adds a button
// @match        https://example.com/*
// @grant        none
// ==/UserScript==

What each line does:

  • @name is the label in Tampermonkey's dashboard. Together with @namespace, it identifies the script, so use a URL you own as the namespace.
  • @version matters when you share the script: Tampermonkey compares versions to decide whether to update.
  • @match says which pages the script runs on. You can have several. More on getting these right below.
  • @grant lists the special functions the script needs from Tampermonkey, such as GM_setValue for storage. none means it needs none, and in Tampermonkey it also turns the sandbox off, leaving only GM_info available.
  • @run-at, not shown, says when to inject. The default is document-idle, after the page's HTML has loaded.

Rather than typing headers by hand, you can fill in a form with the free userscript header generator, which also checks each @match line.

Step 3: A complete working script

Here's a script that does two common jobs: hides an element you don't want, and adds a button that does something useful. It targets a hypothetical news site, so change the @match line and the selectors to your site.

// ==UserScript==
// @name         Cleaner articles
// @namespace    https://example.com/your-name
// @version      0.1.0
// @description  Hides the related-stories sidebar and adds a Copy link button
// @match        https://news.example.com/*
// @grant        GM_addStyle
// @grant        GM_setClipboard
// ==/UserScript==

(function () {
  "use strict"

  // 1. Hide the sidebar with CSS, so it never flashes on screen.
  GM_addStyle(`
    aside.related-stories { display: none !important; }
    main.article { max-width: 760px; margin: 0 auto; }
  `)

  // 2. Add a button under the headline.
  function addButton(heading) {
    if (heading.querySelector(".copy-link-btn")) return
    const button = document.createElement("button")
    button.className = "copy-link-btn"
    button.textContent = "Copy link"
    button.style.cssText = "margin-left:12px;font-size:14px;padding:4px 10px;cursor:pointer"
    button.addEventListener("click", () => {
      GM_setClipboard(`${heading.textContent.trim()}\n${location.href}`, "text")
      button.textContent = "Copied"
      setTimeout(() => (button.textContent = "Copy link"), 1500)
    })
    heading.append(button)
  }

  const heading = document.querySelector("main.article h1")
  if (heading) addButton(heading)
})()

Save with Ctrl+S, open an article on the site, and you should see the sidebar gone and the button next to the headline.

A few habits in that script are worth keeping:

  • Hide with CSS, not with remove(). A style rule applies as soon as the element appears, even if it's added later, and there's no flash.
  • Guard against running twice. The .copy-link-btn check stops duplicate buttons when the function is called again.
  • Wrap the script in a function so your variables don't collide with the page's.

Step 4: Get @match right

If nothing happens at all, the most likely cause is a @match line that matches no page. Tampermonkey uses Chrome's match-pattern rules:

  • The pattern needs a scheme, a host and a path. https://example.com with no path is invalid. Write https://example.com/*.
  • A * scheme matches http and https only.
  • https://*.example.com/* matches example.com and every subdomain.
  • A * in the host can only be the whole host or the first label. https://shop.*.com/* is invalid.
  • Patterns without a port match any port, which is handy for http://localhost/*.
  • The path part is compared with the path and the query string, so /item* matches /item?id=42.

Paste your patterns and some real URLs into the @match pattern tester to see which pages each one covers, with each mistake explained.

Step 5: Handle pages that load content later

Modern sites often draw content after the page loads, and single-page apps change "pages" without reloading at all. A script that runs once at document-idle can miss everything. The fix is a MutationObserver, which tells you when the page changes:

const observer = new MutationObserver(() => {
  const heading = document.querySelector("main.article h1")
  if (heading) addButton(heading)
})
observer.observe(document.body, { childList: true, subtree: true })

Because addButton checks for an existing button, calling it on every change is safe. For heavy pages, keep the callback cheap: check for the element and return quickly.

Step 6: Debug when it doesn't work

Work through these in order:

  1. Is the script running at all? Add console.log("cleaner articles loaded") at the top, reload, and look in DevTools (F12, Console tab). The Tampermonkey icon also shows a count of scripts running on the page.
  2. Is Allow User Scripts on? See step 1.
  3. Does the selector find anything? In the console, run document.querySelector("main.article h1"). If it returns null, the selector is wrong or the element isn't there yet. Copy the element's HTML from the Elements panel and try selectors in the CSS selector tester.
  4. Is it a timing problem? If the selector works in the console but not in the script, the content arrives late. Use the MutationObserver above.
  5. Is a grant missing? Calling GM_setClipboard without granting it throws an error you'll see in the console.

Prefer selectors built on stable attributes, such as data-testid, aria-label or readable class names, over generated class names like css-1x2y3z, which change whenever the site ships.

When to let AI write it

Once you know the shape of a userscript, AI can write the first draft. Asking a chat assistant works for simple scripts, but the assistant can't see the page, so it guesses the selectors, and the usual result is a script that runs and finds nothing.

ScriptPanda, an AI userscript builder made by our studio, takes a different approach: you describe the change in a side panel on the page itself, it reads a trimmed outline of the page's structure, then writes, installs and runs the script so you see the result immediately. It doesn't run existing Tampermonkey scripts, so it complements a manager rather than replacing one. If you'd rather write every line yourself, everything above is all you need.

Keep going

Know why your users uninstall.

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

Start free