Click Without Triggering: Engineering a Zero-Interference Element Selector

Learn how to build a professional DOM element inspector for Chrome extensions. Step-by-step guide to create non-intrusive element selection, prevent default behaviors, and implement smooth highlighting. Includes production-ready code examples.
Learn how to build a professional DOM element inspector for Chrome extensions. Step-by-step guide to create non-intrusive element selection, prevent default behaviors, and implement smooth highlighting. Includes production-ready code examples.

Yesterday I had a chat with one of BrowserUse's co-founders, Magnus Müller, and as I was showing him how our Element Selector extension he had questions around how we prevent default actions on anchors and buttons during element selection. After the call, I thought this would make a good Engineering blog post.

So today, I'll share how we built our element inspector that allows users to select and analyze DOM elements without triggering their default behaviors.

The Challenge

When building a DOM element inspector, one of the primary challenges is handling interactive elements like links and buttons. The goal is to:

  • Allow users to inspect these elements

  • Prevent navigation or form submissions

  • Maintain visual feedback

  • Keep the original page structure intact

Solution Architecture

We'll explore three different approaches, each with its own use cases and trade-offs.

1. The Event Prevention Approach

This is the foundational layer of our solution.

Code Block
(JavaScript)
1
class ElementInspector {
2
constructor() {
3
this.isActive = false;
4
this.boundPreventDefault = this.preventDefaultBehavior.bind(this);
5
}
6
preventDefaultBehavior(event) {
7
if (this.isActive) {
8
event.preventDefault();
9
event.stopPropagation();
10
this.handleElementSelection(event.target);
11
}
12
}
13
enable() {
14
this.isActive = true;
15
const events = ['click', 'mousedown', 'mouseup', 'submit'];
16
events.forEach(event => {
17
document.addEventListener(event, this.boundPreventDefault, true);
18
});
19
}
20
disable() {
21
this.isActive = false;
22
const events = ['click', 'mousedown', 'mouseup', 'submit'];
23
events.forEach(event => {
24
document.removeEventListener(event, this.boundPreventDefault, true);
25
});
26
}
27
}

Let's break down our base ElementInspector class - the foundation of our non-intrusive DOM selection tool.

Code Block
(JavaScript)
1
class ElementInspector {
2
constructor() {
3
this.isActive = false;
4
this.boundPreventDefault = this.preventDefaultBehavior.bind(this);
5
}
6
// ...
7
}

The constructor sets up two critical properties:

  • isActive: A boolean flag that tracks whether the inspector is currently enabled

  • boundPreventDefault: A properly bound event handler that maintains the correct this context Binding the event handler in the constructor is an important performance optimization. Without it, we'd need to create a new function binding every time we attach an event listener, which would be inefficient and potentially create memory leaks.

Code Block
(JavaScript)
1
preventDefaultBehavior(event) {
2
if (this.isActive) {
3
event.preventDefault();
4
event.stopPropagation();
5
this.handleElementSelection(event.target);
6
}
7
}

This method is our event handler, deployed as a strategic interceptor for user interactions. It performs three critical actions:

  1. event.preventDefault(): Cancels the default behavior of the element (like navigation for links or form submission for buttons)

  2. event.stopPropagation(): Prevents the event from bubbling up through the DOM, ensuring no parent elements receive the event

  3. this.handleElementSelection(event.target): Invokes our element selection logic with the target DOM node

The if (this.isActive) check provides a failsafe mechanism - even if our event listeners are somehow still attached, they won't take action unless the inspector is active.

Code Block
(JavaScript)
1
enable() {
2
this.isActive = true;
3
const events = ['click', 'mousedown', 'mouseup', 'submit'];
4
events.forEach(event => {
5
document.addEventListener(event, this.boundPreventDefault, true);
6
});
7
}

The enable() method activates our inspector by:

  1. Setting isActive to true

  2. Attaching our event handler to multiple event types

  3. Using the event capturing phase (true as the third parameter)

