Orca's element context picker was so handy that I turned it into a bookmarklet

2026-09-29 02:25 (83 minutes ago)
Orca's element context picker was so handy that I turned it into a bookmarklet

I have been using Orca ADE recently. Orca has a built-in browser. When you click an element on a page, it collects the element's information (selector, React component tree, text, nearby text, styles, HTML, and more) and passes it to the AI agent.

Clicking an element and handing over its context is faster and more accurate than explaining "please fix the display of this cell in this table" in words. I found this very useful.

However, I also wanted to do the same thing in my everyday Chrome, and with agents running outside Orca. So I made a bookmarklet that works the same way (the implementation was written by Claude Code). It copies the context of the clicked element to the clipboard, so you only need to paste it into the chat box of any agent.

Install

  1. Create a new bookmark in your browser (in Chrome, right-click the bookmarks bar and choose "Add page").
  2. Give it any name, for example 🎯 Pick Element.
  3. Paste all of the code below into the URL field.
javascript:%22use%20strict%22%3B(()%3D%3E%7Bconst%20h%3D%22__elementContextPicker%22%3Bif(window%5Bh%5D)%7Bwindow%5Bh%5D.stop()%3Breturn%7Dconst%20q%3D%222147483647%22%2CK%3D%22%232563eb%22%2Cg%3Ddocument.createElement(%22div%22)%3Bg.style.cssText%3D%60all%3Ainitial%3Bposition%3Afixed%3Binset%3A0%3Bpointer-events%3Anone%3Bz-index%3A%24%7Bq%7D%3B%60%3Bconst%20L%3Dg.attachShadow(%7Bmode%3A%22open%22%7D)%2CC%3De%3D%3E%7Bconst%20n%3Ddocument.createElement(%22div%22)%3Breturn%20n.style.cssText%3De%2CL.appendChild(n)%2Cn%7D%2C%24%3DC(%60position%3Afixed%3Bdisplay%3Anone%3Bbox-sizing%3Aborder-box%3Bpointer-events%3Anone%3Bborder%3A2px%20solid%20%24%7BK%7D%3Bbackground%3Argba(37%2C99%2C235%2C.08)%3Bborder-radius%3A2px%3B%60)%2Cb%3DC(%22position%3Afixed%3Bdisplay%3Anone%3Bpointer-events%3Anone%3Bmax-width%3A420px%3Boverflow%3Ahidden%3Bwhite-space%3Anowrap%3Btext-overflow%3Aellipsis%3Bpadding%3A3px%207px%3Bborder-radius%3A4px%3Bbackground%3A%231f2937%3Bcolor%3A%23fff%3Bfont%3A12px%2F1.4%20ui-monospace%2CSFMono-Regular%2CMenlo%2Cmonospace%3B%22)%2Cx%3DC('position%3Afixed%3Bdisplay%3Anone%3Bpointer-events%3Anone%3Bpadding%3A5px%2012px%3Bborder-radius%3A999px%3Bbackground%3A%23fff%3Bcolor%3A%23111827%3Bbox-shadow%3A0%202px%2010px%20rgba(0%2C0%2C0%2C.25)%3Bfont%3A600%2013px%2F1.4%20-apple-system%2CBlinkMacSystemFont%2C%22Segoe%20UI%22%2Csans-serif%3B')%3Blet%20m%3Dnull%3Btry%7Bm%3Dnew%20CSSStyleSheet%2Cm.replaceSync(%22*%7Bcursor%3Acrosshair%20!important%7Diframe%2Cframe%2Cembed%2Cobject%7Bpointer-events%3Anone%20!important%7D%22)%2Cdocument.adoptedStyleSheets%3D%5B...document.adoptedStyleSheets%2Cm%5D%7Dcatch%7Bm%3Dnull%7Ddocument.documentElement.appendChild(g)%3Blet%20d%3Dnull%2Cf%3D!0%2CT%3D!1%2CM%3D0%3Bconst%20v%3De%3D%3E(e%7C%7C%22%22).replace(%2F%5Cs%2B%2Fg%2C%22%20%22).trim()%2CS%3D(e%2Cn)%3D%3Ee.length%3En%3Fe.slice(0%2Cn)%2B%22%E2%80%A6%22%3Ae%2Cp%3De%3D%3Ee.tagName.toLowerCase()%2Ck%3De%3D%3Ewindow.CSS%26%26CSS.escape%3FCSS.escape(e)%3Ae.replace(%2F(%5B%5E%5Cw-%5D)%2Fg%2C%22%5C%5C%241%22)%2CN%3D(e%2Cn)%3D%3E%5B...e.classList%5D.filter(t%3D%3E!%2F%5B%3A%5B%5C%5D%2F()!%40%25%5D%2F.test(t)).slice(0%2Cn).map(t%3D%3E%22.%22%2Bk(t)).join(%22%22)%2Cj%3De%3D%3Ee.parentElement%7C%7C(e.parentNode%20instanceof%20ShadowRoot%3Fe.parentNode.host%3Anull)%2CU%3De%3D%3E%7Bconst%20n%3De.parentNode%3Bif(!n)return%200%3Bconst%20t%3D%5B...n.children%5D.filter(o%3D%3Eo.tagName%3D%3D%3De.tagName)%3Breturn%20t.length%3E1%3Ft.indexOf(e)%2B1%3A0%7D%2CR%3D(e%2Cn)%3D%3E%7Btry%7Breturn%20n.querySelectorAll(e).length%3D%3D%3D1%7Dcatch%7Breturn!1%7D%7D%2CV%3De%3D%3E%7Bconst%20n%3De.getRootNode()%2Ct%3D%5B%5D%3Bfor(let%20o%3De%3Bo%26%26o.nodeType%3D%3D%3D1%3Bo%3Do.parentElement)%7Bif(o.id%26%26R(%22%23%22%2Bk(o.id)%2Cn))%7Bt.unshift(%22%23%22%2Bk(o.id))%3Bbreak%7Dif(o%3D%3D%3Ddocument.body%7C%7Co%3D%3D%3Ddocument.documentElement)%7Bt.unshift(p(o))%3Bbreak%7Dconst%20r%3DU(o)%3Bif(t.unshift(p(o)%2BN(o%2C2)%2B(r%3F%60%3Anth-of-type(%24%7Br%7D)%60%3A%22%22))%2CR(t.join(%22%20%3E%20%22)%2Cn))break%7Dreturn%20t.join(%22%20%3E%20%22)%7D%2CY%3De%3D%3E%7Bconst%20n%3D%5B%5D%3Bfor(let%20t%3De%3Bt%3B)%7Bn.unshift(V(t))%3Bconst%20o%3Dt.getRootNode()%3Bt%3Do%20instanceof%20ShadowRoot%3Fo.host%3Anull%7Dreturn%20n.join(%22%20%3E%3E%3E%20%22)%7D%2CZ%3D%7Ba%3Ae%3D%3Ee.hasAttribute(%22href%22)%3F%22link%22%3A%22%22%2Carticle%3A%22article%22%2Caside%3A%22complementary%22%2Cbutton%3A%22button%22%2Cdialog%3A%22dialog%22%2Cfooter%3A%22contentinfo%22%2Cform%3A%22form%22%2Ch1%3A%22heading%22%2Ch2%3A%22heading%22%2Ch3%3A%22heading%22%2Ch4%3A%22heading%22%2Ch5%3A%22heading%22%2Ch6%3A%22heading%22%2Cheader%3A%22banner%22%2Chr%3A%22separator%22%2Cimg%3Ae%3D%3Ee.getAttribute(%22alt%22)%3D%3D%3D%22%22%3F%22presentation%22%3A%22img%22%2Cinput%3Ae%3D%3E%7Bconst%20n%3D(e.getAttribute(%22type%22)%7C%7C%22text%22).toLowerCase()%3Breturn%7Bcheckbox%3A%22checkbox%22%2Cradio%3A%22radio%22%2Crange%3A%22slider%22%2Cnumber%3A%22spinbutton%22%2Csearch%3A%22searchbox%22%2Cbutton%3A%22button%22%2Csubmit%3A%22button%22%2Creset%3A%22button%22%2Cimage%3A%22button%22%7D%5Bn%5D%7C%7C%22textbox%22%7D%2Cli%3A%22listitem%22%2Cmain%3A%22main%22%2Cnav%3A%22navigation%22%2Col%3A%22list%22%2Coption%3A%22option%22%2Cprogress%3A%22progressbar%22%2Csection%3A%22region%22%2Cselect%3A%22combobox%22%2Ctable%3A%22table%22%2Ctbody%3A%22rowgroup%22%2Cthead%3A%22rowgroup%22%2Ctfoot%3A%22rowgroup%22%2Ctd%3A%22cell%22%2Ctextarea%3A%22textbox%22%2Cth%3A%22columnheader%22%2Ctr%3A%22row%22%2Cul%3A%22list%22%7D%2CG%3De%3D%3E%7Bconst%20n%3De.getAttribute(%22role%22)%3Bif(n)return%20n%3Bconst%20t%3DZ%5Bp(e)%5D%3Breturn(typeof%20t%3D%3D%22function%22%3Ft(e)%3At)%7C%7Cp(e)%7D%2CA%3De%3D%3E%7Bif(!e)return%22%22%3Bif(typeof%20e%3D%3D%22function%22)return%20e.displayName%7C%7Ce.name%7C%7C%22%22%3Bif(typeof%20e%3D%3D%22object%22)%7Bif(e.displayName)return%20e.displayName%3Bif(e.render)return%20e.render.displayName%7C%7Ce.render.name%7C%7C%22%22%3Bif(e.type)return%20A(e.type)%7Dreturn%22%22%7D%2CJ%3De%3D%3E%7Bfor(let%20n%3De%3Bn%3Bn%3Dn.parentElement)%7Bconst%20t%3DObject.keys(n).find(r%3D%3Er.startsWith(%22__reactFiber%24%22)%7C%7Cr.startsWith(%22__reactInternalInstance%24%22))%3Bif(!t)continue%3Bconst%20o%3D%5B%5D%3Bfor(let%20r%3Dn%5Bt%5D%3Br%3Br%3Dr.return)%7Bconst%20a%3DA(r.type)%3Ba%26%26typeof%20r.type!%3D%22string%22%26%26o.unshift(a)%7Dreturn%20o%7Dreturn%5B%5D%7D%2CQ%3De%3D%3E%7Bfor(let%20n%3De%3Bn%3Bn%3Dn.parentElement)%7Blet%20t%3Dn.__vueParentComponent%3Bif(!t)continue%3Bconst%20o%3D%5B%5D%3Bfor(%3Bt%3Bt%3Dt.parent)%7Bconst%20r%3Dt.type%7C%7C%7B%7D%2Ca%3Dr.name%7C%7Cr.__name%7C%7C(r.__file%3Fr.__file.split(%22%2F%22).pop()%3A%22%22)%3Ba%26%26o.unshift(a)%7Dreturn%20o%7Dreturn%5B%5D%7D%2CX%3De%3D%3E%7Bfor(let%20n%3De%3Bn%3Bn%3Dn.parentElement)%7Bconst%20t%3Dn.__svelte_meta%26%26n.__svelte_meta.loc%3Bif(t)return%60%24%7Bt.file%7D%3A%24%7Bt.line%7D%3A%24%7Bt.column%7D%60%7Dreturn%22%22%7D%2Cee%3D(e%2Cn)%3D%3E%7Bconst%20t%3Dv(e.innerText)%2Co%3D%5B%5D%2Cr%3Dnew%20Set(%5Bt%5D)%2Ca%3Di%3D%3E%7Bconst%20s%3DS(v(i.innerText)%2C80)%3Bs%26%26!r.has(s)%26%26(r.add(s)%2Co.push(s))%7D%3Bfor(let%20i%3De%3Bi%26%26i!%3D%3Ddocument.body%26%26o.length%3Cn%3Bi%3Di.parentElement)%7Blet%20s%3Di.previousElementSibling%2Cc%3Di.nextElementSibling%3Bfor(%3B(s%7C%7Cc)%26%26o.length%3Cn%3B)s%26%26(a(s)%2Cs%3Ds.previousElementSibling)%2Cc%26%26o.length%3Cn%26%26(a(c)%2Cc%3Dc.nextElementSibling)%7Dreturn%20o%7D%2Cte%3De%3D%3E%7Bconst%20n%3DgetComputedStyle(e)%2Ct%3D%5B%60display%3A%20%24%7Bn.display%7D%60%2C%60font-size%3A%20%24%7Bn.fontSize%7D%60%2C%60color%3A%20%24%7Bn.color%7D%60%5D%3Breturn%20n.position!%3D%3D%22static%22%26%26t.push(%60position%3A%20%24%7Bn.position%7D%60)%2Cn.backgroundColor!%3D%3D%22rgba(0%2C%200%2C%200%2C%200)%22%26%26n.backgroundColor!%3D%3D%22transparent%22%26%26t.push(%60background-color%3A%20%24%7Bn.backgroundColor%7D%60)%2Ct%7D%2Cne%3De%3D%3E%7Bconst%20n%3D%5B%5D%3Bfor(let%20t%3Dj(e)%3Bt%26%26t!%3D%3Ddocument.documentElement%3Bt%3Dj(t))n.push(t)%3Breturn%20n%7D%2CO%3De%3D%3E%7Bconst%20n%3De.getBoundingClientRect()%2Ct%3D%5B%60Attached%20browser%20context%20from%20%24%7Blocation.href%7D%60%2C%22%22%2C%22Selected%20element%3A%22%2Cp(e)%5D%3Bt.push(%60Role%3A%20%24%7BG(e)%7D%60)%2Ct.push(%60Selector%3A%20%24%7BY(e)%7D%60)%3Bconst%20o%3DJ(e).slice(-6)%3Bo.length%26%26t.push(%60React%3A%20%24%7Bo.map(l%3D%3E%60%3C%24%7Bl%7D%3E%60).join(%22%20%22)%7D%60)%3Bconst%20r%3DQ(e).slice(-6)%3Br.length%26%26t.push(%60Vue%3A%20%24%7Br.map(l%3D%3E%60%3C%24%7Bl%7D%3E%60).join(%22%20%22)%7D%60)%3Bconst%20a%3DX(e)%3Ba%26%26t.push(%60Svelte%3A%20%24%7Ba%7D%60)%2Ct.push(%60Dimensions%3A%20%24%7BMath.round(n.width)%7Dx%24%7BMath.round(n.height)%7D%60)%3Bconst%20i%3DS(v(e.innerText)%2C1e3)%3Bi%26%26t.push(%22%22%2C%22Text%20content%3A%22%2Ci)%3Bconst%20s%3Dee(e%2C5)%3Bs.length%26%26t.push(%22%22%2C%22Nearby%20context%3A%22%2C...s.map(l%3D%3E%60-%20%24%7Bl%7D%60))%2Ct.push(%22%22%2C%22Computed%20styles%3A%22%2C...te(e).map(l%3D%3E%60%20%20%24%7Bl%7D%60))%2Ct.push(%22%22%2C%22HTML%3A%22%2CS(e.outerHTML%2C2e3))%3Bconst%20c%3Dne(e)%3Breturn%20t.push(%22%22%2C%60Ancestor%20path%3A%20%24%7Bc.map(p).join(%22%20%3E%20%22)%7D%60)%2Ct.push(%60Full%20DOM%20path%3A%20%24%7B%5B...c%5D.reverse().concat(e).map(l%3D%3Ep(l)%2BN(l%2C2)).join(%22%20%3E%20%22)%7D%60)%2Ct.join(%60%0A%60)%7D%2CI%3D(e%2Cn%2Ct)%3D%3E%7Be.style.left%3D%220px%22%2Ce.style.top%3D%220px%22%3Bconst%20o%3De.offsetWidth%2Cr%3De.offsetHeight%2Ca%3Ddocument.documentElement.clientWidth%2Ci%3Ddocument.documentElement.clientHeight%3Blet%20s%3Dn.bottom%2Bt%3Bs%2Br%3Ei%26%26(s%3Dn.top-t-r)%2Cs%3DMath.max(4%2CMath.min(s%2Ci-r-4))%3Bconst%20c%3DMath.max(4%2CMath.min(n.left%2Ca-o-4))%3Be.style.left%3Dc%2B%22px%22%2Ce.style.top%3Ds%2B%22px%22%7D%2CP%3D()%3D%3E%7Bif(!d%7C%7C!d.isConnected)%7B%24.style.display%3D%22none%22%2Cb.style.display%3D%22none%22%2Cx.style.display%3D%22none%22%3Breturn%7Dconst%20e%3Dd.getBoundingClientRect()%3Bif(Object.assign(%24.style%2C%7Bdisplay%3A%22block%22%2Cleft%3Ae.left%2B%22px%22%2Ctop%3Ae.top%2B%22px%22%2Cwidth%3Ae.width%2B%22px%22%2Cheight%3Ae.height%2B%22px%22%7D)%2C!f)%7Bb.style.display%3D%22none%22%2Cx.style.display!%3D%3D%22none%22%26%26I(x%2Ce%2C8)%3Breturn%7Dconst%20n%3DS(v(d.innerText)%2C30)%3Bb.textContent%3D%60%24%7Bp(d)%7D%24%7Bn%3F%60%20%22%24%7Bn%7D%22%60%3A%22%22%7D%20%24%7BMath.round(e.width)%7Dx%24%7BMath.round(e.height)%7D%60%2Cb.style.display%3D%22block%22%2CI(b%2Ce%2C6)%7D%3Blet%20y%3D0%3Bconst%20w%3D()%3D%3E%7By%7C%7C(y%3DrequestAnimationFrame(()%3D%3E%7By%3D0%2CP()%7D))%7D%2Coe%3Dasync%20e%3D%3E%7Btry%7Breturn%20await%20navigator.clipboard.writeText(e)%2C!0%7Dcatch%7Bconst%20t%3Ddocument.createElement(%22textarea%22)%3Bt.value%3De%2Ct.style.cssText%3D%22position%3Afixed%3Btop%3A0%3Bleft%3A0%3Bopacity%3A0%3B%22%2CL.appendChild(t)%2Ct.select()%3Blet%20o%3D!1%3Btry%7Bo%3Ddocument.execCommand(%22copy%22)%7Dcatch%7Bo%3D!1%7Dreturn%20t.remove()%2Co%7D%7D%2CE%3De%3D%3E%7Bf%26%26(e.preventDefault()%2Ce.stopPropagation()%2Ce.stopImmediatePropagation())%7D%2Cz%3De%3D%3E%7Bif(!f)return%3Bconst%20n%3De.composedPath%3Fe.composedPath()%5B0%5D%3Ae.target%2Ct%3Dn%26%26n.nodeType%3D%3D%3D1%3Fn%3Ae.target%3B!t%7C%7Ct%3D%3D%3Dg%7C%7Ct%3D%3D%3Ddocument.documentElement%7C%7Ct!%3D%3Dd%26%26(d%3Dt%2Cw())%7D%2CF%3Dasync%20e%3D%3E%7Bif(!f)return%3BE(e)%3Bconst%20n%3Dd%7C%7Ce.target%3Bif(!n%7C%7Cn.nodeType!%3D%3D1)return%3Bd%3Dn%2Cf%3D!1%2CH()%3Bconst%20t%3Dawait%20oe(O(n))%3BT%7C%7C(%24.style.borderColor%3Dt%3F%22%2316a34a%22%3A%22%23dc2626%22%2Cx.textContent%3Dt%3F%22%E2%9C%85%20Copied%22%3A%22%E2%9A%A0%EF%B8%8F%20Copy%20failed%22%2Cx.style.display%3D%22block%22%2CP()%2CM%3DsetTimeout(_%2C3e3))%7D%2CB%3De%3D%3E%7Be.key%3D%3D%3D%22Escape%22%26%26(E(e)%2C_())%7D%2CD%3D%5B%22mousedown%22%2C%22mouseup%22%2C%22pointerdown%22%2C%22pointerup%22%2C%22dblclick%22%2C%22auxclick%22%2C%22contextmenu%22%5D%2Cu%3D%7Bcapture%3A!0%2Cpassive%3A!1%7D%3Bfunction%20H()%7Bwindow.removeEventListener(%22mousemove%22%2Cz%2Cu)%2Cwindow.removeEventListener(%22click%22%2CF%2Cu)%2Cwindow.removeEventListener(%22keydown%22%2CB%2Cu)%2CD.forEach(e%3D%3Ewindow.removeEventListener(e%2CE%2Cu))%2Cm%26%26(document.adoptedStyleSheets%3Ddocument.adoptedStyleSheets.filter(e%3D%3Ee!%3D%3Dm)%2Cm%3Dnull)%7Dfunction%20_()%7BT%3D!0%2Cf%3D!1%2CclearTimeout(M)%2CH()%2Cwindow.removeEventListener(%22scroll%22%2Cw%2C!0)%2Cwindow.removeEventListener(%22resize%22%2Cw)%2Cy%26%26cancelAnimationFrame(y)%2Cg.remove()%2Cwindow%5Bh%5D%3D%3D%3DW%26%26delete%20window%5Bh%5D%7Dwindow.addEventListener(%22mousemove%22%2Cz%2Cu)%2Cwindow.addEventListener(%22click%22%2CF%2Cu)%2Cwindow.addEventListener(%22keydown%22%2CB%2Cu)%2CD.forEach(e%3D%3Ewindow.addEventListener(e%2CE%2Cu))%2Cwindow.addEventListener(%22scroll%22%2Cw%2C!0)%2Cwindow.addEventListener(%22resize%22%2Cw)%3Bconst%20W%3D%7Bstop%3A_%2CbuildContext%3AO%7D%3Bwindow%5Bh%5D%3DW%7D)()%3B

