DEV Community

Cover image for Microfrontends Without Frameworks: A Simple, Vanilla JavaScript Approach
Vimal Maheedharan
Vimal Maheedharan

Posted on

Microfrontends Without Frameworks: A Simple, Vanilla JavaScript Approach

The question: "Can we build microfrontends using only vanilla JavaScript, HTML, and CSS (without frameworks like React/Vue or build tools like Webpack)?"

Short answer: Yes — absolutely!

We can build microfrontends with plain HTML/JS/CSS using simple, browser-native patterns: Web Components (Custom Elements + Shadow DOM), dynamic script/ES module loading, or iframes.

Below, I'll provide the complete, copy-pasteable approach for a host and two remotes using custom elements and a tiny global event bus. This solution is minimal, framework-free, and easy to reason about. Perfect as a short engineering exercise or a workshop: lightweight, independent deployables, and easy to reason about.

Architecture overview

  • Host: Loads and orchestrates microfrontends (iframes)
  • Products: Microfrontend that emits cart:add events
  • Cart: Microfrontend that receives events and displays cart items

📁 Directory structure

host/
  index.html
products/
  products.html
cart/
  cart.html
Enter fullscreen mode Exit fullscreen mode

Run each folder on a different local server:

Host → http://localhost:3000
Products → http://localhost:3001
Cart → http://localhost:3002

(You can use python -m http.server or VSCode Live Server.)

🖥️ Host app — host/index.html

