Add QA in 3 minutes
npm install → mount <FivePixels /> → add data-report-id. Three steps and you can leave feedback on your staging screen.
npm install @fivepixels-js/react react react-domInstall
Install the package in your React 18+ project.
npm install @fivepixels-js/react react react-domIn Next.js and other SSR apps, render <FivePixels /> on the client only ("use client" + dynamic import).
Mount
Render <FivePixels /> once on the screen that receives feedback. No CSS import required.
import { FivePixels } from "@fivepixels-js/react";
export default function App() {
return (
<>
<FivePixels />
<main>
<section data-report-id="hero" data-report-type="group">
<button data-report-id="hero-cta">Get Started</button>
</section>
</main>
</>
);
}- project.id defaults to "my-app" when omitted. Set project={{ id }} for stage/production or multi-app origins.
- UI mounts inside Shadow Root with bundled Tailwind — no separate stylesheet import.
Mark elements
Add data-report-id to feedback targets. Markers restore to the same element even when the UI changes.
<section data-report-id="hero" data-report-type="group">
<button data-report-id="hero-cta">Get Started</button>
</section>- data-report-type defaults to item (buttons, links, etc.).
- Use data-report-type="group" for section- or card-level targets.
First feedback
You're ready — leave feedback right away. Click Add feedback in the panel or use a shortcut.
| Action | Mac | Windows / Linux |
|---|---|---|
| Leave feedback | ⌘⇧M | Ctrl+Shift+M |
| View feedbacks | ⌘⇧L | Ctrl+Shift+L |
| Preview elements | ⌘⇧E | Ctrl+Shift+E |
In report mode, click any element to open the feedback composer.
In view mode (⌘⇧L), browse markers and the feedback list.
What is fivepixels?
fivepixels lets teams leave feedback on real DOM elements, restore markers after UI changes, and promote issues to GitHub when needed.
- data-report-id — restores marker position via querySelector even when the UI changes.
- Shadow Root UI — panels, overlays, and markers are isolated from host app CSS.
- localStorage or server — omit handlers for browser storage, or pass onList/onCreate/onUpdate for API persistence.
UI Modes
| Mode | Entry | Purpose |
|---|---|---|
| idle | Default | Choose Report, View, or element preview from the panel |
| report | Add feedback · ⌘⇧M | Click elements on screen to leave feedback |
| view | View feedbacks · ⌘⇧L | Browse markers, reply, review, promote to GitHub Issue |
Reply review flow (denied / confirm / checkout) is documented in Feedback Workflow below.
Full Example
Connect project, ui, visibility, team, fields, persistence handlers, side effects, and GitHub in one place. Omit onList/onCreate/onUpdate/onDelete for localStorage-only mode.
visibility.devOnly hides the UI in production builds — ideal for staging-only QA tools.
UI Architecture
FivePixels UI renders in an open Shadow Root (#fivepixels-root), isolated from host app styles.
document.body
└── #fivepixels-root
└── #shadow-root (open)
├── <style> ← purged Tailwind CSS
└── FivePixels UI (panel, overlay, markers)- Target discovery still uses main document querySelector / elementFromPoint.
- Host Shadow DOM internals are not feedback targets by default.
- appearance light | dark | system maps to data-fivepixels-theme inside Shadow Root.
Styling
Default UI uses Tailwind utility classes. Package consumers do not import CSS files.
| Purpose | Path |
|---|---|
| Feedback composer & thread | src/components/panel/feedback/*.tsx |
| Component layout & colors | className in src/components/**/*.tsx |
| Tailwind theme | src/styles/tailwind.css |
| Shadow Root CSS bundle | src/styles/reportStylesheet.ts (build output) |
Config
import { FivePixels } from "@fivepixels-js/react";
export default function App() {
return (
<FivePixels
project={{
id: "multimachine-ceo",
env: "stage",
version: "1.2.3",
}}
ui={{ appearance: "system" }}
/>
);
}Advanced (localStorage only)
<FivePixels
project={{ id: "my-app" }}
ui={{
appearance: "system",
showFeedbackList: false,
visibleShortcutKeys: true,
locale: "ko",
}}
visibility={{ devOnly: true }}
team={{
user: { id: "user-1", name: "Reviewer A" },
reviewers: [
{ id: "1", name: "Reviewer A" },
{ id: "2", name: "Reviewer B" },
],
}}
fields={[
{ key: "message", type: "textarea", label: "Message", required: true },
{ key: "isBug", type: "checkbox", label: "bug" },
{ key: "isImportant", type: "checkbox", label: "IMPORTANT" },
]}
/>| Prop | Description |
|---|---|
| project | { id?, env?, version? } — id defaults to my-app |
| ui | { appearance?, showFeedbackList?, visibleShortcutKeys?, locale?, messages? } |
| visibility | { enabled?, devOnly?, routeKey? } |
| team | { user?, reviewers? } — default author and reviewer list |
| fields | Feedback form fields (textarea, checkbox tags) |
| onList / onCreate / onUpdate / onDelete | Server persistence handlers |
| onEvent / onReply | Post-save side effects (analytics, Slack) |
| github | GitHub Issue promotion — enabled, modes, onCreate |
Keyboard Shortcuts
| Action | Mac | Windows / Linux |
|---|---|---|
| Leave feedback / stop selection | ⌘⇧M | Ctrl+Shift+M |
| Preview selectable elements | ⌘⇧E | Ctrl+Shift+E |
| View feedbacks / close list | ⌘⇧L | Ctrl+Shift+L |
| Focus search (list open) | ⌘⇧S | Ctrl+Shift+S |
| Move list items | ↑ / ↓ | ↑ / ↓ |
| Cancel draft / close / exit mode | Esc | Esc |
| Save draft / save edit | ⌘↩ | Ctrl+Enter |
<FivePixels
project={{ id: "my-app" }}
ui={{ visibleShortcutKeys: true }}
visibility={{ devOnly: true }}
/>Which storage option?
Without handlers, fivepixels uses localStorage. With onList/onCreate/onUpdate, it uses your server. Use the table below to pick a starting point.
| Situation | Recommended path | Section |
|---|---|---|
| Solo demo or PoC | localStorage | Start with localStorage |
| Team reviews on staging | Server or hosted | Use with a team |
| Store on your own server | Self-hosted backend | Build your backend |
Start with localStorage
Without handler props, fivepixels uses browser localStorage. Keys are scoped by project.id and project.env.
<FivePixels project={{ id: "my-app" }} />Data transfer (localStorage only)
| Feature | Description |
|---|---|
| Import | Bulk import via JSON file or drag-and-drop |
| Export | Export current scope as JSON |
| Command | Paste JSON to replace or merge with conflict confirmation |
localStorage is per browser and device.
Team sharing and staging handoff require server-backed storage.
Use with a team
When QA, PM, and designers review on the same staging build, you need shared storage. localStorage splits data per browser.
- Separate data by project and environment (stage/production)
- Everyone sees the same feedback list and markers
- Share reply and review workflows across the team
Options
- fivepixels hosting (coming soon) — one API key, no backend to build
- Self-hosted backend — store on your server via v2 handlers (onList, onListReplies, onCreate, onCreateReply, onUpdate)
Required handlers must be implemented together. v2 adds onListReplies and onCreateReply for lazy-loaded reply threads.
Build your backend
The frontend talks to your backend through handlers. REST paths can follow your conventions, but request/response JSON must match the data contract.
| Priority | Handler | Suggested REST | Purpose |
|---|---|---|---|
| Required | onList | GET /comments?pathname={pathname} | Feedback summaries for current page |
| Required | onListReplies | GET /comments/{id}/replies | Thread — on marker click |
| Required | onCreate | POST /comments | Create feedback |
| Required | onCreateReply | POST /comments/{id}/replies | Create single reply |
| Required | onUpdate | PATCH /comments/{id} | Edit body, status, integrations |
| Optional | onDelete | DELETE /comments/{id} | Delete feedback |
| Optional | onListAll | GET /comments?cursor=&limit=100 | All pages, summarized |
<FivePixels
project={{ id: "my-app", env: "stage", version: "1.2.0" }}
onList={({ pathname }) =>
fetch(
`/api/projects/my-app/comments?pathname=${encodeURIComponent(pathname)}&environment=stage`,
).then((r) => r.json())
}
onListReplies={(commentId) =>
fetch(`/api/projects/my-app/comments/${commentId}/replies`).then((r) => r.json())
}
onCreate={(payload) =>
fetch("/api/projects/my-app/comments", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
}).then((r) => r.json())
}
onCreateReply={(commentId, payload) =>
fetch(`/api/projects/my-app/comments/${commentId}/replies`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
}).then((r) => r.json())
}
onUpdate={(id, payload) =>
fetch(`/api/projects/my-app/comments/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
}).then((r) => r.json())
}
/>- onList returns reply_count and latest_reply only — no replies[].
- onListReplies(commentId) lazy-loads the thread on marker click.
- Replies are saved via onCreateReply single POST.
- Implement onDelete if the UI delete action is required.
GitHub Issue
Promote feedback to GitHub Issues when ready. Call the GitHub API from your app server via github.onCreate.
| Field | Description |
|---|---|
| enabled | Hide Git Issue buttons when false |
| modes | "on-create" (composer) and/or "from-list" (list item) |
| onCreate | Server callback returning { issueNumber, issueUrl } |
After promotion, feedback status becomes git_issued and a system reply with the issue link is appended.
Feedback Workflow (view mode)
In view mode, markers (red item · purple group) drive the reply and review flow.
- Create — select element, message, author, checkbox tags.
- Marker badge — +N when reply_count ≥ 1.
- Hover — status badge, message, author, tags; latest reply preview at bottom.
- Click — onListReplies loads the thread, then opens reply composer (no tags on replies).
- First reply — suggested timeline entry with denied / confirm / select.
- denied — opens composer; sends found_error reply.
- checkout — on latest found_error; sends suggested reply.
- confirm — adds resolved reply; feedback status becomes resolved.
| Reply status | UI label | Meaning |
|---|---|---|
| suggested | SUGGESTED | Proposed fix or answer |
| found_error | FOUND ERROR | Review rejection — needs rework |
| resolved | RESOLVED | Review complete — issue closed |
Data Contract
- ReportField types: textarea, checkbox.
- field_values: Record<string, string | boolean>.
- Replies are stored chronologically in the replies table; latest is the last item.
- Feedback status flow: open → git_issued → resolved → archived.
- integrations.github: { issue_number, issue_url, issued_at }.
Migration
- Use project={{ id, env, version }} instead of flat projectId / environment / appVersion.
- Use project, ui, visibility objects instead of flat deprecated props.
- Replace workspace with project.id.
- Use onList/onCreate/onUpdate/onDelete instead of removed storageAdapter.
- Use onEvent/onReply for analytics instead of legacy side-effect handlers.
- localStorage key: fivepixels:reports:v1:{projectId} or with :{environment} suffix.