Markup widget — install & use
A step-by-step tutorial for getting the @pixelmatters/markup widget running in any web app. Designed to be readable end-to-end by engineers, designers, product managers, and LLMs.
What you get: a floating button in the corner of your app. Anyone who opens the page can click it, drop a pin anywhere, and leave a threaded comment (with optional annotated screenshot). Threads stream into the Markup dashboard in real time.
1. What you need before you start
| You need | Where to get it |
|---|---|
| A Markup account | Markup dashboard — sign in with Google |
| A Project | Dashboard → + New project |
| An API key | Project → Settings → API Keys → New key (the raw key is shown once — copy it) |
| Your API URL | Project → Settings → Install — looks like https://<your-deployment>.convex.site |
| (Production) Your host domain | Project → Settings → Domains — add app.example.com, *.staging.example.com, etc. localhost is allowed automatically in dev |
You'll plug apiUrl and apiKey into the widget. That's it — there's no global CSS to import and no provider to wrap your app in.
2. AI prompt — paste into your assistant
If you're using Claude, ChatGPT, Cursor, or any other LLM, paste the block below. It's self-contained and gives the model exactly what it needs to wire the widget into your codebase. Skip ahead to section 3 if you'd rather install by hand.
You are helping me install the **`@pixelmatters/markup`** feedback widget into my web app.
## What it is
A drop-in feedback widget published on npm as `@pixelmatters/markup`. It mounts a floating action button that lets users pin threaded comments (and optional annotated screenshots) anywhere on the page. It runs inside a shadow DOM so it doesn't affect host CSS.
## My credentials
- `apiUrl`: `https://<MY_DEPLOYMENT>.convex.site` ← replace with the value from Markup dashboard → Settings → Install
- `apiKey`: `markup_...` ← replace with a key from Markup dashboard → Settings → API Keys
Store these in environment variables (e.g. `VITE_MARKUP_API_URL`, `VITE_MARKUP_API_KEY`, or the equivalent for my framework). Do not hardcode them.
## API
```ts
import { init, destroy } from '@pixelmatters/markup'
// or:
import { MarkupWidget } from '@pixelmatters/markup/react'
init({
apiUrl: string, // required
apiKey: string, // required
position?: 'bottom-right' | 'bottom-left', // default 'bottom-right'
theme?: 'light' | 'dark' | 'auto', // default 'auto'
}) // returns a destroy() function — call it on unmount / logout / route teardown
```
For a `<script>` tag drop-in (no bundler), use:
```html
<script
type="module"
src="https://unpkg.com/@pixelmatters/markup"
data-markup-widget="true"
data-api-url="..."
data-api-key="..."
data-position="bottom-right"
></script>
```
The `data-markup-widget="true"` attribute is required.
## Your task
1. Detect my framework (React, Vue, Svelte, Next.js, plain HTML, etc.) by inspecting the project.
2. Install `@pixelmatters/markup` with the package manager already in use (pnpm/npm/yarn).
3. Wire the widget into the **root layout / app shell** so it shows on every page.
4. Read `apiUrl` and `apiKey` from environment variables; create `.env.example` entries and update `.gitignore` if needed.
5. For SPAs, ensure the widget is mounted once at the root (not per route) and unmounted via `destroy()` on logout.
6. Show me a diff of the changes and a one-line note on how to verify (e.g. "run dev server, click the button in the bottom-right").
Constraints:
- Do **not** add CSS imports or provider components — the widget needs neither.
- Do **not** hardcode the key.
- If the project has a CSP, add `https://unpkg.com` to `script-src` only if I'm using the `<script>` tag path.3. Pick an install path
Three ways to add the widget. Pick the one that matches your stack.
Path A — Drop-in <script> tag (no build step)
Best for static sites, marketing pages, Webflow, WordPress, or any HTML you can edit directly.
Paste this just before </body>:
<script
type="module"
src="https://unpkg.com/@pixelmatters/markup"
data-markup-widget="true"
data-api-url="https://your-deployment.convex.site"
data-api-key="markup_..."
data-position="bottom-right"
></script>The data-markup-widget="true" attribute is required — it's how the bootstrap finds its own <script> tag.
Path B — Vanilla JS / TypeScript (any bundler)
# pnpm
pnpm add @pixelmatters/markup
# yarn
yarn add @pixelmatters/markup
# npm
npm install @pixelmatters/markupimport { init } from '@pixelmatters/markup'
const stop = init({
apiUrl: 'https://your-deployment.convex.site',
apiKey: 'markup_...',
position: 'bottom-right', // optional
theme: 'auto', // optional: 'light' | 'dark' | 'auto'
})
// Tear down on logout / SPA route change / unmount:
stop()Path C — React / Preact
import { MarkupWidget } from '@pixelmatters/markup/react'
export default function App() {
return (
<>
{/* your app */}
<MarkupWidget
apiUrl={import.meta.env.VITE_MARKUP_API_URL}
apiKey={import.meta.env.VITE_MARKUP_API_KEY}
position="bottom-right"
theme="auto"
/>
</>
)
}Tip — keep keys out of the repo. Store
apiUrlandapiKeyin environment variables (VITE_MARKUP_API_URL,VITE_MARKUP_API_KEY, etc.). The widget key is a public key (it's bound to your domain allowlist), but rotating it via env vars is still cleaner than committing it.
4. Configuration reference
| Option | Type | Default | Description |
|---|---|---|---|
apiUrl |
string |
required | Your Convex deployment site URL (https://*.convex.site) |
apiKey |
string |
required | Project API key minted in the dashboard |
position |
'bottom-right' | 'bottom-left' |
'bottom-right' |
Where the floating action button sits |
theme |
'light' | 'dark' | 'auto' |
'auto' |
'auto' follows the host's prefers-color-scheme |
init(config) is idempotent — calling it twice with the same config is a no-op; calling it with new values tears down the old instance first. It returns a destroy() function.
5. Try it — a 60-second smoke test
- Drop the snippet from Path A into a blank
index.html. - Open the file with a local server (e.g.
npx serve .) —localhostis auto-allowed. - Click the floating button in the bottom-right.
- Click anywhere on the page → write a comment → submit.
- Open your project in the dashboard — the thread is there.
If nothing appears, jump to Troubleshooting below.
6. How it works (in one diagram)
your app
│ embeds @pixelmatters/markup (Preact, runs inside an open shadow DOM)
▼
widget runtime ──► POST/GET /widget/* (x-markup-api-key + Origin) ──► Convex
│
▼
real-time dashboard- Style isolation: the widget mounts inside a shadow root (
:host { all: initial }). Your CSS can't bleed in; the widget's CSS can't bleed out. - Pin re-anchoring: every pin stores both a CSS selector and a viewport-fraction fallback, so it survives reflow and minor markup changes.
- SPA-aware: the widget patches
history.pushState/replaceStateand listens topopstate, so threads refresh on route changes. - Identity: anonymous by default; signed-in team members get a verified badge.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
401 Unauthorized in the network tab |
Wrong / revoked key | Mint a fresh key in Settings → API Keys |
403 origin not allowed |
Host domain isn't in the project's allowlist | Settings → Domains → add the domain (or *.staging.example.com) |
| Floating button doesn't appear | <script> tag missing data-markup-widget="true", or CSP blocks unpkg.com |
Add the attribute / allow the script origin in your CSP |
| Button works locally but not in prod | You're on a non-localhost domain that isn't allowlisted | Add the prod domain in Settings → Domains |
| Two widgets on the page | init() was called more than once with different configs |
Call the returned destroy() first, or just call init() again — it self-replaces |
8. Uninstalling / disabling
- Remove the
<script>tag, or stop callinginit(). - For React, unmount
<MarkupWidget />. - To kill an active session manually:
import { destroy } from '@pixelmatters/markup'; destroy().
Existing threads stay in the dashboard — uninstalling the widget doesn't delete data.