Learn to write powerful userscripts with practical examples, from basic DOM manipulation to advanced techniques using Greasemonkey APIs.
Userscripts often start with a metadata block that describes the script and how it should run. You don’t have to add this manually—CodeTweak can generate and maintain it for you based on the sidebar settings. If you do add or edit a metadata block in the code editor, CodeTweak will sync those values with the sidebar and keep them up to date.
Common fields you’ll see and what they do:
// ==UserScript==
// @name Example Script
// @namespace https://codetweak.local
// @version 1.0.0
// @description Demonstrates a typical metadata header
// @author You
// @match *://example.com/*
// @run-at document-idle
// @grant GM_getValue
// @grant GM_setValue
// @require https://cdn.example.com/some-lib.min.js
// @resource logo https://example.com/logo.png
// ==/UserScript==
Open the editor by clicking "New Script" from the dashboard. The sidebar helps you configure your script settings while you write code in the main area.
Let's create a simple script that adds a greeting message to any webpage:
// ==UserScript==
// @name Hello World
// @description Adds a greeting to any webpage
// @author You
// @version 1.0
// @match *://*/*
// ==/UserScript==
(function() {
'use strict';
// Create a greeting element
const greeting = document.createElement('div');
greeting.textContent = 'Hello from CodeTweak!';
greeting.style.cssText = `
position: fixed;
top: 10px;
right: 10px;
background: #4ea1ff;
color: white;
padding: 10px 15px;
border-radius: 5px;
z-index: 10000;
font-family: Arial, sans-serif;
`;
// Add to page
document.body.appendChild(greeting);
// Remove after 3 seconds
setTimeout(() => greeting.remove(), 3000);
})();
Most userscripts modify webpage content. Here are essential techniques:
// Find elements by different selectors
const button = document.querySelector('.submit-btn');
const allLinks = document.querySelectorAll('a');
const byId = document.getElementById('main-content');
const byClass = document.getElementsByClassName('article');
// Check if element exists before using
if (button) {
button.click();
}
// Change text content
document.querySelector('h1').textContent = 'New Title';
// Change HTML content
document.querySelector('.content').innerHTML = 'New content
';
// Modify attributes
const img = document.querySelector('img');
img.src = 'new-image.jpg';
img.alt = 'New description';
// Add/remove CSS classes
element.classList.add('highlight');
element.classList.remove('hidden');
element.classList.toggle('active');
// Create and configure element
const newDiv = document.createElement('div');
newDiv.className = 'my-custom-element';
newDiv.textContent = 'Dynamic content';
// Style the element
newDiv.style.cssText = `
background: #f0f0f0;
padding: 10px;
margin: 5px 0;
border-radius: 3px;
`;
// Add to page
document.body.appendChild(newDiv);
Here's a real-world script that adds a download button to YouTube videos:
// ==UserScript==
// @name YouTube Download Helper
// @description Adds download button to YouTube videos
// @author You
// @version 1.0
// @match https://www.youtube.com/watch*
// ==/UserScript==
(function() {
'use strict';
function addDownloadButton() {
// Find the video controls area
const controls = document.querySelector('.ytp-right-controls');
if (!controls || document.querySelector('.download-btn')) return;
// Create download button
const downloadBtn = document.createElement('button');
downloadBtn.className = 'download-btn ytp-button';
downloadBtn.innerHTML = '⬇️';
downloadBtn.title = 'Download Video';
// Style to match YouTube's buttons
downloadBtn.style.cssText = `
background: none;
border: none;
color: white;
font-size: 16px;
cursor: pointer;
padding: 8px;
`;
// Add click handler
downloadBtn.addEventListener('click', () => {
const videoUrl = window.location.href;
const videoId = new URL(videoUrl).searchParams.get('v');
alert(`Video ID: ${videoId}\nUse a download service with this ID.`);
});
// Insert before settings button
controls.insertBefore(downloadBtn, controls.firstChild);
}
// Run when page loads and on navigation
addDownloadButton();
// YouTube is a SPA, so watch for URL changes
let currentUrl = location.href;
new MutationObserver(() => {
if (location.href !== currentUrl) {
currentUrl = location.href;
setTimeout(addDownloadButton, 1000);
}
}).observe(document, { subtree: true, childList: true });
})();
Store data across page visits using Greasemonkey storage APIs:
// ==UserScript==
// @name Visit Counter
// @description Counts how many times you visit a site
// @author You
// @version 1.0
// @match https://example.com/*
// @grant GM_getValue
// @grant GM_setValue
// ==/UserScript==
(function() {
'use strict';
async function updateVisitCounter() {
// Get current count (default to 0)
const currentCount = await GM_getValue('visitCount', 0);
const newCount = currentCount + 1;
// Save new count
await GM_setValue('visitCount', newCount);
// Display counter
const counter = document.createElement('div');
counter.textContent = `Visits: ${newCount}`;
counter.style.cssText = `
position: fixed;
top: 10px;
left: 10px;
background: #333;
color: white;
padding: 5px 10px;
border-radius: 3px;
z-index: 10000;
`;
document.body.appendChild(counter);
}
updateVisitCounter();
})();
Fetch data from external APIs using GM_xmlhttpRequest:
// ==UserScript==
// @name Weather Widget
// @description Shows weather on any page
// @author You
// @version 1.0
// @match *://*/*
// @grant GM_xmlhttpRequest
// ==/UserScript==
(function() {
'use strict';
function createWeatherWidget() {
const widget = document.createElement('div');
widget.id = 'weather-widget';
widget.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
background: rgba(0,0,0,0.8);
color: white;
padding: 15px;
border-radius: 8px;
z-index: 10000;
font-family: Arial, sans-serif;
min-width: 200px;
`;
widget.innerHTML = 'Loading weather...';
document.body.appendChild(widget);
// Fetch weather data (replace with real API)
GM_xmlhttpRequest({
method: 'GET',
url: 'https://api.openweathermap.org/data/2.5/weather?q=London&appid=YOUR_API_KEY',
onload: function(response) {
try {
const data = JSON.parse(response.responseText);
const temp = Math.round(data.main.temp - 273.15); // Convert K to C
const desc = data.weather[0].description;
widget.innerHTML = `
${data.name}
${temp}°C
${desc}
`;
} catch (e) {
widget.innerHTML = 'Weather unavailable';
}
},
onerror: function() {
widget.innerHTML = 'Weather error';
}
});
}
// Add widget when page loads
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', createWeatherWidget);
} else {
createWeatherWidget();
}
})();
Handle dynamic content and complex page modifications:
// Wait for element to appear (useful for dynamic sites)
function waitForElement(selector, timeout = 5000) {
return new Promise((resolve, reject) => {
const element = document.querySelector(selector);
if (element) {
resolve(element);
return;
}
const observer = new MutationObserver(() => {
const element = document.querySelector(selector);
if (element) {
observer.disconnect();
resolve(element);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
setTimeout(() => {
observer.disconnect();
reject(new Error(`Element ${selector} not found within ${timeout}ms`));
}, timeout);
});
}
// Usage
waitForElement('.dynamic-content').then(element => {
element.style.background = 'yellow';
}).catch(console.error);
// Handle clicks on dynamically added elements
document.addEventListener('click', function(e) {
// Check if clicked element matches our target
if (e.target.matches('.special-button')) {
e.preventDefault();
console.log('Special button clicked!');
// Modify the clicked element
e.target.textContent = 'Clicked!';
e.target.style.background = 'green';
}
});
// Add custom styles to the page
function addCustomCSS(css) {
const style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
}
// Usage
addCustomCSS(`
.annoying-popup {
display: none !important;
}
.content {
max-width: 800px !important;
margin: 0 auto !important;
}
`);
A comprehensive script that demonstrates multiple advanced techniques:
// ==UserScript==
// @name Reddit Enhancer
// @description Improves Reddit browsing experience
// @author You
// @version 1.0
// @match https://www.reddit.com/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addStyle
// ==/UserScript==
(function() {
'use strict';
// Configuration
const config = {
hideAds: true,
darkMode: false,
autoExpand: true
};
// Load saved settings
async function loadSettings() {
config.hideAds = await GM_getValue('hideAds', true);
config.darkMode = await GM_getValue('darkMode', false);
config.autoExpand = await GM_getValue('autoExpand', true);
}
// Apply enhancements
function enhance() {
if (config.hideAds) {
hideAdvertisements();
}
if (config.darkMode) {
enableDarkMode();
}
if (config.autoExpand) {
autoExpandPosts();
}
addSettingsButton();
}
function hideAdvertisements() {
const adSelectors = [
'[data-testid="ad-post-container"]',
'.promotedlink',
'.sponsored-post'
];
adSelectors.forEach(selector => {
document.querySelectorAll(selector).forEach(ad => {
ad.style.display = 'none';
});
});
}
function enableDarkMode() {
GM_addStyle(`
body, .Post, .Comment {
background: #1a1a1b !important;
color: #d7dadc !important;
}
.Post, .Comment {
border-color: #343536 !important;
}
`);
}
function autoExpandPosts() {
document.querySelectorAll('[data-click-id="text"]').forEach(post => {
if (post.textContent.includes('...')) {
post.click();
}
});
}
function addSettingsButton() {
const button = document.createElement('button');
button.textContent = 'Enhancer Settings';
button.style.cssText = `
position: fixed;
top: 10px;
right: 10px;
z-index: 10000;
background: #ff4500;
color: white;
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
`;
button.addEventListener('click', showSettings);
document.body.appendChild(button);
}
function showSettings() {
const settings = prompt('Settings (format: hideAds,darkMode,autoExpand)',
`${config.hideAds},${config.darkMode},${config.autoExpand}`);
if (settings) {
const [hideAds, darkMode, autoExpand] = settings.split(',').map(s => s.trim() === 'true');
GM_setValue('hideAds', hideAds);
GM_setValue('darkMode', darkMode);
GM_setValue('autoExpand', autoExpand);
location.reload();
}
}
// Initialize
loadSettings().then(() => {
enhance();
// Re-run on navigation (Reddit is SPA)
const observer = new MutationObserver(() => {
setTimeout(enhance, 1000);
});
observer.observe(document.body, {
childList: true,
subtree: true
});
});
})();
Write efficient, maintainable userscripts:
// ❌ Slow - searches entire document repeatedly
setInterval(() => {
document.querySelectorAll('.item').forEach(item => {
// process item
});
}, 1000);
// ✅ Fast - cache selectors and use event delegation
const container = document.querySelector('.items-container');
if (container) {
container.addEventListener('click', (e) => {
if (e.target.matches('.item')) {
// process clicked item
}
});
}
// ✅ Clean up observers and intervals
let observer;
let intervalId;
function cleanup() {
if (observer) {
observer.disconnect();
observer = null;
}
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
}
// Clean up when page unloads
window.addEventListener('beforeunload', cleanup);
// Wrap risky operations in try-catch
function safeOperation() {
try {
const element = document.querySelector('.might-not-exist');
if (!element) {
console.warn('Element not found, skipping operation');
return;
}
// Perform operation
element.textContent = 'Modified!';
} catch (error) {
console.error('Operation failed:', error);
}
}