This library is currently in development.
agit
agit
QUICK START

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-dom
Get Started
01 / INSTALL

Install

Install the package in your React 18+ project.

bash
npm install @fivepixels-js/react react react-dom

In Next.js and other SSR apps, render <FivePixels /> on the client only ("use client" + dynamic import).

02 / MOUNT

Mount

Render <FivePixels /> once on the screen that receives feedback. No CSS import required.

tsx
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.
03 / MARK

Mark elements

Add data-report-id to feedback targets. Markers restore to the same element even when the UI changes.

tsx
<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.
04 / TRY IT

First feedback

You're ready — leave feedback right away. Click Add feedback in the panel or use a shortcut.

ActionMacWindows / Linux
Leave feedback⌘⇧MCtrl+Shift+M
View feedbacks⌘⇧LCtrl+Shift+L
Preview elements⌘⇧ECtrl+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

ModeEntryPurpose
idleDefaultChoose Report, View, or element preview from the panel
reportAdd feedback · ⌘⇧MClick elements on screen to leave feedback
viewView feedbacks · ⌘⇧LBrowse 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.

text
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.

PurposePath
Feedback composer & threadsrc/components/panel/feedback/*.tsx
Component layout & colorsclassName in src/components/**/*.tsx
Tailwind themesrc/styles/tailwind.css
Shadow Root CSS bundlesrc/styles/reportStylesheet.ts (build output)

Config

tsx
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)

tsx
<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" },
  ]}
/>
PropDescription
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
fieldsFeedback form fields (textarea, checkbox tags)
onList / onCreate / onUpdate / onDeleteServer persistence handlers
onEvent / onReplyPost-save side effects (analytics, Slack)
githubGitHub Issue promotion — enabled, modes, onCreate

Keyboard Shortcuts

ActionMacWindows / Linux
Leave feedback / stop selection⌘⇧MCtrl+Shift+M
Preview selectable elements⌘⇧ECtrl+Shift+E
View feedbacks / close list⌘⇧LCtrl+Shift+L
Focus search (list open)⌘⇧SCtrl+Shift+S
Move list items↑ / ↓↑ / ↓
Cancel draft / close / exit modeEscEsc
Save draft / save edit⌘↩Ctrl+Enter
tsx
<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.

SituationRecommended pathSection
Solo demo or PoClocalStorageStart with localStorage
Team reviews on stagingServer or hostedUse with a team
Store on your own serverSelf-hosted backendBuild your backend

Start with localStorage

Without handler props, fivepixels uses browser localStorage. Keys are scoped by project.id and project.env.

tsx
<FivePixels project={{ id: "my-app" }} />

Data transfer (localStorage only)

FeatureDescription
ImportBulk import via JSON file or drag-and-drop
ExportExport current scope as JSON
CommandPaste 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 own backend guide

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.

PriorityHandlerSuggested RESTPurpose
RequiredonListGET /comments?pathname={pathname}Feedback summaries for current page
RequiredonListRepliesGET /comments/{id}/repliesThread — on marker click
RequiredonCreatePOST /commentsCreate feedback
RequiredonCreateReplyPOST /comments/{id}/repliesCreate single reply
RequiredonUpdatePATCH /comments/{id}Edit body, status, integrations
OptionalonDeleteDELETE /comments/{id}Delete feedback
OptionalonListAllGET /comments?cursor=&limit=100All pages, summarized
tsx
<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.
Backend API guide — REST details, DB schema, checklist

GitHub Issue

Promote feedback to GitHub Issues when ready. Call the GitHub API from your app server via github.onCreate.

FieldDescription
enabledHide Git Issue buttons when false
modes"on-create" (composer) and/or "from-list" (list item)
onCreateServer 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.

  1. Create — select element, message, author, checkbox tags.
  2. Marker badge — +N when reply_count ≥ 1.
  3. Hover — status badge, message, author, tags; latest reply preview at bottom.
  4. Click — onListReplies loads the thread, then opens reply composer (no tags on replies).
  5. First reply — suggested timeline entry with denied / confirm / select.
  6. denied — opens composer; sends found_error reply.
  7. checkout — on latest found_error; sends suggested reply.
  8. confirm — adds resolved reply; feedback status becomes resolved.
Reply statusUI labelMeaning
suggestedSUGGESTEDProposed fix or answer
found_errorFOUND ERRORReview rejection — needs rework
resolvedRESOLVEDReview 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.
fivepixels