<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Gustavo A. Roberto</title>
    <description>The latest articles on DEV Community by Gustavo A. Roberto (@gustavoaroberto).</description>
    <link>https://dev.to/gustavoaroberto</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1064660%2F8f5e8e5a-586f-4c04-abcb-975306d5988a.png</url>
      <title>DEV Community: Gustavo A. Roberto</title>
      <link>https://dev.to/gustavoaroberto</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gustavoaroberto"/>
    <language>en</language>
    <item>
      <title>Building a Chrome Extension That Compares UI Elements Across Pages</title>
      <dc:creator>Gustavo A. Roberto</dc:creator>
      <pubDate>Sat, 24 Oct 2026 02:11:57 +0000</pubDate>
      <link>https://dev.to/gustavoaroberto/building-a-chrome-extension-that-compares-ui-elements-across-pages-3479</link>
      <guid>https://dev.to/gustavoaroberto/building-a-chrome-extension-that-compares-ui-elements-across-pages-3479</guid>
      <description>&lt;p&gt;Chrome DevTools offers powerful capabilities for inspecting web interfaces.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

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

&lt;p&gt;Individually inspecting each button and manually comparing its CSS properties becomes repetitive.&lt;/p&gt;

&lt;p&gt;I wanted a lightweight tool that could capture those properties, preserve them across navigation, and display the differences in one place.&lt;/p&gt;

&lt;p&gt;That idea became &lt;strong&gt;Visqora&lt;/strong&gt;, a free Chrome extension built with React, TypeScript, WXT, and Manifest V3.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  01. Defining the problem
&lt;/h2&gt;

&lt;p&gt;The original goal was straightforward:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make it easier to compare the computed visual properties of DOM elements, even when those elements belong to different pages.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There was also an important boundary to establish.&lt;/p&gt;

&lt;p&gt;Visqora isn't a screenshot regression testing tool.&lt;/p&gt;

&lt;p&gt;It doesn't compare images, evaluate visual baselines, or automatically determine whether differences represent defects.&lt;/p&gt;

&lt;p&gt;Instead, it compares selected computed CSS properties from DOM elements.&lt;/p&gt;

&lt;p&gt;The distinction matters because a different value isn't necessarily an error. Two components may intentionally use different spacing or colors.&lt;/p&gt;

&lt;p&gt;Visqora exposes the differences and leaves their interpretation to the developer, QA professional, or designer.&lt;/p&gt;

&lt;p&gt;For the initial release, I limited the comparison workspace to five elements.&lt;/p&gt;

&lt;p&gt;That constraint kept both the interface and the underlying data model manageable.&lt;/p&gt;

&lt;h2&gt;
  
  
  02. Architecture across execution contexts
&lt;/h2&gt;

&lt;p&gt;A Chrome extension is not a conventional single-context React application.&lt;/p&gt;

&lt;p&gt;Manifest V3 separates functionality across execution environments with different lifecycles and permissions.&lt;/p&gt;

&lt;p&gt;Visqora uses three principal contexts:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;React Side Panel:&lt;/strong&gt; The interface where users start inspection, view captured properties, and compare elements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Background service worker:&lt;/strong&gt; The orchestration layer responsible for runtime messages, active-tab state, inspection sessions, workspace mutations, and persistence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Injected inspector:&lt;/strong&gt; An isolated-world script responsible for interacting with the inspected webpage and capturing computed styles.&lt;/p&gt;

&lt;p&gt;These contexts communicate through explicit messages and shared TypeScript contracts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture overview
&lt;/h3&gt;



&lt;pre data-lang="mermaid"&gt;&lt;code&gt;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 --&amp;gt; P
    P &amp;lt;--&amp;gt;|"Runtime messages"| W
    W --&amp;gt; L
    W --&amp;gt; WS
    W &amp;lt;--&amp;gt;|"Commands and captures"| I
    I &amp;lt;--&amp;gt;|"DOM inspection"| D
    WS &amp;lt;--&amp;gt;|"Persist and restore"| S
    P --&amp;gt; C
    C --&amp;gt; P&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;Figure 1. Visqora's execution contexts and their main responsibilities.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The diagram illustrates a deliberate separation of concerns.&lt;/p&gt;

&lt;p&gt;The Side Panel does not directly inspect the webpage or mutate the authoritative workspace.&lt;/p&gt;

&lt;p&gt;The injected inspector doesn't manage the React interface or long-lived comparison data.&lt;/p&gt;

&lt;p&gt;The background service worker coordinates the operation and owns workspace mutations.&lt;/p&gt;

&lt;p&gt;That separation introduces messaging overhead, but it also gives each component a clearer responsibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  03. On-demand injection and minimal access
&lt;/h2&gt;

&lt;p&gt;One decision I wanted to preserve was avoiding unnecessary access to webpages.&lt;/p&gt;

&lt;p&gt;Visqora doesn't inject a persistent inspector into every page the user visits.&lt;/p&gt;

&lt;p&gt;Instead, inspection starts through an explicit user action.&lt;/p&gt;

