DEV Community

Cover image for Building Reliable Content Scripts: Why XPath Beats querySelector in Chrome Extensions
Jay Malli
Jay Malli

Posted on

Building Reliable Content Scripts: Why XPath Beats querySelector in Chrome Extensions

Ever built a Chrome extension that worked perfectly... until the website updated their CSS classes? ๐Ÿคฆโ€โ™‚๏ธ

You're not alone. CSS selectors are fragile because they depend on styling decisions that change frequently. Let me show you a better approach using XPath.


๐Ÿšจ The Problem: CSS Selectors Are Built for Styling, Not Stability

Think of CSS selectors like following directions using landmarks: "Turn left at the blue house". What happens when someone paints the house red? Your directions break. ๐Ÿ ๐ŸŽจ

Here's a real example I've seen break in production:

// โŒ Fragile approach
const submitButton = document.querySelector('.btn.btn-primary.submit-action');
Enter fullscreen mode Exit fullscreen mode

Why this breaks:

  • ๐ŸŽจ Marketing team changes btn-primary to btn-brand
  • ๐Ÿ”ง Developer refactors CSS to use utility classes
  • ๐Ÿงช A/B test adds .experiment-variant-b class
  • ๐Ÿ’ฅ Your extension stops working

โœ… The Solution: XPath with Smart Heuristics

XPath is like having a GPS coordinate system for your DOM. Instead of relying on cosmetic classes, it uses stable identifiers that websites rarely change. ๐ŸŽฏ

// โœ… Stable approach
import { getXPathForNode, resolveXPath } from 'dom-xpath-toolkit';

// Generate stable XPath
const submitButton = document.querySelector('[data-testid="submit-form"]');
const stableXPath = getXPathForNode(submitButton);
// Result: //*[@data-testid="submit-form"]

// Later, reliably find it again
const button = resolveXPath('//*[@data-testid="submit-form"]');
button?.click();
Enter fullscreen mode Exit fullscreen mode

๐Ÿ› ๏ธ Real-World Example: Building a Form Auto-Filler

Let's build a content script that automatically fills login forms. I'll show both approaches so you can see the difference.

โŒ The Fragile CSS Way

// manifest.json
{
  "manifest_version": 3,
  "content_scripts": [{
    "matches": ["https://example.com/*"],
    "js": ["content.js"]
  }]
}

// content.js - CSS approach
function autofillLogin() {
  const emailField = document.querySelector('.input-field.email');
  const passwordField = document.querySelector('.form-control[type="password"]');
  const submitBtn = document.querySelector('.btn.submit');

  if (!emailField || !passwordField || !submitBtn) {
    console.error('Form elements not found!'); // This happens ALL THE TIME
    return;
  }

  emailField.value = 'user@example.com';
  passwordField.value = 'securepassword123';
  submitBtn.click();
}

autofillLogin();
Enter fullscreen mode Exit fullscreen mode

โš ๏ธ What breaks this:

  • CSS framework migration (Bootstrap โ†’ Tailwind)
  • Class name obfuscation in production builds
  • Component library updates
  • Dynamic class generation from CSS-in-JS

โœ… The Stable XPath Way

// content.js - XPath approach
import { 
  resolveXPath, 
  getXPathByAttribute,
  getXPathByLabel 
} from 'dom-xpath-toolkit';

function autofillLogin() {
  // Multiple fallback strategies!
  const emailField = 
    resolveXPath('//*[@name="email"]') ||
    resolveXPath('//*[@type="email"]') ||
    resolveXPath(getXPathByLabel('Email'));

  const passwordField = 
    resolveXPath('//*[@name="password"]') ||
    resolveXPath('//*[@autocomplete="current-password"]');

  const submitBtn = 
    resolveXPath('//*[@type="submit"]') ||
    resolveXPath(getXPathByAttribute('role', 'button', 'Submit'));

  if (!emailField || !passwordField || !submitBtn) {
    console.error('Form elements not found');
    return;
  }

  emailField.value = 'user@example.com';
  passwordField.value = 'securepassword123';
  submitBtn.click();
}

autofillLogin();
Enter fullscreen mode Exit fullscreen mode

๐Ÿ’ช Why this is better:

  • โœ… Targets semantic HTML attributes (name, type, autocomplete)
  • โœ… Falls back through multiple strategies
  • โœ… Works across framework rewrites
  • โœ… Survives minification and obfuscation

๐ŸŽฏ Stability Hierarchy: What to Target

The toolkit uses a smart priority system. Here's what it looks for in order:

import { getXPathForNode } from 'dom-xpath-toolkit';

// Priority 1: ID (most stable) ๐Ÿ”‘
// <button id="checkout-btn">Buy Now</button>
// Result: //*[@id="checkout-btn"]

// Priority 2: data-* attributes (test IDs) ๐Ÿงช
// <button data-testid="checkout">Buy Now</button>
// Result: //*[@data-testid="checkout"]

// Priority 3: Semantic attributes ๐Ÿ“‹
// <button name="submit" type="submit">Buy Now</button>
// Result: //button[@type="submit" and @name="submit"]

// Priority 4: ARIA attributes โ™ฟ
// <button aria-label="Checkout">Buy Now</button>
// Result: //*[@aria-label="Checkout"]

// Last resort: Structure ๐Ÿ—๏ธ
// <div><button>Buy Now</button></div>
// Result: //div/button[contains(text(),"Buy Now")]
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“ฆ Installation in Your Extension

Setting up the toolkit takes 2 minutes:

npm install dom-xpath-toolkit
Enter fullscreen mode Exit fullscreen mode

Then bundle it with your extension:

// content.js
import { getXPathForNode, resolveXPath } from 'dom-xpath-toolkit';

// Now you can use it anywhere in your content scripts!
Enter fullscreen mode Exit fullscreen mode

โš ๏ธ When NOT to Use XPath

XPath isn't always the answer. Stick with CSS selectors when:

  • โœ… You control the HTML (your own popup/options page)
  • โœ… You need :hover, ::before pseudo-selectors (XPath can't do these)
  • โœ… Performance on massive DOMs (CSS is slightly faster)

But for content scripts interacting with third-party sites? XPath wins every time. ๐Ÿ†


๐ŸŽฎ Try It Yourself

I've built a live playground where you can test XPath generation on real websites:

๐Ÿ‘‰ XPath Toolkit Playground

Features:

  • ๐Ÿงช Test different heuristic strategies
  • ๐Ÿ“Š See stability scores for each selector
  • ๐Ÿ“‹ Copy generated XPaths directly

๐Ÿ“š Quick Reference

GitHub: dom-xpath-toolkit

npm: dom-xpath-toolkit


๐Ÿค Let's Connect!

If you found this guide helpful, let's keep the conversation going! I regularly post deep dives, security tips, and new projects I'm working on.

Connect with me on LinkedInโ€”I'd love to hear about what you're building.

Find me on LinkedIn


Top comments (0)