If you paste it into the address bar instead, the browser may remove the leading javascript:. Paste it into the bookmark's URL field.

Usage

  1. Click the bookmark on the page you want to inspect.
  2. Move the mouse. The hovered element gets a blue outline, and a tooltip such as td "Pending 2" 70x33 appears below it.
  3. Click to copy the element's context to the clipboard. The outline turns green, "✅ Copied" appears below the element for 3 seconds, and then the picker exits automatically.
  4. To cancel, press Esc or click the bookmark again.

While picking, the page's own click actions (link navigation, onClick handlers) do not fire.

What gets copied

The format follows Orca's output. For example, selecting a table cell gives this:

Attached browser context from http://localhost:3000/orders

Selected element:
td
Role: cell
Selector: td.px-2.py-1\.5:nth-of-type(5)
React: <OrdersPage> <OrderList> <TableCard> <DataTable>
Dimensions: 70x33

Text content:
Pending 2

Nearby context:
- Paid Express
- 3 items
- Tokyo
- 2026-09-25 16:16
- #1

Computed styles:
  display: table-cell
  font-size: 14px
  color: rgb(30, 30, 60)

HTML:
<td class="px-2 py-1.5 align-top text-sm">Pending 2</td>

Ancestor path: tr > tbody > table > div > main > body
Full DOM path: body.flex.min-h-screen > main.flex-1 > div.w-full.overflow-auto > table.min-w-full.border-collapse > tbody > tr.cursor-pointer.border-b > td.px-2.py-1\.5
  • Selector is built by walking from the element up to its parents, stacking tag.class1.class2:nth-of-type(n), and stopping as soon as querySelectorAll matches exactly one element. It also stops at a unique id. Classes with symbols, such as Tailwind's hover:bg-..., are skipped because they are hard to read.
  • React follows the __reactFiber$... property on the DOM element and lists component names from the outside in. If available, it also outputs the Vue 3 component tree (__vueParentComponent) and the Svelte source location in dev builds (__svelte_meta).
  • Nearby context collects up to 5 texts from sibling elements, alternating before and after, nearest first.
  • Role shows the role attribute if there is one, and otherwise the implicit ARIA role (cell for a td). This differs from Orca, which shows td for a td.