&lt;p&gt;When the user clicks &lt;strong&gt;Inspect&lt;/strong&gt;, the Side Panel requests permission for the current HTTP or HTTPS origin.&lt;/p&gt;

&lt;p&gt;The background worker then attempts to communicate with an existing inspector.&lt;/p&gt;

&lt;p&gt;If no inspector is available, it uses &lt;code&gt;chrome.scripting.executeScript()&lt;/code&gt; to inject the inspector and retries the activation command.&lt;/p&gt;

&lt;p&gt;The inspector runs in Chrome's isolated world.&lt;/p&gt;

&lt;p&gt;It creates the hover overlay and registers the event listeners needed to identify and capture elements.&lt;/p&gt;

&lt;p&gt;When inspection stops, it cleans up the overlay and listeners.&lt;/p&gt;

&lt;p&gt;This approach reduces unnecessary interaction with webpages, but it also creates additional lifecycle and orchestration complexity.&lt;/p&gt;

&lt;p&gt;The background worker must account for injection failures, navigation, stale requests, and repeated activation.&lt;/p&gt;

&lt;p&gt;There is a clear trade-off:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Less persistent page access means more responsibility for correct activation and cleanup.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  04. Capturing snapshots instead of DOM elements
&lt;/h2&gt;

&lt;p&gt;Once inspection is active, Visqora uses pointer events to identify the element under the cursor.&lt;/p&gt;

&lt;p&gt;The overlay highlights the current target.&lt;/p&gt;

&lt;p&gt;When the user selects an element, the inspector creates a serializable snapshot.&lt;/p&gt;

&lt;p&gt;The implementation uses &lt;code&gt;getComputedStyle()&lt;/code&gt; to read a specific allowlist of 18 CSS properties.&lt;/p&gt;

&lt;p&gt;It also captures a generated identifier, the element's tag name, and a bounded structural selector.&lt;/p&gt;

&lt;p&gt;A simplified conceptual flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important decision is that &lt;strong&gt;Visqora stores a snapshot, not a live DOM reference&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A DOM element belongs to a particular document and can disappear after navigation.&lt;/p&gt;

&lt;p&gt;A serializable snapshot can remain useful after that document no longer exists.&lt;/p&gt;

&lt;p&gt;The background worker adds source metadata such as the originating tab, URL, document identity when available, and capture timestamp.&lt;/p&gt;

&lt;p&gt;This also establishes a privacy boundary.&lt;/p&gt;

&lt;p&gt;The snapshot extractor does not copy arbitrary HTML, text content, form values, or DOM references into the comparison workspace.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fydksafwsd6d62zegzmoe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fydksafwsd6d62zegzmoe.png" alt="Single-element inspection showing grouped CSS properties." width="800" height="641"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 2. Single-element inspection showing grouped CSS properties.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  05. A pure comparison engine
&lt;/h2&gt;

&lt;p&gt;The comparison engine is deliberately independent of browser APIs.&lt;/p&gt;

&lt;p&gt;It receives captured snapshots and produces an ordered set of property comparisons.&lt;/p&gt;

&lt;p&gt;For each supported property, it compares the stored values and marks the property as different when values do not match.&lt;/p&gt;

&lt;p&gt;Whitespace is normalized, but the engine doesn't attempt deeper semantic equivalence.&lt;/p&gt;

&lt;p&gt;For example, it doesn't claim that all differently formatted CSS color values or length units are necessarily equivalent.&lt;/p&gt;

&lt;p&gt;This is intentional.&lt;/p&gt;

&lt;p&gt;The comparison engine identifies differences in the recorded data. It doesn't attempt to become a full CSS interpretation engine.&lt;/p&gt;

&lt;p&gt;The Side Panel chooses its presentation based on the number of snapshots:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;With one snapshot, it displays a grouped property inspector.&lt;/li&gt;
&lt;li&gt;With two to five snapshots, it displays a comparison table.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because the engine is pure, the comparison rules can be tested independently of Chrome APIs, DOM access, and React rendering.&lt;/p&gt;

&lt;p&gt;That separation made the feature easier to maintain and reason about.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpovaiydu0723ognqybzo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpovaiydu0723ognqybzo.png" alt="Comparison between two or more elements, highlighting property differences." width="800" height="641"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 3. Comparison between two or more elements, highlighting property differences.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  06. State management across pages and tabs
&lt;/h2&gt;

&lt;p&gt;Cross-page comparison introduces a lifecycle problem.&lt;/p&gt;

&lt;p&gt;The webpage being inspected can navigate, reload, or disappear.&lt;/p&gt;

&lt;p&gt;The Side Panel can also close.&lt;/p&gt;

&lt;p&gt;And under Manifest V3, the background service worker should not be treated as a permanently running process.&lt;/p&gt;

&lt;p&gt;The solution was to distinguish two kinds of state.&lt;/p&gt;
&lt;h3&gt;
  
  
  Active inspection state
&lt;/h3&gt;

