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
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
- The bookmark is a javascript: URL that appends one <script> tag pointing at this site, and nothing else.
- The script walks the page's stylesheets for --* names whose values look like colours, plus inline styles, and de-duplicates them by name.
- 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.