Implementation notes

Showing the outline without breaking the layout

Adding outline or border to the selected element can shift the layout on some pages, or conflict with the page's CSS. So the target element is never touched.

A single element with position: fixed; pointer-events: none is placed directly under <html>, and the outline, tooltip, and badge are drawn inside its Shadow DOM. Their positions are calculated from getBoundingClientRect() and recalculated on every scroll and resize. Because of pointer-events: none, mouse events go straight through to the page elements below.

Stopping the page's click actions

Listeners for click, mousedown, pointerdown, and similar events are registered on window in the capture phase, and they call preventDefault() and stopImmediatePropagation(). React receives events at its root element, so they are stopped before React sees them.

Avoiding CSP blocks

Inserting a <style> element or using innerHTML gets blocked on sites with a strict CSP. So styles are applied with el.style.cssText (CSSOM), and the CSS for the crosshair cursor is added with a Constructable Stylesheet (document.adoptedStyleSheets).

Clipboard

navigator.clipboard.writeText is not available on http pages other than localhost (non-secure contexts). If it fails, the script falls back to the older method of a textarea plus document.execCommand('copy').

Shadow DOM and iframes

  • When you pick an element inside a Web Component, a selector is built for each shadow root and the parts are joined with >>> (for example, x-card >>> span.label:nth-of-type(2)). This is not valid CSS, but it shows where the path enters a shadow root.
  • Events inside an iframe do not reach the parent window. So while picking, iframes get pointer-events: none, and the iframe's parent element is selected instead. You cannot pick elements inside an iframe.