&lt;p&gt;Inspection is tied to the current browser context.&lt;/p&gt;

&lt;p&gt;The worker tracks tab, session, operation, and document identity where available.&lt;/p&gt;

&lt;p&gt;When navigation or a tab switch invalidates the current inspection, the active interaction is stopped or reset.&lt;/p&gt;

&lt;p&gt;Visqora doesn't silently reactivate inspection on the new page.&lt;/p&gt;

&lt;p&gt;The user must explicitly start it again.&lt;/p&gt;
&lt;h3&gt;
  
  
  Comparison workspace
&lt;/h3&gt;

&lt;p&gt;Captured snapshots are different.&lt;/p&gt;

&lt;p&gt;They remain useful even when inspection ends.&lt;/p&gt;

&lt;p&gt;Visqora preserves them through &lt;code&gt;chrome.storage.session&lt;/code&gt;, which allows the workspace to survive navigation and Side Panel reopening within the supported browser session.&lt;/p&gt;

&lt;p&gt;The background worker is the single owner of workspace mutations.&lt;/p&gt;

&lt;p&gt;To prevent concurrent storage operations from overwriting one another, mutations are serialized through a promise queue.&lt;/p&gt;

&lt;p&gt;Stored snapshots are also validated when restored rather than blindly trusted.&lt;/p&gt;

&lt;p&gt;This architecture preserves the user's comparison work without treating the current webpage as permanent.&lt;/p&gt;
&lt;h2&gt;
  
  
  07. Handling stale messages
&lt;/h2&gt;

&lt;p&gt;One subtle challenge in browser extensions is asynchronous timing.&lt;/p&gt;

&lt;p&gt;Imagine a user starting inspection and then navigating before the operation finishes.&lt;/p&gt;

&lt;p&gt;An injection result, element capture, or state update may arrive after the original document has been replaced.&lt;/p&gt;

&lt;p&gt;Without additional checks, the application could associate stale data with the current session.&lt;/p&gt;

&lt;p&gt;Visqora addresses this by validating the identities associated with operations and captures.&lt;/p&gt;

&lt;p&gt;The background worker checks relevant tab, session, operation, and document information before accepting events.&lt;/p&gt;

&lt;p&gt;Document IDs aren't guaranteed to be available for every operation, so the system uses the identities that are available.&lt;/p&gt;

&lt;p&gt;This is an important distinction between simply sending messages and designing a reliable message protocol.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A response is not necessarily valid just because it arrived successfully. It must also belong to the operation that is still current.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  08. A production failure in the first public release
&lt;/h2&gt;

&lt;p&gt;The first Chrome Web Store release exposed another messaging issue.&lt;/p&gt;

&lt;p&gt;After Visqora v0.4.0 was approved, I installed the published extension and opened the Side Panel.&lt;/p&gt;

&lt;p&gt;The interface was completely blank.&lt;/p&gt;

&lt;p&gt;The console reported an error equivalent to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cannot destructure property 'inspection'
as it is undefined.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Initially, this suggested a React initialization issue.&lt;/p&gt;

&lt;p&gt;But the problem originated in communication with the background worker.&lt;/p&gt;

&lt;p&gt;On startup, the Side Panel requested its initial state using &lt;code&gt;GET_PANEL_STATE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The published worker used an asynchronous &lt;code&gt;chrome.runtime.onMessage&lt;/code&gt; listener that returned a Promise.&lt;/p&gt;

&lt;p&gt;In the affected runtime, that approach didn't keep the response channel open as the implementation expected.&lt;/p&gt;

&lt;p&gt;The Side Panel received &lt;code&gt;undefined&lt;/code&gt; instead of the expected state, and the following React render crashed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Correcting the runtime response contract
&lt;/h3&gt;

&lt;p&gt;The corrected implementation uses Chrome's callback-based messaging contract.&lt;/p&gt;

&lt;p&gt;For asynchronous work, the listener returns &lt;code&gt;true&lt;/code&gt; synchronously and later responds through &lt;code&gt;sendResponse&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sendResponse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET_PANEL_STATE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;getPanelState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sendResponse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;sendResponse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Illustrative code showing the asynchronous response-channel contract, not a direct extract from the repository.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The hotfix also strengthened response validation in the Side Panel.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Workspace restoration also handles empty, malformed, or unavailable session storage.&lt;/p&gt;

&lt;p&gt;These improvements address the original failure while making unexpected communication problems easier to diagnose.&lt;/p&gt;

&lt;h2&gt;
  
  
  09. The real surprise: artifact drift
&lt;/h2&gt;

&lt;p&gt;The investigation uncovered an additional problem.&lt;/p&gt;

&lt;p&gt;The published v0.4.0 Side Panel bundle was byte-for-byte identical to the corresponding local bundle.&lt;/p&gt;

&lt;p&gt;The background worker was not.&lt;/p&gt;

&lt;p&gt;The callback-style response correction had already entered the repository after the Store artifact was created, inside an unrelated change.&lt;/p&gt;

