Skip to content

Bridge · #428

Any page's palette, in one click

Drag the button below to your bookmarks bar. On any page you then visit, it reads the colour custom properties that page defines — from stylesheets and inline styles — and draws a panel where every swatch copies its own declaration. Nothing about the page you are on leaves your browser: the script makes no request after loading its own file.

Install

Read paletteDrag this button to your bookmarks bar — or on a touch device, bookmark this page and replace the bookmark's address with the code below.
javascript:(()=>{const s=document.createElement('script');
  s.src='<this site>/api/exports/motif-bookmarklet.js?t='+Date.now();
  document.body.appendChild(s);})()

The button above is wired to the origin you are reading this on, so the same page works on a preview, on localhost or on a deployed host. The snippet shown here has the host blanked out on purpose — a copy with the wrong domain in it is a bookmarklet that fails silently.

How it works

  1. The bookmark is a javascript: URL that appends one <script> tag pointing at this site, and nothing else.
  2. The script walks the page's stylesheets for --* names whose values look like colours, plus inline styles, and de-duplicates them by name.
  3. Clicking a swatch copies --name: value; in the same form the stylesheet uses.

What it cannot do

  • Read cross-origin stylesheets. A page loading a CDN stylesheet exposes no rules to it, so tokens defined there are invisible — the panel says “no colour custom properties found” rather than pretending the page has none.
  • Copy on pages with a strict clipboard policy. The button then reports that the clipboard was blocked, which is the honest failure.
  • Save anything. There is no account, no storage and no server-side record of what pages you ran it on.

The script (3.9 KB, complete)

/* Motif UI palette reader — loaded by the bookmarklet on /integrations/bookmarklet.
 * Runs on the page you are looking at, reads its CSS custom properties, and
 * draws a panel. It sends nothing anywhere: there is no network call after this
 * file itself is fetched. */
(() => {
  const ID = "motif-palette-panel";
  const existing = document.getElementById(ID);
  if (existing) { existing.remove(); return; }

  const vars = [];
  for (const sheet of Array.from(document.styleSheets)) {
    let rules;
    try { rules = sheet.cssRules; } catch { continue; } // cross-origin sheet
    for (const rule of Array.from(rules || [])) {
      if (!rule.style) continue;
      for (const name of Array.from(rule.style)) {
        if (!name.startsWith("--")) continue;
        const value = rule.style.getPropertyValue(name).trim();
        if (/^(#|rgb|hsl)/.test(value)) vars.push([name, value]);
      }
    }
  }
  // Inline style attributes are a second, cheaper source.
  for (const el of Array.from(document.querySelectorAll("[style]"))) {
    for (const name of Array.from(el.style)) {
      if (!name.startsWith("--")) continue;
      const value = el.style.getPropertyValue(name).trim();
      if (/^(#|rgb|hsl)/.test(value)) vars.push([name, value]);
    }
  }

  const seen = new Map();
  for (const [name, value] of vars) if (!seen.has(name)) seen.set(name, value);

  const panel = document.createElement("div");
  panel.id = ID;
  panel.setAttribute("role", "dialog");
  panel.setAttribute("aria-label", "Colour custom properties on this page");
  panel.style.cssText = [
    "position:fixed", "z-index:2147483647", "right:16px", "bottom:16px",
    "max-height:70vh", "overflow:auto", "width:300px", "padding:14px",
    "border-radius:14px", "border:1px solid rgba(255,255,255,.16)",
    "background:#0b0d14", "color:#edf0f7", "font:12px/1.5 ui-monospace,monospace",
    "box-shadow:0 20px 60px rgba(0,0,0,.55)",
  ].join(";");

  const head = document.createElement("div");
  head.textContent = seen.size
    ? seen.size + " colour custom properties on this page"
    : "No colour custom properties found on this page";
  head.style.cssText = "font-weight:700;margin-bottom:8px";
  panel.appendChild(head);

  if (!seen.size) {
    const hint = document.createElement("div");
    hint.textContent = "This page may not define any --custom-properties, or its stylesheets are cross-origin.";
    hint.style.cssText = "opacity:.75";
    panel.appendChild(hint);
  }

  for (const [name, value] of seen) {
    const row = document.createElement("button");
    row.type = "button";
    row.style.cssText = "display:flex;align-items:center;gap:8px;width:100%;padding:5px 6px;border:0;border-radius:8px;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer";
    row.onmouseenter = () => { row.style.background = "rgba(255,255,255,.06)"; };
    row.onmouseleave = () => { row.style.background = "transparent"; };
    const swatch = document.createElement("span");
    swatch.style.cssText = "width:14px;height:14px;border-radius:4px;border:1px solid rgba(255,255,255,.25);background:" + value;
    const label = document.createElement("span");
    label.textContent = name + "  " + value;
    label.style.cssText = "flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap";
    row.append(swatch, label);
    row.title = "Copy " + name;
    row.onclick = async () => {
      try { await navigator.clipboard.writeText(name + ": " + value + ";"); label.textContent = "copied " + name; }
      catch { label.textContent = "copy blocked by the page"; }
    };
    panel.appendChild(row);
  }

  const close = document.createElement("button");
  close.type = "button";
  close.textContent = "Close";
  close.style.cssText = "margin-top:10px;width:100%;padding:6px;border-radius:8px;border:1px solid rgba(255,255,255,.16);background:transparent;color:inherit;font:inherit;cursor:pointer";
  close.onclick = () => panel.remove();
  panel.appendChild(close);

  document.body.appendChild(panel);
})();

Served from /api/exports/motif-bookmarklet.js, so the bookmark always runs the version this site currently serves. There is no version pinning, which is the same trade the other exports make.