vizu
Live · the page is the demo

20 methods. 15 events. 5 hooks.

A drop-in script that turns any web page into a commentable surface. Comments anchor to the element itself, so they survive a redeploy. Press the shortcut to leave one on this page; the three annotations you see are real.

minified + gzipped
14.8kB
minified + gzipped
typed events
15
typed events
dependencies
0
dependencies
license
MIT
license
Screenshot · Mar 12
gone
Live · today
Why · 01

Screenshots break the address.

he default workflow for design feedback is to take a screenshot, mark it up in chat, paste a link in a ticket. The screenshot is frozen the second it's taken. The page keeps changing. Three days later the comment points at “the third card from the left.” The third card from the left no longer exists.

Vizu collapses three places (chat, design, code) into one. The comment lives on the same DOM element it was about, identified by a fingerprint that survives edits. The reviewer's intent and the reviewer's address travel together.

See the same page, two timestamps, in the hero above.

How · 02

Three parts. Each one emits its own events.

Anchoring

Comments anchor by fingerprint.

Every comment records six signals about the element it's attached to. When the page re-renders, Vizu walks each rung in order, falling through to fuzzier matches only when the cleaner ones miss.

  1. 01data-vizu-key/exact, host-controlled. Always wins.
  2. 02#id/exact, per HTML uniqueness.
  3. 03Class signature/classList Jaccard match within tag, when unambiguous.
  4. 04CSS selector path/full hierarchy from root.
  5. 05Ancestor chain/3 levels of {tag, nth-of-type}, tolerant to wrapper inserts.
  6. 06Text snippet/fuzzy Levenshtein match. Last resort.

A comment on the third “Buy” button is still on the third “Buy” button after a redesign. Across our drift test corpus, 96% of comments re-anchor through wrapper inserts, class renames, text edits, and sibling shuffles. When an element is truly deleted, the comment orphans cleanly instead of jumping to a similar element nearby. The drifted ones wear a visible badge so you know to glance before acting.

Tip · for the elements you review most

Add data-vizu-key=“…” to elements you anticipate reviewing repeatedly. Comments anchored to tagged elements resolve at 100% accuracy — every other rung is a fallback for elements you didn't tag.

type ElementFingerprint = {
  selector: string
  parentSelector: string
  tagName: string
  textSnippet: string // first 80 chars
  siblingIndex: number
  attributes: {
    id?: string
    classList?: string[]
    role?: string
    ariaLabel?: string
    dataKey?: string // data-vizu-key
  }
}
Eventing

The library emits. Your app decides.

Vizu ships no backend, no prompt builder, no persistence by default. It emits 15 typed events; you wire whatever side effects make sense for your application.

POST comments to your API on comment:added. Tie identity to your session on user:changed. Register a Copy-as-prompt action on the React side. The library knows nothing about how you store or use the data it collects.

comment:addedVisitor saved a new comment
comment:removedVisitor deleted one
comments:clearedBulk wipe via clearAll()
comments:setHydrated programmatically
comments:loadedStorage adapter resolved
element:selectedVisitor clicked an element
element:deselectedPopover closed
sidebar:opened / :closedDrawer toggle
action:invokedPill action clicked
user:changedsetUser() called
enabled / disabledLifecycle
mounted / unmountedUI mount lifecycle
Storage

Storage is pluggable.

Four built-in modes cover the common cases. Implement the three-method StorageAdapter interface for anything else. Storage is invoked in the background; the UI never blocks on it.

local
Persist to localStorage. Default in script-tag mode.
memory
RAM only. Wipes on reload. Default in programmatic mode; host owns persistence via events.
none
No-op. Host hydrates via setComments() and listens to events.
Custom
{ load, save, clear }. Point at your backend.
Use · 03

Three workflows. One library.

Default

Solo design review

One reviewer, one machine. storage: 'local'. Drop the script tag onto a static page, leave comments while iterating, copy the prompt into your LLM. No backend required.

script tag · localStorage · clipboard
Pattern

Team async review

Multiple reviewers on the same page. Storage = your own StorageAdapter. Subscribe to comment:added and POST to your API.

React + custom adapter · server · multi-user
Pattern

AI iteration loop

Reviewer copies the prompt. LLM rewrites the page. The redeploy keeps the same DOM structure where possible; fingerprints re-anchor unresolved comments. Next round starts where the last one ended.

01 comment02 copy prompt03 rebuild04 re-anchor
useVizuAction({
  id: 'copy-prompt',
  label: 'Copy as prompt',
  onClick: (ctx) =>
    ctx.copyToClipboard(buildPrompt(ctx)),
});
Vizu + Claude / GPT · re-anchor through edits
MCP · 04