&lt;p&gt;As a result, the current local code behaved correctly while the published package still contained the faulty implementation.&lt;/p&gt;

&lt;p&gt;The problem wasn't minification or an unexpected React production optimization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We had been testing code that was different from the code users actually installed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That changed the focus of the release process.&lt;/p&gt;

&lt;p&gt;Instead of validating only the repository's current state, we needed to validate the exact production artifact being distributed.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Testing and releasing v0.4.1
&lt;/h2&gt;

&lt;p&gt;The v0.4.1 hotfix concentrated on reliability.&lt;/p&gt;

&lt;p&gt;We added regression coverage for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Initial Side Panel rendering.&lt;/li&gt;
&lt;li&gt;Missing and malformed worker responses.&lt;/li&gt;
&lt;li&gt;Runtime response-channel behavior.&lt;/li&gt;
&lt;li&gt;Empty and restored comparison workspaces.&lt;/li&gt;
&lt;li&gt;Session-storage failures.&lt;/li&gt;
&lt;li&gt;Asynchronous response success and failure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The release passed 60 automated tests across eight files, along with linting, formatting, type checking, and production build verification.&lt;/p&gt;

&lt;p&gt;We also tested the generated production extension in Chrome.&lt;/p&gt;

&lt;p&gt;The final interactive checks were particularly important because headless testing couldn't fully reproduce every user permission interaction.&lt;/p&gt;

&lt;p&gt;After testing, the corrected v0.4.1 package was submitted to the Chrome Web Store.&lt;/p&gt;

&lt;p&gt;The published update was installed and verified successfully.&lt;/p&gt;

&lt;p&gt;The lesson was broader than fixing a message listener.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The actual installation artifact is part of the software we need to test.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  11. What I learned
&lt;/h2&gt;

&lt;p&gt;A small browser extension can involve surprisingly rich architectural decisions.&lt;/p&gt;

&lt;p&gt;Building Visqora reinforced several principles I value in frontend engineering.&lt;/p&gt;

&lt;h3&gt;
  
  
  Treat execution contexts as explicit boundaries
&lt;/h3&gt;

&lt;p&gt;The React UI, service worker, and injected page code have different responsibilities and lifecycles.&lt;/p&gt;

&lt;p&gt;Communication between them needs contracts, validation, and failure handling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep a single owner for mutable state
&lt;/h3&gt;

&lt;p&gt;Centralizing workspace mutations in the background worker simplifies consistency, even though it means state changes must cross a messaging boundary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Separate live interactions from historical data
&lt;/h3&gt;

&lt;p&gt;Inspection belongs to a specific document and session.&lt;/p&gt;

&lt;p&gt;Captured snapshots can remain useful after that context disappears.&lt;/p&gt;

&lt;p&gt;Keeping the two separate enables cross-page comparisons without pretending DOM elements live forever.&lt;/p&gt;

&lt;h3&gt;
  
  
  Design for privacy through data minimization
&lt;/h3&gt;

&lt;p&gt;An inspector doesn't need to capture everything it can access.&lt;/p&gt;

&lt;p&gt;Extracting a bounded set of properties reduces unnecessary data collection and clarifies the feature's purpose.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test failures at integration boundaries
&lt;/h3&gt;

&lt;p&gt;A successful unit test can't prove that Chrome messaging, extension permissions, and packaged assets behave correctly together.&lt;/p&gt;

&lt;p&gt;The release artifact and the browser runtime must also be part of verification.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep product scope focused
&lt;/h3&gt;

&lt;p&gt;Visqora isn't trying to replace DevTools, screenshot regression platforms, or design-system auditing tools.&lt;/p&gt;

&lt;p&gt;It focuses on one problem: helping people inspect and compare implementation details efficiently.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. What's next?
&lt;/h2&gt;

&lt;p&gt;Visqora is still an early-stage tool.&lt;/p&gt;

&lt;p&gt;For now, I'm focused on understanding how frontend developers, QA engineers, and designers use it in real workflows.&lt;/p&gt;

&lt;p&gt;The extension is free and doesn't require an account.&lt;/p&gt;

&lt;p&gt;Future improvements will depend on real feedback rather than adding features simply to expand the product.&lt;/p&gt;

&lt;p&gt;Building Visqora has been a useful exercise in browser APIs, TypeScript contracts, state ownership, DOM inspection, asynchronous communication, and software distribution.&lt;/p&gt;