The event capturing phase is critical here. Most JavaScript events go through three phases:

  • Capturing phase (down the DOM tree)

  • Target phase (at the target element)

  • Bubbling phase (back up the DOM tree)

By using the capturing phase, we intercept events before they reach their target elements, allowing us to prevent default behaviors before they're triggered.

We listen for multiple event types to ensure comprehensive coverage:

  • click: Captures most interactive element behaviors

  • mousedown/mouseup: Catches drag-based interactions and some custom widgets

  • submit: Specifically targets form submissions that might not trigger click events

Code Block
(JavaScript)
1
disable() {
2
this.isActive = false;
3
const events = ['click', 'mousedown', 'mouseup', 'submit'];
4
events.forEach(event => {
5
document.removeEventListener(event, this.boundPreventDefault, true);
6
});
7
}

2. The Overlay Approach

This is our primary solution at Samelogic, offering the best balance of performance and user experience.

Code Block
(JavaScript)
1
class VisualElementInspector extends ElementInspector {
2
constructor() {
3
super();
4
this.overlay = null;
5
this.highlighter = null;
6
}
7
createOverlay() {
8
const overlay = document.createElement('div');
9
overlay.id = 'samelogic-inspector-overlay';
10
overlay.style.cssText = `
11
position: fixed;
12
top: 0;
13
left: 0;
14
width: 100%;
15
height: 100%;
16
z-index: 2147483647;
17
pointer-events: none;
18
`;
19
20
const highlighter = document.createElement('div');
21
highlighter.id = 'samelogic-element-highlighter';
22
highlighter.style.cssText = `
23
position: absolute;
24
background: rgba(130, 71, 229, 0.2);
25
border: 2px solid rgb(130, 71, 229);
26
pointer-events: none;
27
transition: all 0.2s ease;
28
z-index: 2147483647;
29
`;
30
31
overlay.appendChild(highlighter);
32
document.body.appendChild(overlay);
33
34
this.overlay = overlay;
35
this.highlighter = highlighter;
36
}
37
handleMouseMove = (event) => {
38
if (!this.isActive) return;
39
40
const target = document.elementFromPoint(event.clientX, event.clientY);
41
if (!target) return;
42
43
this.updateHighlighter(target);
44
}
45
updateHighlighter(element) {
46
const rect = element.getBoundingClientRect();
47
Object.assign(this.highlighter.style, {
48
top: `${rect.top}px`,
49
left: `${rect.left}px`,
50
width: `${rect.width}px`,
51
height: `${rect.height}px`
52
});
53
}
54
enable() {
55
super.enable();
56
this.createOverlay();
57
this.overlay.style.pointerEvents = 'auto';
58
document.addEventListener('mousemove', this.handleMouseMove);
59
}
60
disable() {
61
super.disable();
62
document.removeEventListener('mousemove', this.handleMouseMove);
63
this.overlay?.remove();
64
this.overlay = null;
65
this.highlighter = null;
66
}
67
}

3. The Iframe Approach (For Complex Cases)

For handling edge cases and complex single-page applications:

Code Block
(JavaScript)
1
class IframeElementInspector extends VisualElementInspector {
2
createIframeOverlay() {
3
const iframe = document.createElement('iframe');
4
iframe.id = 'samelogic-inspector-frame';
5
iframe.style.cssText = `
6
position: fixed;
7
top: 0;
8
left: 0;
9
width: 100%;
10
height: 100%;
11
border: none;
12
z-index: 2147483647;
13
background: transparent;
14
`;
15
16
document.body.appendChild(iframe);
17
18
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
19
iframeDoc.body.style.cssText = 'margin: 0; pointer-events: none;';
20
21
return iframe;
22
}
23
}

Best Practices and Lessons Learned

1. Performance Optimization

To ensure smooth performance, especially during mousemove events:

