DEV Community

Cover image for Building a Chrome Extension That Compares UI Elements Across Pages
Gustavo A. Roberto
Gustavo A. Roberto

Posted on AI-assisted

Building a Chrome Extension That Compares UI Elements Across Pages

Chrome DevTools offers powerful capabilities for inspecting web interfaces.

But when reviewing visual consistency across a product, I often find that the challenge isn't inspecting one element. It's comparing several elements across different screens.

Consider a checkout flow with buttons appearing on different pages. You might want to check whether their typography, padding, colors, and borders are consistent.

Individually inspecting each button and manually comparing its CSS properties becomes repetitive.

I wanted a lightweight tool that could capture those properties, preserve them across navigation, and display the differences in one place.

That idea became Visqora, a free Chrome extension built with React, TypeScript, WXT, and Manifest V3.

What started as a small inspection tool turned into an interesting exercise in browser architecture, asynchronous communication, state management, privacy, and production reliability.

01. Defining the problem

The original goal was straightforward:

Make it easier to compare the computed visual properties of DOM elements, even when those elements belong to different pages.

There was also an important boundary to establish.

Visqora isn't a screenshot regression testing tool.

It doesn't compare images, evaluate visual baselines, or automatically determine whether differences represent defects.

Instead, it compares selected computed CSS properties from DOM elements.

The distinction matters because a different value isn't necessarily an error. Two components may intentionally use different spacing or colors.

Visqora exposes the differences and leaves their interpretation to the developer, QA professional, or designer.

For the initial release, I limited the comparison workspace to five elements.

That constraint kept both the interface and the underlying data model manageable.

02. Architecture across execution contexts

A Chrome extension is not a conventional single-context React application.

Manifest V3 separates functionality across execution environments with different lifecycles and permissions.

Visqora uses three principal contexts:

React Side Panel: The interface where users start inspection, view captured properties, and compare elements.

Background service worker: The orchestration layer responsible for runtime messages, active-tab state, inspection sessions, workspace mutations, and persistence.

Injected inspector: An isolated-world script responsible for interacting with the inspected webpage and capturing computed styles.

These contexts communicate through explicit messages and shared TypeScript contracts.

Architecture overview

flowchart TB
    U["User"]

    subgraph UI["React Side Panel"]
        P["InspectorPanel"]
        C["Pure comparison engine"]
    end

    subgraph BG["MV3 Background Service Worker"]
        W["Message orchestration"]
        L["Inspection lifecycle"]
        WS["Workspace management"]
    end

    subgraph PAGE["Inspected webpage"]
        I["On-demand injected inspector"]
        D["Page DOM and computed styles"]
    end

    S[("chrome.storage.session")]

    U --> P
    P <-->|"Runtime messages"| W
    W --> L
    W --> WS
    W <-->|"Commands and captures"| I
    I <-->|"DOM inspection"| D
    WS <-->|"Persist and restore"| S
    P --> C
    C --> P

Figure 1. Visqora's execution contexts and their main responsibilities.

The diagram illustrates a deliberate separation of concerns.

The Side Panel does not directly inspect the webpage or mutate the authoritative workspace.

The injected inspector doesn't manage the React interface or long-lived comparison data.

The background service worker coordinates the operation and owns workspace mutations.

That separation introduces messaging overhead, but it also gives each component a clearer responsibility.

03. On-demand injection and minimal access

One decision I wanted to preserve was avoiding unnecessary access to webpages.

Visqora doesn't inject a persistent inspector into every page the user visits.

Instead, inspection starts through an explicit user action.

When the user clicks Inspect, the Side Panel requests permission for the current HTTP or HTTPS origin.

The background worker then attempts to communicate with an existing inspector.

If no inspector is available, it uses chrome.scripting.executeScript() to inject the inspector and retries the activation command.

The inspector runs in Chrome's isolated world.

It creates the hover overlay and registers the event listeners needed to identify and capture elements.

When inspection stops, it cleans up the overlay and listeners.

This approach reduces unnecessary interaction with webpages, but it also creates additional lifecycle and orchestration complexity.

The background worker must account for injection failures, navigation, stale requests, and repeated activation.

There is a clear trade-off:

Less persistent page access means more responsibility for correct activation and cleanup.

04. Capturing snapshots instead of DOM elements

Once inspection is active, Visqora uses pointer events to identify the element under the cursor.

The overlay highlights the current target.

When the user selects an element, the inspector creates a serializable snapshot.