&lt;p&gt;And if there's one lesson I'll carry into future releases, it's this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The product is not the source code sitting in the repository. It's the software that users actually run.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Explore Visqora
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Chrome Web Store:&lt;/strong&gt; &lt;a href="https://chromewebstore.google.com/detail/visqora/djjlhjglgjjaeafhekmejaeobfmhpngh" rel="noopener noreferrer"&gt;https://chromewebstore.google.com/detail/visqora/djjlhjglgjjaeafhekmejaeobfmhpngh&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Product website:&lt;/strong&gt; &lt;a href="https://visqora.55onze.com" rel="noopener noreferrer"&gt;https://visqora.55onze.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;55onze:&lt;/strong&gt; &lt;a href="https://55onze.com" rel="noopener noreferrer"&gt;https://55onze.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Gustavo A Roberto · Frontend Engineering, Architecture &amp;amp; Technical Leadership&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>architecture</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>From Server-Rendered Forms to a Framework-Agnostic JavaScript Library</title>
      <dc:creator>Gustavo A. Roberto</dc:creator>
      <pubDate>Thu, 22 Oct 2026 22:19:52 +0000</pubDate>
      <link>https://dev.to/gustavoaroberto/from-server-rendered-forms-to-a-framework-agnostic-javascript-library-1g4j</link>
      <guid>https://dev.to/gustavoaroberto/from-server-rendered-forms-to-a-framework-agnostic-javascript-library-1g4j</guid>
      <description>&lt;p&gt;A few years ago, while working on a Java and Thymeleaf application, I faced an interesting frontend challenge.&lt;/p&gt;

&lt;p&gt;One of its workflows involved collecting notary-related information through a form that spanned multiple pages, with different validation requirements along the way.&lt;/p&gt;

&lt;p&gt;The challenge wasn't creating inputs or rendering HTML. It was coordinating the entire experience while preserving user progress between separate documents.&lt;/p&gt;

&lt;p&gt;That problem eventually inspired &lt;strong&gt;Demetria&lt;/strong&gt;, a JavaScript library I recently revisited and rebuilt as an open-source project.&lt;/p&gt;

&lt;p&gt;In this article, I want to share the problem that inspired it, the architecture behind the current implementation, and some of the trade-offs involved in building a framework-agnostic library.&lt;/p&gt;

&lt;h2&gt;
  
  
  01. The problem with multi-page forms
&lt;/h2&gt;

&lt;p&gt;When working with modern frontend frameworks, it's easy to take client-side state management for granted.&lt;/p&gt;

&lt;p&gt;But in a traditional server-rendered application, navigating between pages can mean loading an entirely new HTML document.&lt;/p&gt;

&lt;p&gt;Any state stored exclusively in JavaScript memory disappears.&lt;/p&gt;

&lt;p&gt;For a complex form, this creates several challenges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Preserving previously entered values.&lt;/li&gt;
&lt;li&gt;Tracking progress through multiple steps.&lt;/li&gt;
&lt;li&gt;Enforcing validation before moving forward.&lt;/li&gt;
&lt;li&gt;Keeping form behavior consistent between pages.&lt;/li&gt;
&lt;li&gt;Avoiding duplication of navigation and state-management logic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the original application, I used &lt;code&gt;localStorage&lt;/code&gt; to maintain form state between pages.&lt;/p&gt;

&lt;p&gt;That solved the immediate problem, but it also raised a broader architectural question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Could we describe an entire multi-step form as data and let a reusable library handle its rendering, state, validation, and navigation?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That was the idea behind Demetria.&lt;/p&gt;

&lt;p&gt;The name itself came from Demeter, the Greek goddess associated with agriculture and seasonal cycles. I liked the analogy between the progression of seasons and the different stages of a form.&lt;/p&gt;

&lt;h2&gt;
  
  
  02. Rebuilding an old idea
&lt;/h2&gt;

&lt;p&gt;Years later, I returned to Demetria with more experience in frontend architecture and library design.&lt;/p&gt;

&lt;p&gt;I didn't want to build another comprehensive form management framework.&lt;/p&gt;

&lt;p&gt;Libraries such as React Hook Form already solve many problems extremely well, but my goal was different.&lt;/p&gt;

&lt;p&gt;I wanted something that could work in a modern application or a traditional server-rendered system without requiring either one to adopt a particular frontend framework.&lt;/p&gt;

&lt;p&gt;The initial requirements were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use plain JavaScript or TypeScript.&lt;/li&gt;
&lt;li&gt;Accept JSON-compatible form definitions.&lt;/li&gt;
&lt;li&gt;Generate native HTML elements.&lt;/li&gt;
&lt;li&gt;Support multiple steps and built-in validation.&lt;/li&gt;
&lt;li&gt;Allow optional persistence across HTML documents.&lt;/li&gt;
&lt;li&gt;Leave visual styling to the consuming application.&lt;/li&gt;
&lt;li&gt;Keep runtime dependencies at zero.&lt;/li&gt;
&lt;li&gt;Treat accessibility as a core design concern.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These requirements shaped the boundaries of the library.&lt;/p&gt;

&lt;p&gt;Rather than building a large ecosystem, I wanted a small tool with a clearly defined responsibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  03. Architecture: separating responsibilities
&lt;/h2&gt;

&lt;p&gt;One of the most important decisions was keeping form orchestration separate from presentation.&lt;/p&gt;

&lt;p&gt;The current architecture can be understood through three main responsibilities: the form engine, the DOM renderer, and optional persistence.&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture diagram
&lt;/h3&gt;



