HTML5 Please

How LinkedIn Chrome Extensions Are Built: A Tour of the Web Platform APIs Behind the Sidebar

Open LinkedIn with an extension like Ln2CRM installed and something new appears on the page: a sidebar that reads the profile in front of you, checks it against your CRM, and saves it in one click. None of that is magic. It is built entirely from web platform APIs that any developer can use. This is a tour of the pieces that make a LinkedIn extension work, with Ln2CRM as the running example.

Content scripts: getting your code onto the page

A browser extension cannot touch a page until it injects a content script. The manifest declares which pages to run on and what to load:

{
  "content_scripts": [
    {
      "matches": ["https://www.linkedin.com/*"],
      "js": ["content.js"],
      "run_at": "document_idle"
    }
  ]
}

The script runs in an isolated world: it shares the page DOM but not the page JavaScript, so your variables never collide with LinkedIn's. From there you can read the profile and add your own interface.

Watching a single page application with MutationObserver

LinkedIn is a single page application. When a user clicks from one profile to the next there is no full page load, so a naive script that runs once at startup would only ever see the first profile. Extensions solve this by watching the DOM for changes with a MutationObserver, and by listening to History API navigation:

const observer = new MutationObserver(() => {
  const name = document.querySelector('h1');
  if (name) onProfileChanged(name.textContent);
});
observer.observe(document.body, { childList: true, subtree: true });

That is how the sidebar knows to refresh the moment you land on a new person, with no reload.

Isolating the UI with Shadow DOM

Injecting your own panel into someone else's page is a styling nightmare: their CSS leaks into your buttons and yours leaks into their layout. The fix is Shadow DOM. Attaching a shadow root gives your sidebar its own encapsulated tree that outside styles cannot reach into:

const host = document.createElement('div');
document.body.appendChild(host);
const root = host.attachShadow({ mode: 'open' });
root.innerHTML = '<style>/* only affects the sidebar */</style><aside>...</aside>';

This is the same Web Components technology you would use to ship a reusable widget, applied here to keep the extension and the host page from stepping on each other.

Talking to the CRM: fetch and CORS

The interesting work happens when the sidebar sends data to a CRM. That is a plain fetch call to the CRM's REST API:

const res = await fetch('https://api.pipedrive.com/v1/persons', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: name, org_id: org })
});

Two details matter. First, CORS governs which origins an API will answer, so extensions usually route these requests through a background service worker that holds broader host permissions than the page. Second, every request carries an auth token, which brings us to storage.

State and messaging: storage and message passing

An extension is split across two worlds: the content script on the page and a background service worker. They cannot share variables, so they pass messages instead, the same mental model as postMessage between windows:

chrome.runtime.sendMessage({ type: 'saveContact', payload: contact });

Tokens, settings and cached lookups live in extension storage, the durable cousin of localStorage, so the user stays signed in and the sidebar remembers its state between visits.

Building the sidebar: DOM APIs

Everything the user sees is assembled with ordinary DOM methods: createElement, append, and event listeners. There is no framework requirement. Many extensions ship a small amount of vanilla JavaScript precisely because it loads fast and stays out of the host page's way.

Putting it together: Ln2CRM as a worked example

Ln2CRM is a clean example of all of this in one tool. It injects a sidebar into LinkedIn with a content script, tracks which profile you are on with a MutationObserver, renders its interface inside a Shadow DOM so LinkedIn's styles never break it, and syncs contacts to Pipedrive, HubSpot, Zoho or Salesforce through fetch calls to each CRM's API, with auth held in extension storage. It also works on regular LinkedIn rather than requiring Sales Navigator, which is simply a matter of how its content script matches pages. Reading a polished extension like this is one of the fastest ways to learn the pattern.

Tips if you build your own

  • Debounce your MutationObserver callback. LinkedIn fires a lot of mutations, and you do not want to render again on every single one.
  • Prefer stable selectors and expect them to change anyway. Sites redesign, and a good extension ships an update quickly when they do.
  • Do the network work in the background worker, not the content script, to keep CORS handling and secrets off the page.
  • Wrap every piece of your interface in a Shadow DOM so an unrelated site update cannot restyle your panel.

The takeaway is that a modern browser extension is just the web platform used well. Content scripts, MutationObserver, Shadow DOM, fetch, CORS and storage are all standard, all documented, and all available to you today. A tool like Ln2CRM is proof of how far those primitives go when they are combined with care.