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.
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.
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.
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.
data-vizu-key/exact, host-controlled. Always wins.#id/exact, per HTML uniqueness.Class signature/classList Jaccard match within tag, when unambiguous.CSS selector path/full hierarchy from root.Ancestor chain/3 levels of {tag, nth-of-type}, tolerant to wrapper inserts.Text 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.
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
}
}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 commentcomment:removedVisitor deleted onecomments:clearedBulk wipe via clearAll()comments:setHydrated programmaticallycomments:loadedStorage adapter resolvedelement:selectedVisitor clicked an elementelement:deselectedPopover closedsidebar:opened / :closedDrawer toggleaction:invokedPill action clickeduser:changedsetUser() calledenabled / disabledLifecyclemounted / unmountedUI mount lifecycleFour 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.
localStorage. Default in script-tag mode.setComments() and listens to events.{ load, save, clear }. Point at your backend.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.
Multiple reviewers on the same page. Storage = your own StorageAdapter. Subscribe to comment:added and POST to your API.
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.
useVizuAction({
id: 'copy-prompt',
label: 'Copy as prompt',
onClick: (ctx) =>
ctx.copyToClipboard(buildPrompt(ctx)),
});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_workspacesget_workspacelist_commentsOwner-scoped, OAuth-gated through Clerk. Writes stay behind the host-token popup. Set it up per workspace in your dashboard.
// 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.
new Vizu(options?)VizuOptions below.VizuOptions = { namespace, pageVersion, shortcut,
accent, storage, user, startEnabled,
ignoreSelectors, actions }'local' / 'memory' / 'none' / StorageAdapter. Shortcut defaults to 'mod+shift+e'.enable()disable()toggle()isEnabled(): booleandestroy()getComments(): VizuComment[]setComments(comments, opts?)opts.persist = false skips the storage adapter, for hydrating from your backend on load.clearAll(): Promise<void>setUser(user | null)getUser(): VizuUser | null{ id?, name, avatarUrl?, email? }.addAction({ id, label, onClick, variant?, title? })onClick receives an ActionContext with comments, pageHtml, user, copyToClipboard, toast.removeAction(id)invokeAction(id)getActions(): VizuAction[]on(event, handler): () => voidoff(event, handler)snapshotHtml(): stringjumpToFingerprint(fp)@unhingged/vizu-react<VizuProvider options={...}>useVizu(): VizuuseComments(): VizuComment[]useVizuUser(): [user, setUser]useVizuEvent(event, handler)useVizuAction({...})any HTML page · zero build step
Available · v0.0.1<!-- 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>@unhingged/vizu-react · hooks + provider
Available · v0.0.1$ 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// app/layout.tsx
'use client'
import { VizuProvider } from '@unhingged/vizu-react'
<VizuProvider options={{ namespace: 'my-app' }}>
{children}
</VizuProvider>@unhingged/vizu-vue · useVizu composable
Coming · v0.1$ bun add @unhingged/vizu-core @unhingged/vizu-vue@unhingged/vizu-angular · VizuService
Coming · v0.1$ bun add @unhingged/vizu-core @unhingged/vizu-angularNeed a different framework? Vizu's core is framework-agnostic. Wrap it for Svelte, Solid, Qwik, or anything in ~30 lines.