redlining

Getting started

Two minutes from a fresh project to the first applied annotation. Everything here is development-only; nothing changes in your production build.

Install

pnpm add -D redlining
npx redlining init

redlining init is idempotent. It writes three things and skips whatever already exists:

If there is no app/ directory it prints what to create instead of guessing.

Set up with an agent

Paste this into your coding agent (Claude Code or any other) inside the repository. It installs, wires, initialises and verifies Redlining, and reports back without committing.

Setup prompt
Set up Redlining in this repository. Redlining is a development-only overlay for Next.js (and Vite + React) that lets me annotate the running app; the annotations become a file-anchored spec you apply with the /redline command. Do exactly the steps below, verify, and report. Do not commit.

1. Detect the setup. Look for next.config.* or vite.config.*, and for the lockfile to learn the package manager (pnpm-lock.yaml, yarn.lock, package-lock.json, bun.lockb). Use that package manager for everything; never switch it.

2. Install: add `redlining` as a dev dependency (e.g. `pnpm add -D redlining`). If the registry does not have the package, stop and tell me instead of improvising.

3. Wire it in.
   Next.js (16 or newer, React 19):
   - In next.config.*, wrap the exported config with `withRedlining` from 'redlining/next'. Preserve everything already there; if the config is a function, pass the function to withRedlining unchanged.
   - In the ROOT layout (app/layout.tsx or src/app/layout.tsx, not a nested layout), import { Redlining } from 'redlining' and render <Redlining /> as the last child of <body>.
   Vite + React:
   - In vite.config.*, add `redlining()` from 'redlining/vite' as the first plugin.
   - Render <Redlining /> once in the root component.

4. Run `npx redlining init`. It writes app/api/redlining/route.ts (or src/app/...), a `.redlining/` line in .gitignore, and .claude/commands/redline.md, and skips anything that already exists. If it reports that there is no app/ directory (Vite, Pages Router), do not invent a route; the overlay's "Copy prompt" works without one.

5. If the components that should be annotatable live outside app/, components/ and src/, pass the directories as `{ include: [...] }` in the second argument of withRedlining (or redlining()).

6. Verify, then stop the server:
   - Start the dev server, fetch one page, and confirm the HTML contains `data-rl="…"` attributes on host elements such as <nav> or <button>.
   - Run the production build and confirm its output contains no `data-rl` attribute.
   - Next.js only: POST to /api/redlining with body {"session":{"route":"/","url":"http://localhost:3000/","viewport":{"w":1,"h":1},"annotations":[]}} while the dev server runs; expect 200 and .redlining/annotations.md to appear. Delete .redlining/ afterwards.

7. Report: the files you changed, the include list in effect, how to open the overlay (Alt+R in the running app), and anything you could not verify and why.

Afterwards, press Alt+R in the running app, annotate, save, and run /redline.

Next.js setup

Requires Next.js 16 or newer and React 19. Both the default Turbopack dev server and next dev --webpack are supported.

// next.config.ts
import { withRedlining } from 'redlining/next'

export default withRedlining({
  // your config
})
// app/layout.tsx
import { Redlining } from 'redlining'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <Redlining />
      </body>
    </html>
  )
}

withRedlining() returns a config function so it can see the phase: the loader is registered only while the dev server runs. Pass a function config of your own and it is composed, sync or async. By default files under app/, components/ and src/ get anchors; change that with the include option (see Options).

Monorepo? Turbopack's root follows the lockfile, which may be above your app. Redlining matches files by a path pattern rather than a root-relative glob, so app/** inside apps/web/ still works.

Vite setup

// vite.config.ts
import react from '@vitejs/plugin-react'
import { redlining } from 'redlining/vite'
import { defineConfig } from 'vite'

export default defineConfig({ plugins: [redlining(), react()] })

Mount <Redlining /> in your root component. The plugin runs only under vite dev and stamps files under src/ by default. Pass redlining({ endpoint: true }) to serve the save endpoint from the dev server at /api/redlining (it writes to .redlining/ and reads reply.md like the Next.js route); without it, Save downloads the files.

Other stacks: Angular, Vue, plain HTML

Everything that reads the DOM works on any page: select, draw, move, tweak with provenance and class hints, notes with reference images, the Markdown export. What needs a framework is the exact file:line (JSX is stamped by the loader; HTML templates are not yet, so those anchors are selector-only) and the component names (React's owner chain; Angular's ng.getComponent in dev mode; Vue 3's instance names).

<!-- dev only: React is bundled, ~155 KB gzipped -->
<script type="module" src="/node_modules/redlining/dist/standalone.js"
        data-redlining data-endpoint="off"></script>
// or from code
import { mount } from 'redlining/standalone'
const unmount = mount({ endpoint: false, hotkey: 'Alt+R' })

data-endpoint="off" (or endpoint: false) makes Save download annotations.md, annotations.json and the images; the browser may ask once before allowing several downloads. Move them into .redlining/ and run /redline. Point data-endpoint at a path instead when your dev server hosts the handler (createHandlers from redlining/next/route takes a Fetch Request; the Vite plugin shows how to adapt Node's).

Your first session

  1. Start the dev server and open a page. A small pen button sits bottom-right (Next DevTools keeps bottom-left).
  2. Press Alt+R, or click the pen. The toolbar appears and the cursor becomes a crosshair.
  3. Hover anything. The smallest host element under the cursor is outlined, with a badge such as MainNav · app/(app)/layout.tsx:42 · <nav>. Press [ to select the parent instead, ] to come back down; +scroll does the same.
  4. Click. A numbered pin drops and a note popover opens with the textarea focused. Choose Change or Remove, type the note, press to save (+ for a newline, Esc to cancel).
  5. Add more. Draw mode (D) for something that does not exist yet, Move mode (M) for something that should live elsewhere. The guide covers each.
  6. Review with L: the list panel shows every annotation; click a note to edit it, the × to delete.
  7. Save with + (or the green send button). A toast confirms: Saved — run /redline, or hand .redlining/ to your agent.

Applying with your agent

In Claude Code, in the same project:

/redline

The command tells the agent to read .redlining/annotations.md, look at the screenshot if there is one, apply every annotation in order at the exact anchor, reuse existing components and design tokens, change nothing that is not listed, verify in the running app, report done / partial / skipped per annotation, write that report to .redlining/reply.md, and finally delete the export files. The file may hold several routes when you annotated more than one page.

Your session in the browser is not deleted by that. Hot reload shows the result, and the next time you open the overlay it reads the reply, checks every annotation against the page and opens the list with the verdicts: applied, differs · font-size is 15px, expected 16px, missing or check by eye, next to what the agent said. Remove applied clears what landed; adjust the rest and save again.

Options

<Redlining
  endpoint="/api/redlining"   // where Save posts; false downloads the files
  hotkey="Alt+R"              // e.g. "Ctrl+Shift+K"
  position="bottom-right"     // bottom-left | top-right | top-left
  theme="light"               // or "dark"
  maxAnnotations={15}         // a warning shows at 10
  screenshot                  // include screenshot.png with burned-in pins
  framework="tailwind4"       // tailwind4 | tailwind3 | css-modules | css; detected when omitted
/>
withRedlining(config, {
  include: ['app', 'components', 'src'],
})
// app/api/redlining/route.ts — instead of the default re-export
import { createHandlers } from 'redlining/next/route'
export const { GET, POST } = createHandlers({ outDir: '.redlining', maxBytes: 16 * 1024 * 1024 })

The route refuses requests outside development, caps the body at 16 MB, and only ever writes inside outDir, which must be inside the project.