Writing Userscripts

Learn to write powerful userscripts with practical examples, from basic DOM manipulation to advanced techniques using Greasemonkey APIs.

Userscript Metadata

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.

What the Metadata Block Contains

Common fields you’ll see and what they do:

Example

// ==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==
            
Good to know: You can omit the entire metadata block. The editor will generate it from your sidebar selections (name, URLs, run at, grants, requires, resources, etc.) and keep both the header and sidebar in sync.

Quick Start

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.

Writing Your First Script

Hello World Example

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);
})();
Try it: Copy this code into the editor, save it, and visit any website to see the greeting appear.

DOM Manipulation Basics

Most userscripts modify webpage content. Here are essential techniques:

Finding Elements

// 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();
}

Modifying Content

// 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');

Creating New Elements

// 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);

Practical Example: YouTube Enhancer

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 });
})();

Advanced Scripting

Using GM APIs for Persistent Storage

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();
})();

Making HTTP Requests

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(); } })();

Advanced DOM Techniques

Handle dynamic content and complex page modifications:

Waiting for Elements

// 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);

Event Delegation

// 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';
    }
});

CSS Injection

// 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;
    }
`);

Real-World Example: Reddit Enhancer

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
        });
    });
})();

Performance & Best Practices

Write efficient, maintainable userscripts:

Efficient Element Selection

// ❌ 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
        }
    });
}

Memory Management

// ✅ 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);

Error Handling

// 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);
    }
}
Pro Tip: Use the browser's Developer Tools (F12) to inspect elements and test your selectors before writing your script.