&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TB
    A["Host application"]
    B["JSON-compatible schema"]
    C["createForm(options)"]
    D["Form engine"]
    E["Native DOM renderer"]
    F["HTML form"]
    G["User interaction"]
    H["Optional Web Storage"]
    I["Host CSS and integration"]

    A --&amp;gt; C
    B --&amp;gt; C
    C --&amp;gt; D
    D --&amp;gt; E
    E --&amp;gt; F
    F --&amp;gt; G
    G --&amp;gt; D
    D &amp;lt;--&amp;gt; H
    A --&amp;gt; I
    I --&amp;gt; F&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;em&gt;Figure 1. A conceptual overview of Demetria's architecture and integration boundaries.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The form engine
&lt;/h3&gt;

&lt;p&gt;The engine coordinates form state, validation, and navigation.&lt;/p&gt;

&lt;p&gt;It understands the schema, current step, values, and transition rules without depending on the application's visual presentation.&lt;/p&gt;

&lt;p&gt;This separation allows the same form behavior to be used in different integration environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  The DOM renderer
&lt;/h3&gt;

&lt;p&gt;The renderer transforms the schema into native HTML elements, including &lt;code&gt;form&lt;/code&gt;, &lt;code&gt;fieldset&lt;/code&gt;, &lt;code&gt;legend&lt;/code&gt;, &lt;code&gt;label&lt;/code&gt;, &lt;code&gt;input&lt;/code&gt;, &lt;code&gt;select&lt;/code&gt;, &lt;code&gt;textarea&lt;/code&gt;, and &lt;code&gt;button&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It also connects user interactions to the form engine.&lt;/p&gt;

&lt;p&gt;Using native browser elements reduces the need for custom behavior and makes it easier to preserve familiar keyboard interactions and semantics.&lt;/p&gt;

&lt;p&gt;Demetria deliberately ships without mandatory CSS.&lt;/p&gt;

&lt;p&gt;Applications can use the generated styling hooks to apply their own visual identity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Optional persistence
&lt;/h3&gt;

&lt;p&gt;The third responsibility is state persistence.&lt;/p&gt;

&lt;p&gt;By default, Demetria keeps its state in memory.&lt;/p&gt;

&lt;p&gt;When an application needs to preserve data between page loads, it can explicitly enable &lt;code&gt;sessionStorage&lt;/code&gt; or &lt;code&gt;localStorage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is particularly important for the problem that originally inspired the project.&lt;/p&gt;

&lt;p&gt;The library doesn't need to become a router or manage the server-side application lifecycle. It only needs to restore compatible form state when the application initializes it again.&lt;/p&gt;

&lt;p&gt;That boundary keeps the implementation focused.&lt;/p&gt;

&lt;h2&gt;
  
  
  04. A declarative API
&lt;/h2&gt;

&lt;p&gt;The public API revolves around &lt;code&gt;createForm()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Instead of manually creating fields and coordinating individual inputs, the consuming application provides a JSON-compatible schema describing the form.&lt;/p&gt;

&lt;p&gt;Here is a working example based on Demetria's v0.1.1 API.&lt;/p&gt;

&lt;p&gt;First, create a target element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"demetria-form"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"./app.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then define the form in JavaScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createForm&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;demetria&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;registration&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;personal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Personal information&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fullName&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Full name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preferences&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Preferences&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;newsletter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkbox&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Receive updates&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;contactMethod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;select&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Preferred contact method&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Phone&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;phone&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createForm&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#demetria-form&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;onComplete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Completed form:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Demetria generates the form, handles transitions between steps, validates fields, and passes the completed values to the application.&lt;/p&gt;

&lt;p&gt;Notice that &lt;code&gt;email&lt;/code&gt; doesn't include &lt;code&gt;required: true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In Demetria, &lt;strong&gt;fields are optional by default&lt;/strong&gt;. An optional email can be left empty, but a non-empty value must satisfy the applicable email validation.&lt;/p&gt;

&lt;p&gt;This distinction is important because optional does not mean unvalidated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why a JSON-compatible schema?
&lt;/h3&gt;

&lt;p&gt;Choosing a declarative schema creates a clear contract between configuration and behavior.&lt;/p&gt;

&lt;p&gt;It allows a form definition to be serialized, shared, and understood without executing arbitrary JavaScript from the schema.&lt;/p&gt;

&lt;p&gt;But that choice has trade-offs.&lt;/p&gt;

&lt;p&gt;A declarative format can be less expressive than an API built around callbacks and custom components.&lt;/p&gt;

&lt;p&gt;For the initial release, I preferred a smaller, predictable contract over supporting every possible form requirement.&lt;/p&gt;

&lt;p&gt;The objective was to make the common behavior reliable before introducing greater extensibility.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzfg6r4ygys9ek8vn566i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzfg6r4ygys9ek8vn566i.png" alt="Single Page Form Example" width="799" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  05. One library, two navigation models
&lt;/h2&gt;

&lt;p&gt;A particularly important architectural requirement was supporting both single-page and multi-page flows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Single-page forms
&lt;/h3&gt;

&lt;p&gt;In a single-page flow, all steps exist within the same document.&lt;/p&gt;