The implementation uses getComputedStyle() to read a specific allowlist of 18 CSS properties.

It also captures a generated identifier, the element's tag name, and a bounded structural selector.

A simplified conceptual flow:

DOM element
    ↓
getComputedStyle()
    ↓
Extract 18 supported properties
    ↓
Normalize values
    ↓
Create serializable capture
    ↓
Send capture to background worker
    ↓
Add trusted source metadata
    ↓
Store snapshot in comparison workspace
Enter fullscreen mode Exit fullscreen mode

The important decision is that Visqora stores a snapshot, not a live DOM reference.

A DOM element belongs to a particular document and can disappear after navigation.

A serializable snapshot can remain useful after that document no longer exists.

The background worker adds source metadata such as the originating tab, URL, document identity when available, and capture timestamp.

This also establishes a privacy boundary.

The snapshot extractor does not copy arbitrary HTML, text content, form values, or DOM references into the comparison workspace.

The limitation is equally important: snapshots represent point-in-time values. They don't automatically update when the original element changes.

Single-element inspection showing grouped CSS properties.
Figure 2. Single-element inspection showing grouped CSS properties.

05. A pure comparison engine

The comparison engine is deliberately independent of browser APIs.

It receives captured snapshots and produces an ordered set of property comparisons.

For each supported property, it compares the stored values and marks the property as different when values do not match.

Whitespace is normalized, but the engine doesn't attempt deeper semantic equivalence.

For example, it doesn't claim that all differently formatted CSS color values or length units are necessarily equivalent.

This is intentional.

The comparison engine identifies differences in the recorded data. It doesn't attempt to become a full CSS interpretation engine.

The Side Panel chooses its presentation based on the number of snapshots:

  • With one snapshot, it displays a grouped property inspector.
  • With two to five snapshots, it displays a comparison table.

Because the engine is pure, the comparison rules can be tested independently of Chrome APIs, DOM access, and React rendering.

That separation made the feature easier to maintain and reason about.

Comparison between two or more elements, highlighting property differences.
Figure 3. Comparison between two or more elements, highlighting property differences.

06. State management across pages and tabs

Cross-page comparison introduces a lifecycle problem.

The webpage being inspected can navigate, reload, or disappear.

The Side Panel can also close.

And under Manifest V3, the background service worker should not be treated as a permanently running process.

The solution was to distinguish two kinds of state.

Active inspection state

Inspection is tied to the current browser context.

The worker tracks tab, session, operation, and document identity where available.

When navigation or a tab switch invalidates the current inspection, the active interaction is stopped or reset.

Visqora doesn't silently reactivate inspection on the new page.

The user must explicitly start it again.

Comparison workspace

Captured snapshots are different.

They remain useful even when inspection ends.

Visqora preserves them through chrome.storage.session, which allows the workspace to survive navigation and Side Panel reopening within the supported browser session.

The background worker is the single owner of workspace mutations.

To prevent concurrent storage operations from overwriting one another, mutations are serialized through a promise queue.

Stored snapshots are also validated when restored rather than blindly trusted.

This architecture preserves the user's comparison work without treating the current webpage as permanent.

07. Handling stale messages

One subtle challenge in browser extensions is asynchronous timing.

Imagine a user starting inspection and then navigating before the operation finishes.

An injection result, element capture, or state update may arrive after the original document has been replaced.

Without additional checks, the application could associate stale data with the current session.

Visqora addresses this by validating the identities associated with operations and captures.

The background worker checks relevant tab, session, operation, and document information before accepting events.

Document IDs aren't guaranteed to be available for every operation, so the system uses the identities that are available.

This is an important distinction between simply sending messages and designing a reliable message protocol.

A response is not necessarily valid just because it arrived successfully. It must also belong to the operation that is still current.

08. A production failure in the first public release

The first Chrome Web Store release exposed another messaging issue.

After Visqora v0.4.0 was approved, I installed the published extension and opened the Side Panel.

The interface was completely blank.

The console reported an error equivalent to:

Cannot destructure property 'inspection'
as it is undefined.
Enter fullscreen mode Exit fullscreen mode

Initially, this suggested a React initialization issue.

But the problem originated in communication with the background worker.

On startup, the Side Panel requested its initial state using GET_PANEL_STATE.

The published worker used an asynchronous chrome.runtime.onMessage listener that returned a Promise.

In the affected runtime, that approach didn't keep the response channel open as the implementation expected.

