Markup

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/markup
import { 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 apiUrl and apiKey in 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

  1. Drop the snippet from Path A into a blank index.html.
  2. Open the file with a local server (e.g. npx serve .) — localhost is auto-allowed.
  3. Click the floating button in the bottom-right.
  4. Click anywhere on the page → write a comment → submit.
  5. 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

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

Existing threads stay in the dashboard — uninstalling the widget doesn't delete data.