&lt;p&gt;Demetria updates the active step without requiring a full browser navigation.&lt;/p&gt;

&lt;p&gt;The engine preserves values as users move forward and backward.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-page forms
&lt;/h3&gt;

&lt;p&gt;In a multi-page flow, the browser loads separate HTML documents.&lt;/p&gt;

&lt;p&gt;The host application controls navigation, while Demetria restores compatible form state through explicitly configured persistence.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createForm&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#demetria-form&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;persistence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sessionStorage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;registration-flow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;stepId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preferences&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;stepId&lt;/code&gt; option selects the step that the current document should render.&lt;/p&gt;

&lt;p&gt;Different pages can initialize Demetria with the same schema and persistence key while explicitly selecting their corresponding step.&lt;/p&gt;

&lt;p&gt;This approach avoids introducing a client-side router.&lt;/p&gt;

&lt;p&gt;It also makes the library useful in traditional applications where navigation is controlled by the server or the surrounding application.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy19k9jns1vcxzflzgd31.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy19k9jns1vcxzflzgd31.jpeg" alt="Multi page form example" width="800" height="525"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The persistence trade-off
&lt;/h3&gt;

&lt;p&gt;Browser storage is convenient, but it comes with limitations.&lt;/p&gt;

&lt;p&gt;Both &lt;code&gt;localStorage&lt;/code&gt; and &lt;code&gt;sessionStorage&lt;/code&gt; are accessible to JavaScript executing in the same origin.&lt;/p&gt;

&lt;p&gt;They should not automatically be treated as safe places for sensitive personal information.&lt;/p&gt;

&lt;p&gt;For that reason, Demetria doesn't persist form state by default.&lt;/p&gt;

&lt;p&gt;Applications must explicitly enable it and remain responsible for evaluating the security and privacy requirements of their data.&lt;/p&gt;

&lt;p&gt;In applications collecting sensitive information, server-side persistence with appropriate security controls may be more suitable.&lt;/p&gt;

&lt;h2&gt;
  
  
  06. Accessibility is more than semantic HTML
&lt;/h2&gt;

&lt;p&gt;Accessibility was one of the areas I wanted to approach differently when rebuilding Demetria.&lt;/p&gt;

&lt;p&gt;A form-generation library has an important responsibility: the markup and behavior it produces become part of the user experience in every consuming application.&lt;/p&gt;

&lt;p&gt;That means accessibility problems can propagate across integrations.&lt;/p&gt;

&lt;p&gt;The renderer therefore emphasizes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Native semantic controls.&lt;/li&gt;
&lt;li&gt;Programmatically associated labels.&lt;/li&gt;
&lt;li&gt;Visible required-field indicators.&lt;/li&gt;
&lt;li&gt;Validation errors associated with their fields.&lt;/li&gt;
&lt;li&gt;Keyboard-operable navigation.&lt;/li&gt;
&lt;li&gt;Focus management after validation failures.&lt;/li&gt;
&lt;li&gt;Predictable focus when changing steps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But semantic HTML alone isn't enough.&lt;/p&gt;

&lt;p&gt;Focus and state transitions are particularly important in multi-step experiences.&lt;/p&gt;

&lt;h3&gt;
  
  
  A real issue discovered during testing
&lt;/h3&gt;

&lt;p&gt;During the accessibility audit for v0.1.1, we found that completing a form could cause keyboard focus to be lost.&lt;/p&gt;

&lt;p&gt;The renderer unnecessarily rebuilt the form after completion.&lt;/p&gt;

&lt;p&gt;That removed the focused button from the document, leaving focus on the page body.&lt;/p&gt;

&lt;p&gt;Instead of adding a workaround that manually restored focus, we removed the unnecessary renderer rebuild.&lt;/p&gt;

&lt;p&gt;This preserved the focused element and corrected the underlying lifecycle behavior.&lt;/p&gt;

&lt;p&gt;It was a useful reminder that accessibility issues can reveal deeper implementation problems.&lt;/p&gt;

&lt;p&gt;The same audit identified improvements related to accessible naming, responsive reflow, contrast, and ARIA announcements.&lt;/p&gt;

&lt;p&gt;Automated browser tests and axe-core checks helped verify the fixes, but I don't consider those results proof of complete WCAG 2.2 AA conformance.&lt;/p&gt;

&lt;p&gt;Manual assistive-technology testing remains important.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpx0n83ksz2uhyvydtgew.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpx0n83ksz2uhyvydtgew.png" alt="Form Error Example" width="687" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  07. Zero runtime dependencies as a design constraint
&lt;/h2&gt;

&lt;p&gt;Another deliberate choice was keeping Demetria free of runtime dependencies.&lt;/p&gt;

&lt;p&gt;The browser already provides most of the capabilities required for this problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Native DOM APIs.&lt;/li&gt;
&lt;li&gt;HTML form controls.&lt;/li&gt;
&lt;li&gt;Events.&lt;/li&gt;
&lt;li&gt;Web Storage.&lt;/li&gt;
&lt;li&gt;JavaScript data structures.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adding a large runtime dependency tree would introduce additional compatibility considerations and maintenance costs.&lt;/p&gt;