Comments aren't a sink. They're an input.

The iteration loop above keeps a human in the middle. You copy the prompt, paste into Claude. MCP lifts that step out. Your agent reads comments directly through three read-only tools, same workspace, same data.

list_workspaces
every workspace you own. newest first.
get_workspace
one workspace plus its comment count.
list_comments
cursor-paginated. filter by status or pageUrl.

Owner-scoped, OAuth-gated through Clerk. Writes stay behind the host-token popup. Set it up per workspace in your dashboard.

01 page02 mcp03 agent
// claude_desktop_config.json
{ "mcpServers": { "vizu": { "url": "https://vizu.unhingged.com/api/mcp" } } }

Same shape works in Cursor. Continue.dev uses the streamable-http entry in ~/.continue/config.yaml.

API · 05

20 methods. 15 events. 5 hooks.

/Construct
new Vizu(options?)
Construct an instance. All options are optional. See VizuOptions below.
VizuOptions = { namespace, pageVersion, shortcut, accent, storage, user, startEnabled, ignoreSelectors, actions }
Storage: 'local' / 'memory' / 'none' / StorageAdapter. Shortcut defaults to 'mod+shift+e'.
/Lifecycle
enable()
Mount the UI and start the highlighter.
disable()
Unmount the UI; clears rendered markers.
toggle()
Enable if disabled, disable if enabled.
isEnabled(): boolean
Current state.
destroy()
Full teardown. Removes keyboard listeners.
/Comments
getComments(): VizuComment[]
Synchronous snapshot of stored comments.
setComments(comments, opts?)
Replace the list. opts.persist = false skips the storage adapter, for hydrating from your backend on load.
clearAll(): Promise<void>
Wipe every comment in this namespace.
/User
setUser(user | null)
Attach identity to subsequent new comments.
getUser(): VizuUser | null
Current user. { id?, name, avatarUrl?, email? }.
/Actions
addAction({ id, label, onClick, variant?, title? })
Register an action button in the pill. onClick receives an ActionContext with comments, pageHtml, user, copyToClipboard, toast.
removeAction(id)
Unregister an action.
invokeAction(id)
Trigger an action programmatically.
getActions(): VizuAction[]
Current registered actions.
/Events
on(event, handler): () => void
Subscribe to a typed event. Returns an unsubscribe function. Full event list in How.
off(event, handler)
Manual unsubscribe.
/Helpers
snapshotHtml(): string
Serialize the page DOM for LLM export. Strips Vizu UI.
jumpToFingerprint(fp)
Scroll to and pulse an element by its fingerprint.
/React bindings@unhingged/vizu-react
<VizuProvider options={...}>
Mount once at the layout root.
useVizu(): Vizu
The instance for imperative calls.
useComments(): VizuComment[]
Live comments. Re-renders on change.
useVizuUser(): [user, setUser]
Hook tuple for identity.
useVizuEvent(event, handler)
Auto-unsubscribes on unmount.
useVizuAction({...})
Auto-unregisters on unmount.
Install · 06

Pick a target. Drop it in.

01

Script tag

any HTML page · zero build step

Available · v0.0.1
Add to your page
<!-- exposes window.__vizu -->
<script
  src="https://unpkg.com/@unhingged/vizu-core/dist/vizu.min.js"
  data-shortcut="mod+shift+e"
  data-start-enabled="true">
</script>
02

React / Next.js

@unhingged/vizu-react · hooks + provider

Available · v0.0.1
Install
zsh
$ bun add @unhingged/vizu-core @unhingged/vizu-react
# or
$ npm install @unhingged/vizu-core @unhingged/vizu-react
# or
$ pnpm add @unhingged/vizu-core @unhingged/vizu-react
Mount
// app/layout.tsx
'use client'
import { VizuProvider } from '@unhingged/vizu-react'

<VizuProvider options={{ namespace: 'my-app' }}>
  {children}
</VizuProvider>
03

Vue 3

@unhingged/vizu-vue · useVizu composable

Coming · v0.1
Planned · package not yet published
zsh
$ bun add @unhingged/vizu-core @unhingged/vizu-vue
04

Angular

@unhingged/vizu-angular · VizuService

Coming · v0.1
Planned · package not yet published
zsh
$ bun add @unhingged/vizu-core @unhingged/vizu-angular

Need a different framework? Vizu's core is framework-agnostic. Wrap it for Svelte, Solid, Qwik, or anything in ~30 lines.

Vizu — Comment on the page, not on a screenshot of it.