CrescentHandbook
Userscript card
Cards

The Userscript card

Write a userscript or browser extension that changes a website you didn’t make: fix what annoys you, add what’s missing, or make it weird.

Change something in a website you didn’t make! A few lines of JavaScript in your own browser can rewrite any page as it loads, and the site never finds out.

What counts

Code that changes a website someone else made, running in the visitor’s browser. A userscript counts: that’s a bit of JavaScript that a userscript manager runs on the pages you pick. So does a browser extension with a content script that reaches into the page.

The website has to be someone else’s. Changing your own site is just… making your website, which sounds more like the Homepage card! An extension that never touches a page doesn’t count either, so a new-tab page or a calculator in a popup is out.

A theme that’s only CSS (the kind you’d load with Stylus) is a bit thin on its own. Adding some JavaScript that changes how the site actually behaves makes it count for sure. If you’ve restyled a whole site from top to bottom and you’re not sure, ask in #crescent-help before you build on it.

Getting started, step by step

This gets you from nothing to a script that works on a real site. It’s deliberately a boring example, so please don’t ship it! The idea is yours.

1. Get a userscript manager

Violentmonkey is free and open source, and works in Chrome, Firefox, Edge and most other browsers. Tampermonkey is the most popular one and is totally fine too. On Safari, there’s Userscripts.

On Chrome and the browsers built on it, there’s one extra switch these days, since extensions now need your permission to run code you wrote yourself. Right-click the manager’s icon, pick Manage extension, and turn on Allow user scripts. If you can’t find it, your browser is older and wants Developer mode switched on at chrome://extensions instead (Tampermonkey’s FAQ has pictures). If your scripts silently do nothing on Chrome, this is almost always why! Firefox doesn’t need it.

2. Write the header

Make a new script from the manager’s dashboard. Every userscript starts with a comment that tells the manager what it is and where it runs:

// ==UserScript==
// @name        Reading time for Wikipedia
// @namespace   https://github.com/your-name
// @version     0.1
// @match       https://en.wikipedia.org/wiki/*
// @grant       none
// ==/UserScript==

@match is the important line. Your script only runs on pages whose address fits the pattern, and * means anything can go there. @grant none says you don’t need any of the manager’s special functions yet. Violentmonkey’s docs list everything else the header can hold.

3. Make a visible change

Under the header, plain DOM code:

let words = 0;
for (const paragraph of document.querySelectorAll("#mw-content-text p")) {
  words += paragraph.textContent.split(/\s+/).length;
}

const note = document.createElement("p");
note.textContent = `About ${Math.ceil(words / 230)} minutes to read`;
document.querySelector("#firstHeading").after(note);

Save it, open any Wikipedia article (or reload one you already had open), and there it is under the title! If nothing shows up, press F12 and look in the Console for a red error.

4. Find your own selectors

I already knew Wikipedia’s ids, which made that easy. On any other site, open DevTools (F12), click the element picker in its top-left corner (or press Ctrl+Shift+C, Cmd+Shift+C on a Mac) and click the thing you want to change. The Elements panel jumps straight to its HTML. Chrome’s docs have a good tour of that panel.

Try your selector in the Console before you put it in your script: document.querySelectorAll("...") shows you exactly what it matches. Right-click, Copy, Copy selector also works, but it’ll give you something like #app > div:nth-child(3) > div > span, which breaks the moment the site moves anything. There’s more on that further down.

5. Wait for the page

Lots of sites build their pages with JavaScript after they load. If querySelector gives you null for something you can clearly see on screen, it didn’t exist yet when your script ran. A MutationObserver tells you whenever the page changes, and this little helper calls your function once for every matching element, whether it’s there now or turns up later:

function onEach(selector, callback) {
  const seen = new WeakSet();
  const check = () => {
    for (const element of document.querySelectorAll(selector)) {
      if (seen.has(element)) continue;
      seen.add(element);
      callback(element);
    }
  };
  check();
  new MutationObserver(check).observe(document.body, { childList: true, subtree: true });
}

onEach("article", (post) => {
  post.style.outline = "3px dashed hotpink";
});

Single-page apps (YouTube, Reddit, GitHub and lots more) have one more catch: clicking a link doesn’t load a new page, it swaps the content in place. Your script runs once, and @match isn’t checked again as you click around. Match the whole site instead, and look at location.pathname inside your callback to decide what to do.

6. Add some style