The Side Panel received undefined instead of the expected state, and the following React render crashed.

Correcting the runtime response contract

The corrected implementation uses Chrome's callback-based messaging contract.

For asynchronous work, the listener returns true synchronously and later responds through sendResponse.

Conceptually:

chrome.runtime.onMessage.addListener(
  (message, sender, sendResponse) => {
    if (message.type !== 'GET_PANEL_STATE') {
      return false;
    }

    getPanelState()
      .then(sendResponse)
      .catch((error) => {
        sendResponse({
          error: String(error)
        });
      });

    return true;
  }
);
Enter fullscreen mode Exit fullscreen mode

Illustrative code showing the asynchronous response-channel contract, not a direct extract from the repository.

The hotfix also strengthened response validation in the Side Panel.

Instead of assuming every message contains a valid state object, the interface now checks incoming responses and renders an explicit error when they are invalid.

Workspace restoration also handles empty, malformed, or unavailable session storage.

These improvements address the original failure while making unexpected communication problems easier to diagnose.

09. The real surprise: artifact drift

The investigation uncovered an additional problem.

The published v0.4.0 Side Panel bundle was byte-for-byte identical to the corresponding local bundle.

The background worker was not.

The callback-style response correction had already entered the repository after the Store artifact was created, inside an unrelated change.

As a result, the current local code behaved correctly while the published package still contained the faulty implementation.

The problem wasn't minification or an unexpected React production optimization.

We had been testing code that was different from the code users actually installed.

That changed the focus of the release process.

Instead of validating only the repository's current state, we needed to validate the exact production artifact being distributed.

10. Testing and releasing v0.4.1

The v0.4.1 hotfix concentrated on reliability.

We added regression coverage for:

  • Initial Side Panel rendering.
  • Missing and malformed worker responses.
  • Runtime response-channel behavior.
  • Empty and restored comparison workspaces.
  • Session-storage failures.
  • Asynchronous response success and failure.

The release passed 60 automated tests across eight files, along with linting, formatting, type checking, and production build verification.

We also tested the generated production extension in Chrome.

The final interactive checks were particularly important because headless testing couldn't fully reproduce every user permission interaction.

After testing, the corrected v0.4.1 package was submitted to the Chrome Web Store.

The published update was installed and verified successfully.

The lesson was broader than fixing a message listener.

The actual installation artifact is part of the software we need to test.

11. What I learned

A small browser extension can involve surprisingly rich architectural decisions.

Building Visqora reinforced several principles I value in frontend engineering.

Treat execution contexts as explicit boundaries

The React UI, service worker, and injected page code have different responsibilities and lifecycles.

Communication between them needs contracts, validation, and failure handling.

Keep a single owner for mutable state

Centralizing workspace mutations in the background worker simplifies consistency, even though it means state changes must cross a messaging boundary.

Separate live interactions from historical data

Inspection belongs to a specific document and session.

Captured snapshots can remain useful after that context disappears.

Keeping the two separate enables cross-page comparisons without pretending DOM elements live forever.

Design for privacy through data minimization

An inspector doesn't need to capture everything it can access.

Extracting a bounded set of properties reduces unnecessary data collection and clarifies the feature's purpose.

Test failures at integration boundaries

A successful unit test can't prove that Chrome messaging, extension permissions, and packaged assets behave correctly together.

The release artifact and the browser runtime must also be part of verification.

Keep product scope focused

Visqora isn't trying to replace DevTools, screenshot regression platforms, or design-system auditing tools.

It focuses on one problem: helping people inspect and compare implementation details efficiently.

12. What's next?

Visqora is still an early-stage tool.

For now, I'm focused on understanding how frontend developers, QA engineers, and designers use it in real workflows.

The extension is free and doesn't require an account.

Future improvements will depend on real feedback rather than adding features simply to expand the product.

Building Visqora has been a useful exercise in browser APIs, TypeScript contracts, state ownership, DOM inspection, asynchronous communication, and software distribution.

And if there's one lesson I'll carry into future releases, it's this:

The product is not the source code sitting in the repository. It's the software that users actually run.


Explore Visqora

Chrome Web Store: https://chromewebstore.google.com/detail/visqora/djjlhjglgjjaeafhekmejaeobfmhpngh

Product website: https://visqora.55onze.com

55onze: https://55onze.com

Gustavo A Roberto · Frontend Engineering, Architecture & Technical Leadership

Top comments (0)