&lt;p&gt;That doesn't make dependencies inherently undesirable.&lt;/p&gt;

&lt;p&gt;It simply means they should solve a problem that justifies their cost.&lt;/p&gt;

&lt;p&gt;Demetria still uses development tools for TypeScript compilation, testing, linting, and accessibility verification.&lt;/p&gt;

&lt;p&gt;The goal is to keep the library lightweight for the applications that consume it.&lt;/p&gt;

&lt;h2&gt;
  
  
  08. Testing the actual distribution
&lt;/h2&gt;

&lt;p&gt;One lesson I wanted to apply during the rebuild was that successfully compiling source code isn't enough to prove that a library can be consumed.&lt;/p&gt;

&lt;p&gt;The initial historical npm release had packaging problems.&lt;/p&gt;

&lt;p&gt;For the new implementation, I wanted to verify the actual package artifact, not only the TypeScript sources.&lt;/p&gt;

&lt;p&gt;That meant checking the compiled JavaScript, type declarations, package exports, and tarball contents.&lt;/p&gt;

&lt;p&gt;We also added unit tests and browser-based checks for the generated forms and documentation examples.&lt;/p&gt;

&lt;p&gt;The first functional release, v0.1.0, established the library's core behavior.&lt;/p&gt;

&lt;p&gt;The next patch, v0.1.1, focused on accessibility and stability improvements without introducing breaking API changes.&lt;/p&gt;

&lt;p&gt;I prefer that sequence to rushing into new features before the existing functionality is reliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  09. What I learned
&lt;/h2&gt;

&lt;p&gt;Rebuilding Demetria reinforced a few engineering principles that have become important in my work.&lt;/p&gt;

&lt;h3&gt;
  
  
  A good abstraction knows what it doesn't own
&lt;/h3&gt;

&lt;p&gt;Demetria handles form orchestration, but it doesn't need to become a frontend framework, router, validation ecosystem, or form-building platform.&lt;/p&gt;

&lt;p&gt;Keeping those responsibilities separate makes the library easier to reason about.&lt;/p&gt;

&lt;h3&gt;
  
  
  Framework independence should solve a real problem
&lt;/h3&gt;

&lt;p&gt;Being framework-agnostic isn't automatically valuable.&lt;/p&gt;

&lt;p&gt;In this case, it supports a concrete integration requirement: generating form experiences in applications that may not use a modern frontend framework.&lt;/p&gt;

&lt;h3&gt;
  
  
  Accessibility is part of behavior
&lt;/h3&gt;

&lt;p&gt;Labels and semantic elements matter, but accessibility also depends on navigation, focus, validation, announcements, and lifecycle behavior.&lt;/p&gt;

&lt;p&gt;It needs to influence architecture and testing, not just markup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Declarative APIs create valuable boundaries
&lt;/h3&gt;

&lt;p&gt;A JSON-compatible schema makes the form structure explicit.&lt;/p&gt;

&lt;p&gt;That improves portability and predictability while establishing clear limitations around extensibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  Small, functional releases build confidence
&lt;/h3&gt;

&lt;p&gt;A library doesn't need dozens of features to be useful.&lt;/p&gt;

&lt;p&gt;A focused implementation, working examples, accurate documentation, and a reliable distribution process are a much better starting point.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. What's next?
&lt;/h2&gt;

&lt;p&gt;Demetria is still an early-stage open-source project.&lt;/p&gt;

&lt;p&gt;Future improvements may include conditional fields, custom validation rules, and more flexible renderer customization.&lt;/p&gt;

&lt;p&gt;But I don't want to expand the API simply to increase the number of features.&lt;/p&gt;

&lt;p&gt;The next changes should come from actual integration needs and feedback from developers using the library.&lt;/p&gt;

&lt;p&gt;For now, the main goal is to make the existing functionality understandable, accessible, and reliable.&lt;/p&gt;

&lt;p&gt;Demetria started with a complex form in a server-rendered Java application.&lt;/p&gt;

&lt;p&gt;Years later, the same idea became an opportunity to revisit library architecture, declarative APIs, browser persistence, and accessibility.&lt;/p&gt;

&lt;p&gt;And that's probably my favorite part of the project.&lt;/p&gt;

&lt;p&gt;Sometimes, revisiting an old engineering problem with a different perspective is enough to turn a specific solution into something reusable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Explore Demetria
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/gustavoaroberto/demetria" rel="noopener noreferrer"&gt;https://github.com/gustavoaroberto/demetria&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation and live examples:&lt;/strong&gt; &lt;a href="https://gustavoaroberto.github.io/demetria/" rel="noopener noreferrer"&gt;https://gustavoaroberto.github.io/demetria/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;npm:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/demetria" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/demetria&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Gustavo A Roberto · Frontend Engineering, Architecture &amp;amp; Technical Leadership&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