<!doctype html>
<html>
<head>
  <meta charset="utf-8"/>
  <title>Host (iframes)</title>
  <style>
    nav{background:#222;color:#fff;padding:8px}
    #frameWrap{display:flex;gap:12px;padding:12px}
    iframe{width:48%;height:400px;border:1px solid #ccc;}
  </style>
</head>
<body>
  <nav>
    <button id="btnLoad">Load Microfrontends</button>
    <button id="btnSendAuth">Send Auth Token</button>
  </nav>

  <div id="frameWrap"></div>

  <script>
  const ALLOWED = {
    products: 'http://localhost:3001',
    cart: 'http://localhost:3002'
  };

  const frameWrap = document.getElementById('frameWrap');

  function createIframe(id, src, sandbox = 'allow-scripts allow-same-origin') {
    const f = document.createElement('iframe');
    f.id = id;
    f.src = src;
    f.sandbox = sandbox;
    f.loading = 'lazy';
    f.title = id;
    frameWrap.appendChild(f);
    return f;
  }

  // Handle messages from remotes
  window.addEventListener('message', (ev) => {
    const origin = ev.origin;
    const data = ev.data || {};
    if (!data.type) return;

    if (![ALLOWED.products, ALLOWED.cart].includes(origin)) {
      console.warn('Blocked message from unknown origin:', origin);
      return;
    }

    console.log('Host received:', data, 'from', origin);

    // Forward "cart:add" to Cart MFE
    if (data.type === 'cart:add') {
      const cartFrame = document.getElementById('cartFrame');
      if (cartFrame && cartFrame.contentWindow) {
        cartFrame.contentWindow.postMessage(data, ALLOWED.cart);
      }
    }

    // Handle auth requests
    if (data.type === 'auth:request') {
      const token = 'FAKE_TOKEN_12345';
      ev.source.postMessage({ type: 'auth:response', payload: { token } }, origin);
    }

    // Dynamic iframe resizing
    if (data.type === 'frame:resize' && ev.source === document.getElementById('productsFrame')?.contentWindow) {
      document.getElementById('productsFrame').style.height = data.payload + 'px';
    }
  });

  document.getElementById('btnLoad').onclick = () => {
    frameWrap.innerHTML = '';
    createIframe('productsFrame', ALLOWED.products + '/products.html');
    createIframe('cartFrame', ALLOWED.cart + '/cart.html');
  };

  document.getElementById('btnSendAuth').onclick = () => {
    const token = 'FAKE_TOKEN_12345';
    const prod = document.getElementById('productsFrame');
    const cart = document.getElementById('cartFrame');

    if (prod && prod.contentWindow) {
      prod.contentWindow.postMessage({ type:'auth:response', payload:{ token } }, ALLOWED.products);
    }
    if (cart && cart.contentWindow) {
      cart.contentWindow.postMessage({ type:'auth:response', payload:{ token } }, ALLOWED.cart);
    }
  };
  </script>
</body>
</html>

Enter fullscreen mode Exit fullscreen mode

📦 Products microfrontend — products/products.html

<!doctype html>
<html>
<head><meta charset="utf-8"/><title>Products</title></head>
<body>
  <h3>Products (iframe)</h3>
  <div id="list"></div>

  <script>
  const ORIGIN_PARENT = 'http://localhost:3000';
  const products = [
    {id:1,name:'Widget A',price:199},
    {id:2,name:'Widget B',price:299}
  ];

  function render(){
    document.getElementById('list').innerHTML = products.map(p => `
      <div style="display:flex;justify-content:space-between;padding:6px;border-bottom:1px solid #eee">
        <div>${p.name} — ₹${p.price}</div>
        <button data-id="${p.id}">Add</button>
      </div>`).join('');

    document.querySelectorAll("button").forEach(btn => {
      btn.onclick = () => {
        const id = +btn.dataset.id;
        const item = products.find(x => x.id === id);
        parent.postMessage({ type:'cart:add', payload:item }, ORIGIN_PARENT);
      };
    });
  }
  render();

  // Request auth
  parent.postMessage({ type:'auth:request' }, ORIGIN_PARENT);

  // Listen for messages
  window.addEventListener('message', (ev) => {
    if (ev.origin !== ORIGIN_PARENT) return;
    if (ev.data.type === 'auth:response') {
      console.log('Products received token:', ev.data.payload.token);
    }
  });

  // Ask host to adjust height
  setTimeout(() => {
    parent.postMessage({ type:'frame:resize', payload: document.body.scrollHeight }, ORIGIN_PARENT);
  }, 200);
  </script>
</body>
</html>

Enter fullscreen mode Exit fullscreen mode

🛒 Cart microfrontend — cart/cart.html

<!doctype html>
<html>
<head><meta charset="utf-8"/><title>Cart</title></head>
<body>
  <h3>Cart (iframe)</h3>
  <div id="items">No items</div>

  <script>
  const ORIGIN_PARENT = 'http://localhost:3000';
  const items = [];

  function render(){
    document.getElementById('items').innerHTML = items.length
      ? items.map(it => `<div>${it.name} — ₹${it.price}</div>`).join('')
      : 'No items';
  }
  render();

  window.addEventListener('message', (ev) => {
    if (ev.origin !== ORIGIN_PARENT) return;
    const msg = ev.data;

    if (msg.type === 'cart:add') {
      items.push(msg.payload);
      render();
    }

    if (msg.type === 'auth:response') {
      console.log('Cart received token:', msg.payload.token);
    }
  });

  // Request token at startup
  parent.postMessage({ type:'auth:request' }, ORIGIN_PARENT);
  </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

How to test locally

cd host && python -m http.server 3000
cd products && python -m http.server 3001
cd cart && python -m http.server 3002
Enter fullscreen mode Exit fullscreen mode
  • Visit: http://localhost:3000
  • Action: Click “Load Microfrontends”
  • Action: Click “Add” in Products — items should appear in Cart
  • Observe: Browser console logs for auth:request, auth:response, cart:add, etc.

🔐 Security and production notes

  • Origin checks: Verify ev.origin for every message handler. Never trust postMessage() without origin checks.
  • Iframe sandboxing: Use sandbox and grant minimal permissions (allow-scripts, allow-same-origin only if needed).
  • Token handling: Prefer short-lived tokens via postMessage (not URLs).
  • Transport: Serve over HTTPS in production.
  • CSP: Apply strict Content Security Policy headers on remote pages.

Acceptance criteria

  • Host orchestration: Host loads two independent microfrontends in iframes.
  • Event flow: Products iframe sends cart:add events; host forwards them to Cart iframe.
  • UI: Cart displays added items correctly.
  • Security: Messages are origin-checked and tokens are exchanged via postMessage.
  • Isolation: Iframes can be swapped/updated independently.

Jai Chinjo

Top comments (0)