Declarative Routing with importmap: A New Proposal for File-based Web Components

Introduction In modern frontend development, routing is typically handled by dedicated libraries like React Router or Vue Router, which map route paths to components. However, for lightweight frameworks or architectures based on Web Components and file-based structures, such traditional routing solutions can feel overly complex or redundant. This article proposes a new approach: using the browser-native importmap to define routing in a declarative and structural way, without writing additional routing code. The Problem with Traditional Routing In file-based Web Component architectures, the following problems often arise: Route definitions and component imports are separated, increasing maintenance cost. Path-to-component mappings are often hardcoded in JS files. Even small projects require installing and configuring a routing library. The Proposal: Use importmap as a Routing Table importmap is a standard feature for ES Modules that maps module specifiers to URLs. By leveraging this feature, we can map route paths directly to component file URLs. Example importmap: { "imports": { "routes:": { "/": "./components/home-page.js", "/users": "./components/user-list.js", "/products/:id": "./components/product-detail.js" }, "components:navbar": "./components/navbar.js" } } Using a "routes:" namespace makes it easy to structurally organize routing paths and component sources. Mapping Route Paths to Custom Elements defineComponents({ "home-page": "/", "user-list": "/users", "product-detail": "/products/:id", "navbar-component": "components:navbar" }); The defineComponents() function checks whether the path (e.g., "/users") exists in importmap.imports["routes:"], and if it does, uses the mapped URL to import and register the corresponding Web Component. This enables writing simple, declarative component mappings without explicitly referencing the "routes:" namespace. Routing Engine: Minimal Implementation Here's a basic example of how the router works internally: const routeMap = new Map(); const importMap = document.querySelector('script[type="importmap"]').textContent; const routes = JSON.parse(importMap).imports["routes:"] || {}; function defineComponents(mapping) { for (const [tag, path] of Object.entries(mapping)) { if (routes[path]) { routeMap.set(path, tag); } else { // Non-routing component (immediate import) import(path).then(mod => customElements.define(tag, mod.default)); } } initRouter(); } function initRouter() { window.addEventListener("popstate", () => { const currentPath = location.pathname; for (const [route, tag] of routeMap.entries()) { if (matchRoute(route, currentPath)) { loadComponent(tag, routes[route]); break; } } }); window.dispatchEvent(new Event("popstate")); } function loadComponent(tag, modulePath) { import(modulePath).then(mod => { if (!customElements.get(tag)) { customElements.define(tag, mod.default); } const mount = document.querySelector("main"); mount.innerHTML = ""; mount.appendChild(document.createElement(tag)); }); } function matchRoute(pattern, path) { return pattern === path; // basic exact match } This code: Records the route-to-tag mappings via defineComponents() Listens to popstate for browser navigation events Dynamically imports and mounts the matched Web Component Further enhancements could include support for dynamic parameters (:id), link click interception with pushState, and 404 fallback handling. Advantages Centralized route definitions using standard importmap No need for external routing libraries Readable, declarative component-to-path mappings Easily extensible with dynamic routes or localization Ideal for small SPAs, static sites, or custom Web Component-based frameworks Summary By using importmap as a routing table, we can eliminate the need for verbose router configurations and embrace a structural, declarative approach to Web Component routing. Combined with minimal runtime logic, this strategy enables lightweight SPAs and file-driven UI frameworks to scale cleanly. Whether you're building a static app, a custom element-based SPA, or experimenting with new architectures, this pattern offers a refreshing alternative to traditional routing libraries.

Apr 20, 2025 - 14:53
 0
Declarative Routing with importmap: A New Proposal for File-based Web Components

Introduction

In modern frontend development, routing is typically handled by dedicated libraries like React Router or Vue Router, which map route paths to components. However, for lightweight frameworks or architectures based on Web Components and file-based structures, such traditional routing solutions can feel overly complex or redundant.

This article proposes a new approach: using the browser-native importmap to define routing in a declarative and structural way, without writing additional routing code.

The Problem with Traditional Routing

In file-based Web Component architectures, the following problems often arise:

  • Route definitions and component imports are separated, increasing maintenance cost.
  • Path-to-component mappings are often hardcoded in JS files.
  • Even small projects require installing and configuring a routing library.

The Proposal: Use importmap as a Routing Table

importmap is a standard feature for ES Modules that maps module specifiers to URLs. By leveraging this feature, we can map route paths directly to component file URLs.

Example importmap:


Using a "routes:" namespace makes it easy to structurally organize routing paths and component sources.

Mapping Route Paths to Custom Elements

defineComponents({
  "home-page": "/",
  "user-list": "/users",
  "product-detail": "/products/:id",
  "navbar-component": "components:navbar"
});

The defineComponents() function checks whether the path (e.g., "/users") exists in importmap.imports["routes:"], and if it does, uses the mapped URL to import and register the corresponding Web Component.

This enables writing simple, declarative component mappings without explicitly referencing the "routes:" namespace.

Routing Engine: Minimal Implementation

Here's a basic example of how the router works internally:

const routeMap = new Map();
const importMap = document.querySelector('script[type="importmap"]').textContent;
const routes = JSON.parse(importMap).imports["routes:"] || {};

function defineComponents(mapping) {
  for (const [tag, path] of Object.entries(mapping)) {
    if (routes[path]) {
      routeMap.set(path, tag);
    } else {
      // Non-routing component (immediate import)
      import(path).then(mod => customElements.define(tag, mod.default));
    }
  }
  initRouter();
}

function initRouter() {
  window.addEventListener("popstate", () => {
    const currentPath = location.pathname;
    for (const [route, tag] of routeMap.entries()) {
      if (matchRoute(route, currentPath)) {
        loadComponent(tag, routes[route]);
        break;
      }
    }
  });
  window.dispatchEvent(new Event("popstate"));
}

function loadComponent(tag, modulePath) {
  import(modulePath).then(mod => {
    if (!customElements.get(tag)) {
      customElements.define(tag, mod.default);
    }
    const mount = document.querySelector("main");
    mount.innerHTML = "";
    mount.appendChild(document.createElement(tag));
  });
}

function matchRoute(pattern, path) {
  return pattern === path; // basic exact match
}

This code:

  • Records the route-to-tag mappings via defineComponents()
  • Listens to popstate for browser navigation events
  • Dynamically imports and mounts the matched Web Component

Further enhancements could include support for dynamic parameters (:id), link click interception with pushState, and 404 fallback handling.

Advantages

  • Centralized route definitions using standard importmap
  • No need for external routing libraries
  • Readable, declarative component-to-path mappings
  • Easily extensible with dynamic routes or localization
  • Ideal for small SPAs, static sites, or custom Web Component-based frameworks

Summary

By using importmap as a routing table, we can eliminate the need for verbose router configurations and embrace a structural, declarative approach to Web Component routing. Combined with minimal runtime logic, this strategy enables lightweight SPAs and file-driven UI frameworks to scale cleanly.

Whether you're building a static app, a custom element-based SPA, or experimenting with new architectures, this pattern offers a refreshing alternative to traditional routing libraries.