CSS is often the easiest way to hide or restyle things. Put a <style> on the page:

const style = document.createElement("style");
style.textContent = `
  #firstHeading + p {
    font-style: italic;
    color: gray;
  }
`;
document.head.append(style);

GM_addStyle(css) does the same thing in one line, once you add // @grant GM_addStyle to the header. If you’re hiding something and it flashes on screen before it disappears, add // @run-at document-start too, so your CSS goes in before the page draws anything.

Now it’s your turn

That’s all the machinery! Which site, and what you change about it, is up to you. Some places to start looking:

  • The site you use most. What do you do on it every day that takes several clicks and should take one?
  • Something it’s missing. A dark mode, keyboard shortcuts, real dates where it says “3 days ago”, a word counter, a filter it should’ve had all along…
  • Something that annoys you. Autoplay, recommendations, the bits designed to keep you scrolling.
  • Something someone else struggles with. Text that’s too small, buttons with no labels for a screen reader, colours that are hard to tell apart.
  • Make it weird! Cloud to Butt, a classic extension from 2013, swapped “the cloud” for “my butt” on every page you visited. Surely you can beat that.

Going further

  • Remember things. GM_getValue and GM_setValue keep settings between visits, and GM_registerMenuCommand adds an item to the manager’s menu, which is a handy place for a settings switch. Each one needs its own @grant line. Violentmonkey documents all of the GM_ functions.

  • Talk to other sites. A normal fetch from the page is usually blocked from reading other sites. GM_xmlhttpRequest isn’t, so you can pull in data from an API and show it right on the page. Tampermonkey will also ask you to add a @connect line naming the site you talk to.

  • Turn it into a real extension. A Manifest V3 extension with a content script is mostly your userscript without the header, plus a manifest.json:

    {
      "manifest_version": 3,
      "name": "Reading time",
      "version": "0.1",
      "content_scripts": [
        {
          "matches": ["https://en.wikipedia.org/wiki/*"],
          "js": ["content.js"]
        }
      ]
    }
    

    Chrome’s Hello World tutorial and MDN’s Your first extension take it from there, including how to give it a popup or an options page.

  • Selectors that survive a redesign. Sites change their HTML whenever they feel like it. Ids, data-testid, aria-label and role attributes tend to stick around; long div:nth-child chains and generated class names like css-1x8f3k don’t. When something you’re looking for isn’t there, have your script quietly do nothing rather than crash, so a redesign breaks one feature and not the whole page.

A few things to stay away from

Please don’t make anything that cheats or automates abuse, like auto-farming, game bots, spam, or fake clicks on ads. Nothing that gets around a paywall either, and nothing that sends other people’s data anywhere. If the people who use that site would be upset to find out what your script does, pick a different idea!

Before you ship

There’s a few things every project needs before we can approve it:

  • It works on the site as it is today. It doesn’t have to be finished, but the main thing it does should actually work.
  • All of your code is public, on GitHub or somewhere like it, and readable (not minified or obfuscated).
  • Anyone can install it in under two minutes of setup, even someone who doesn’t code.
  • Someone else could rebuild it from your repo and README.

What that looks like depends on what you made:

  • A userscript: a Greasy Fork listing, or a link to the raw .user.js file on GitHub. As long as the file name ends in .user.js, Violentmonkey and Tampermonkey offer to install it as soon as you open the link. Mention the Chrome switch from step 1 in your README, since a lot of people won’t have it on yet!
  • An extension: a listing on the Chrome Web Store or Firefox Add-ons, or a GitHub release with a zip and clear steps for loading it (chrome://extensions, Developer mode, Load unpacked in Chrome; about:debugging, This Firefox, Load Temporary Add-on in Firefox). The Chrome Web Store charges a one-time $5 fee to publish, so a GitHub release is completely fine.

We also need to see the change ourselves, on a site we can open without paying or making a special account. If your change lives behind a login (your school’s portal, a game you have to sign up for…), your README needs screenshots or a video that show it working properly. Put videos on YouTube or Vimeo, or upload an .mp4, rather than Google Drive.

Shipping one

  • Demo: the install link, plus the exact page we should open to see it working. Tell us where to look!
  • Screenshot: a before and after of the same page, side by side.
  • README: what it changes and why, which sites it runs on, and how to install it. A short video is great if it does anything that moves, and if the site fought you on something, tell us how you got around it!
Last updated 28 Oct 2026