Code Block
(JavaScript)
1
class OptimizedElementInspector extends VisualElementInspector {
2
constructor() {
3
super();
4
this.throttledMouseMove = this.throttle(this.handleMouseMove, 16); // ~60fps
5
}
6
throttle(func, limit) {
7
let inThrottle;
8
return function(...args) {
9
if (!inThrottle) {
10
func.apply(this, args);
11
inThrottle = true;
12
setTimeout(() => inThrottle = false, limit);
13
}
14
}
15
}
16
enable() {
17
super.enable();
18
document.addEventListener('mousemove', this.throttledMouseMove);
19
}
20
disable() {
21
super.disable();
22
document.removeEventListener('mousemove', this.throttledMouseMove);
23
}
24
}

2. Accessibility Considerations

Make sure your inspector is keyboard-accessible:

Code Block
(JavaScript)
1
class AccessibleElementInspector extends OptimizedElementInspector {
2
constructor() {
3
super();
4
this.currentFocusIndex = -1;
5
this.selectableElements = [];
6
}
7
handleKeyboard = (event) => {
8
if (!this.isActive) return;
9
10
switch(event.key) {
11
case 'Tab':
12
event.preventDefault();
13
this.navigateElements(event.shiftKey ? -1 : 1);
14
break;
15
case 'Enter':
16
event.preventDefault();
17
this.handleElementSelection(this.selectableElements[this.currentFocusIndex]);
18
break;
19
}
20
}
21
enable() {
22
super.enable();
23
this.selectableElements = Array.from(document.querySelectorAll('*'));
24
document.addEventListener('keydown', this.handleKeyboard);
25
}
26
}

3. Memory Management

Proper cleanup is crucial for extensions:

Code Block
(JavaScript)
1
class ManagedElementInspector extends AccessibleElementInspector {
2
destroy() {
3
this.disable();
4
this.selectableElements = [];
5
this.currentFocusIndex = -1;
6
// Clear any references that might cause memory leaks
7
this.boundPreventDefault = null;
8
this.throttledMouseMove = null;
9
}
10
}

Integration Example

Here's how to put it all together:

Code Block
(JavaScript)
1
// content-script.js
2
const inspector = new ManagedElementInspector();
3
// Listen for messages from the extension popup
4
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
5
switch (request.action) {
6
case 'START_INSPECTION':
7
inspector.enable();
8
break;
9
case 'STOP_INSPECTION':
10
inspector.disable();
11
break;
12
case 'CLEANUP':
13
inspector.destroy();
14
break;
15
}
16
});

In closing, building a robust element inspector requires careful consideration of various factors:

  • Event handling and prevention

  • Visual feedback

  • Performance optimization

  • Accessibility

  • Memory management

At Samelogic, we've found that the combination of event prevention and overlay approach provides the best user experience while maintaining performance. The iframe approach serves as a reliable fallback for complex cases.Remember to:

  • Always provide visual feedback for user actions

  • Implement proper cleanup mechanisms

  • Consider accessibility from the start

  • Optimize for performance, especially for mousemove events

  • Handle edge cases gracefully

Try this in our Chrome Extension yourself: 👉 Install our Element Inspector


Understand customer intent in minutes, not months

15-minute setup. Instant insights. The fastest way to decode what your users really want.
Used by teams at
Company logo 0Company logo 1Company logo 2Company logo 3Company logo 4Company logo 5Company logo 6Company logo 7Company logo 8Company logo 9Company logo 10Company logo 11Company logo 12Company logo 13Company logo 14Company logo 15Company logo 16Company logo 17Company logo 18Company logo 19Company logo 20Company logo 21Company logo 22Company logo 23Company logo 24Company logo 25Company logo 26Company logo 27Company logo 0Company logo 1Company logo 2Company logo 3Company logo 4Company logo 5Company logo 6Company logo 7Company logo 8Company logo 9Company logo 10Company logo 11Company logo 12Company logo 13Company logo 14Company logo 15Company logo 16Company logo 17Company logo 18Company logo 19Company logo 20Company logo 21Company logo 22Company logo 23Company logo 24Company logo 25Company logo 26Company logo 27