Limitations

  • You cannot pick elements inside an iframe.
  • On sites with a very strict CSP, the browser may block the bookmarklet (javascript: URL) from running at all.
  • React component names are only meaningful in development builds. In production builds they are minified to 1–2 letter names. The Svelte source location also appears only in development builds.

Tests

I wrote 8 tests with Playwright (headless Chromium):

  • The position and size of every element on the page do not change before and after hovering (the layout is not broken).
  • Each item of the copied content. Searching with the output Selector matches only the target element.
  • The page's click handlers do not fire.
  • The fallback when the Clipboard API is unavailable.
  • Shadow DOM, iframes, scrolling after the copy, and running the bookmarklet repeatedly.

I also checked that the tests really catch bugs. I ran them against a version with the fallback deliberately broken and against the code before the fixes, and confirmed that the relevant tests fail. This check found a hole: the headless clipboard can be shared between browser contexts, so a test could pass by reading the value left by the previous test.

Source code

The bookmarklet is this code minified with esbuild, passed through encodeURIComponent, and prefixed with javascript:.

npx esbuild element-context-picker.js --minify --target=es2019 --charset=utf8
(() => {
  const KEY = '__elementContextPicker';
  if (window[KEY]) {
    window[KEY].stop();
    return;
  }

  const Z = '2147483647';
  const ACCENT = '#2563eb';

  // Constructable stylesheets and CSSOM writes are not blocked by a page's
  // style-src CSP, unlike <style> elements or innerHTML.
  const host = document.createElement('div');
  host.style.cssText = `all:initial;position:fixed;inset:0;pointer-events:none;z-index:${Z};`;
  const root = host.attachShadow({ mode: 'open' });

  const make = (css) => {
    const el = document.createElement('div');
    el.style.cssText = css;
    root.appendChild(el);
    return el;
  };
  const box = make(
    `position:fixed;display:none;box-sizing:border-box;pointer-events:none;` +
      `border:2px solid ${ACCENT};background:rgba(37,99,235,.08);border-radius:2px;`,
  );
  const tip = make(
    `position:fixed;display:none;pointer-events:none;max-width:420px;overflow:hidden;` +
      `white-space:nowrap;text-overflow:ellipsis;padding:3px 7px;border-radius:4px;` +
      `background:#1f2937;color:#fff;font:12px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;`,
  );
  const badge = make(
    `position:fixed;display:none;pointer-events:none;padding:5px 12px;border-radius:999px;` +
      `background:#fff;color:#111827;box-shadow:0 2px 10px rgba(0,0,0,.25);` +
      `font:600 13px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;`,
  );

  let cursorSheet = null;
  try {
    cursorSheet = new CSSStyleSheet();
    // Events inside frames never reach this window, so frames are made inert
    // and picking lands on their container instead.
    cursorSheet.replaceSync(
      '*{cursor:crosshair !important}iframe,frame,embed,object{pointer-events:none !important}',
    );
    document.adoptedStyleSheets = [...document.adoptedStyleSheets, cursorSheet];
  } catch (_) {
    cursorSheet = null;
  }

  document.documentElement.appendChild(host);

  let current = null;
  let picking = true;
  let stopped = false;
  let exitTimer = 0;

  const clean = (s) => (s || '').replace(/\s+/g, ' ').trim();
  const cut = (s, n) => (s.length > n ? s.slice(0, n) + '…' : s);
  const tag = (el) => el.tagName.toLowerCase();
  const esc = (s) => (window.CSS && CSS.escape ? CSS.escape(s) : s.replace(/([^\w-])/g, '\\$1'));

  const classesOf = (el, max) =>
    [...el.classList]
      .filter((c) => !/[:[\]/()!@%]/.test(c))
      .slice(0, max)
      .map((c) => '.' + esc(c))
      .join('');

  const parentOf = (node) =>
    node.parentElement || (node.parentNode instanceof ShadowRoot ? node.parentNode.host : null);

  const nthOfType = (el) => {
    const p = el.parentNode;
    if (!p) return 0;
    const same = [...p.children].filter((c) => c.tagName === el.tagName);
    return same.length > 1 ? same.indexOf(el) + 1 : 0;
  };

  const isUnique = (sel, root) => {
    try {
      return root.querySelectorAll(sel).length === 1;
    } catch (_) {
      return false;
    }
  };

  const selectorInRoot = (el) => {
    const root = el.getRootNode();
    const parts = [];
    for (let node = el; node && node.nodeType === 1; node = node.parentElement) {
      if (node.id && isUnique('#' + esc(node.id), root)) {
        parts.unshift('#' + esc(node.id));
        break;
      }
      if (node === document.body || node === document.documentElement) {
        parts.unshift(tag(node));
        break;
      }
      const n = nthOfType(node);
      parts.unshift(tag(node) + classesOf(node, 2) + (n ? `:nth-of-type(${n})` : ''));
      if (isUnique(parts.join(' > '), root)) break;
    }
    return parts.join(' > ');
  };

  // `>>>` is not valid CSS; it marks where a selector continues inside a shadow root.
  const buildSelector = (el) => {
    const segments = [];
    for (let node = el; node; ) {
      segments.unshift(selectorInRoot(node));
      const root = node.getRootNode();
      node = root instanceof ShadowRoot ? root.host : null;
    }
    return segments.join(' >>> ');
  };

  const IMPLICIT_ROLES = {
    a: (el) => (el.hasAttribute('href') ? 'link' : ''),
    article: 'article', aside: 'complementary', button: 'button', dialog: 'dialog',
    footer: 'contentinfo', form: 'form', h1: 'heading', h2: 'heading', h3: 'heading',
    h4: 'heading', h5: 'heading', h6: 'heading', header: 'banner', hr: 'separator',
    img: (el) => (el.getAttribute('alt') === '' ? 'presentation' : 'img'),
    input: (el) => {
      const t = (el.getAttribute('type') || 'text').toLowerCase();
      return (
        { checkbox: 'checkbox', radio: 'radio', range: 'slider', number: 'spinbutton',
          search: 'searchbox', button: 'button', submit: 'button', reset: 'button',
          image: 'button' }[t] || 'textbox'
      );
    },
    li: 'listitem', main: 'main', nav: 'navigation', ol: 'list', option: 'option',
    progress: 'progressbar', section: 'region', select: 'combobox', table: 'table',
    tbody: 'rowgroup', thead: 'rowgroup', tfoot: 'rowgroup', td: 'cell',
    textarea: 'textbox', th: 'columnheader', tr: 'row', ul: 'list',
  };
  const roleOf = (el) => {
    const explicit = el.getAttribute('role');
    if (explicit) return explicit;
    const r = IMPLICIT_ROLES[tag(el)];
    return (typeof r === 'function' ? r(el) : r) || tag(el);
  };

  const componentName = (type) => {
    if (!type) return '';
    if (typeof type === 'function') return type.displayName || type.name || '';
    if (typeof type === 'object') {
      if (type.displayName) return type.displayName;
      if (type.render) return type.render.displayName || type.render.name || '';
      if (type.type) return componentName(type.type);
    }
    return '';
  };

  const reactComponents = (el) => {
    for (let node = el; node; node = node.parentElement) {
      const key = Object.keys(node).find(
        (k) => k.startsWith('__reactFiber$') || k.startsWith('__reactInternalInstance$'),
      );
      if (!key) continue;
      const names = [];
      for (let f = node[key]; f; f = f.return) {
        const name = componentName(f.type);
        if (name && typeof f.type !== 'string') names.unshift(name);
      }
      return names;
    }
    return [];
  };

  const vueComponents = (el) => {
    for (let node = el; node; node = node.parentElement) {
      let inst = node.__vueParentComponent;
      if (!inst) continue;
      const names = [];
      for (; inst; inst = inst.parent) {
        const t = inst.type || {};
        const name = t.name || t.__name || (t.__file ? t.__file.split('/').pop() : '');
        if (name) names.unshift(name);
      }
      return names;
    }
    return [];
  };

  // Svelte attaches __svelte_meta only in dev builds.
  const svelteLocation = (el) => {
    for (let node = el; node; node = node.parentElement) {
      const loc = node.__svelte_meta && node.__svelte_meta.loc;
      if (loc) return `${loc.file}:${loc.line}:${loc.column}`;
    }
    return '';
  };

  const nearbyContext = (el, limit) => {
    const own = clean(el.innerText);
    const out = [];
    const seen = new Set([own]);
    const push = (node) => {
      const t = cut(clean(node.innerText), 80);
      if (t && !seen.has(t)) {
        seen.add(t);
        out.push(t);
      }
    };
    for (let node = el; node && node !== document.body && out.length < limit; node = node.parentElement) {
      let prev = node.previousElementSibling;
      let next = node.nextElementSibling;
      while ((prev || next) && out.length < limit) {
        if (prev) {
          push(prev);
          prev = prev.previousElementSibling;
        }
        if (next && out.length < limit) {
          push(next);
          next = next.nextElementSibling;
        }
      }
    }
    return out;
  };

  const computedStyles = (el) => {
    const cs = getComputedStyle(el);
    const lines = [`display: ${cs.display}`, `font-size: ${cs.fontSize}`, `color: ${cs.color}`];
    if (cs.position !== 'static') lines.push(`position: ${cs.position}`);
    if (cs.backgroundColor !== 'rgba(0, 0, 0, 0)' && cs.backgroundColor !== 'transparent') {
      lines.push(`background-color: ${cs.backgroundColor}`);
    }
    return lines;
  };

  const ancestors = (el) => {
    const list = [];
    for (let node = parentOf(el); node && node !== document.documentElement; node = parentOf(node)) {
      list.push(node);
    }
    return list;
  };

  const buildContext = (el) => {
    const rect = el.getBoundingClientRect();
    const lines = [`Attached browser context from ${location.href}`, '', 'Selected element:', tag(el)];
    lines.push(`Role: ${roleOf(el)}`);
    lines.push(`Selector: ${buildSelector(el)}`);
    const react = reactComponents(el).slice(-6);
    if (react.length) lines.push(`React: ${react.map((n) => `<${n}>`).join(' ')}`);
    const vue = vueComponents(el).slice(-6);
    if (vue.length) lines.push(`Vue: ${vue.map((n) => `<${n}>`).join(' ')}`);
    const svelte = svelteLocation(el);
    if (svelte) lines.push(`Svelte: ${svelte}`);
    lines.push(`Dimensions: ${Math.round(rect.width)}x${Math.round(rect.height)}`);

    const text = cut(clean(el.innerText), 1000);
    if (text) lines.push('', 'Text content:', text);

    const nearby = nearbyContext(el, 5);
    if (nearby.length) lines.push('', 'Nearby context:', ...nearby.map((t) => `- ${t}`));

    lines.push('', 'Computed styles:', ...computedStyles(el).map((l) => `  ${l}`));
    lines.push('', 'HTML:', cut(el.outerHTML, 2000));

    const anc = ancestors(el);
    lines.push('', `Ancestor path: ${anc.map(tag).join(' > ')}`);
    lines.push(
      `Full DOM path: ${[...anc].reverse().concat(el).map((n) => tag(n) + classesOf(n, 2)).join(' > ')}`,
    );
    return lines.join('\n');
  };

  const placeBelow = (el, rect, gap) => {
    el.style.left = '0px';
    el.style.top = '0px';
    const w = el.offsetWidth;
    const h = el.offsetHeight;
    const vw = document.documentElement.clientWidth;
    const vh = document.documentElement.clientHeight;
    let top = rect.bottom + gap;
    if (top + h > vh) top = rect.top - gap - h;
    top = Math.max(4, Math.min(top, vh - h - 4));
    const left = Math.max(4, Math.min(rect.left, vw - w - 4));
    el.style.left = left + 'px';
    el.style.top = top + 'px';
  };

  const render = () => {
    if (!current || !current.isConnected) {
      box.style.display = 'none';
      tip.style.display = 'none';
      badge.style.display = 'none';
      return;
    }
    const r = current.getBoundingClientRect();
    Object.assign(box.style, {
      display: 'block',
      left: r.left + 'px',
      top: r.top + 'px',
      width: r.width + 'px',
      height: r.height + 'px',
    });
    if (!picking) {
      tip.style.display = 'none';
      if (badge.style.display !== 'none') placeBelow(badge, r, 8);
      return;
    }
    const text = cut(clean(current.innerText), 30);
    tip.textContent = `${tag(current)}${text ? ` "${text}"` : ''} ${Math.round(r.width)}x${Math.round(r.height)}`;
    tip.style.display = 'block';
    placeBelow(tip, r, 6);
  };

  let raf = 0;
  const scheduleRender = () => {
    if (raf) return;
    raf = requestAnimationFrame(() => {
      raf = 0;
      render();
    });
  };

  const copyText = async (text) => {
    try {
      await navigator.clipboard.writeText(text);
      return true;
    } catch (_) {
      // navigator.clipboard is unavailable on non-secure origins (plain http other than localhost).
      const ta = document.createElement('textarea');
      ta.value = text;
      ta.style.cssText = 'position:fixed;top:0;left:0;opacity:0;';
      root.appendChild(ta);
      ta.select();
      let ok = false;
      try {
        ok = document.execCommand('copy');
      } catch (_) {
        ok = false;
      }
      ta.remove();
      return ok;
    }
  };

  const block = (e) => {
    if (!picking) return;
    e.preventDefault();
    e.stopPropagation();
    e.stopImmediatePropagation();
  };

  const onMove = (e) => {
    if (!picking) return;
    const el = e.composedPath ? e.composedPath()[0] : e.target;
    const target = el && el.nodeType === 1 ? el : e.target;
    if (!target || target === host || target === document.documentElement) return;
    if (target !== current) {
      current = target;
      scheduleRender();
    }
  };

  const onClick = async (e) => {
    if (!picking) return;
    block(e);
    const el = current || e.target;
    if (!el || el.nodeType !== 1) return;
    current = el;
    picking = false;
    detachInput();
    const ok = await copyText(buildContext(el));
    if (stopped) return;
    box.style.borderColor = ok ? '#16a34a' : '#dc2626';
    badge.textContent = ok ? '✅ Copied' : '⚠️ Copy failed';
    badge.style.display = 'block';
    render();
    exitTimer = setTimeout(stop, 3000);
  };

  const onKey = (e) => {
    if (e.key === 'Escape') {
      block(e);
      stop();
    }
  };

  const BLOCKED = ['mousedown', 'mouseup', 'pointerdown', 'pointerup', 'dblclick', 'auxclick', 'contextmenu'];
  const opts = { capture: true, passive: false };

  function detachInput() {
    window.removeEventListener('mousemove', onMove, opts);
    window.removeEventListener('click', onClick, opts);
    window.removeEventListener('keydown', onKey, opts);
    BLOCKED.forEach((t) => window.removeEventListener(t, block, opts));
    if (cursorSheet) {
      document.adoptedStyleSheets = document.adoptedStyleSheets.filter((s) => s !== cursorSheet);
      cursorSheet = null;
    }
  }

  function stop() {
    stopped = true;
    picking = false;
    clearTimeout(exitTimer);
    detachInput();
    window.removeEventListener('scroll', scheduleRender, true);
    window.removeEventListener('resize', scheduleRender);
    if (raf) cancelAnimationFrame(raf);
    host.remove();
    if (window[KEY] === api) delete window[KEY];
  }

  window.addEventListener('mousemove', onMove, opts);
  window.addEventListener('click', onClick, opts);
  window.addEventListener('keydown', onKey, opts);
  BLOCKED.forEach((t) => window.addEventListener(t, block, opts));
  window.addEventListener('scroll', scheduleRender, true);
  window.addEventListener('resize', scheduleRender);

  const api = { stop, buildContext };
  window[KEY] = api;
})();
Please rate this article (No signup or login required)
Currently unrated
The author runs the application development company Cyberneura.
We look forward to discussing your development needs.

Categories

Archive