Most web pages go through this:
Writer → Google Doc → Designer → Figma → 2-3 revision rounds → Dev → more revision rounds
Three people. Four tools. Every transition is a manual content transcription: doc to Figma text layers to JSX strings. A single copy edit means touching three tools and three people. The writer can't see it rendered. The designer reviews a mockup, not the real page. The developer gets messy input and spends days cleaning it up before they can publish.
Chalk fixes the input:
Staging page → Writer edits inline → Designer comments on the real page → AI applies → Dev imports and ships
The staging page replaces both the Google Doc and the Figma. The writer edits real copy in real layout. The designer reviews the actual rendered page, not a mockup. By the time dev gets the content, it's final: reviewed, refined, and structured as clean JSON they can import directly. Hours to publish, not days.
Chalk is a React component library that turns any staging page into this surface. Store your content as JSON with TextElement { id, text } fields. Chalk adds inline editing, anchored comments, and AI review on top.
Not a CMS. Not a page builder. A thin overlay that gives the writer and designer a shared surface, and gives the developer clean input.
npm install chalk-edit{
"pageId": "landing",
"hero": {
"h1": { "id": "landing.hero.h1", "text": "Ship faster with fewer meetings" },
"body": { "id": "landing.hero.body", "text": "Stop transcribing copy between tools." }
}
}Every text field is a TextElement { id, text }. That's the only contract.
import { ChalkProvider, ChalkToolbar, CommentPanel, ReviewPanel, ChalkText } from "chalk-edit/react";
export default function LandingPage({ content }) {
return (
<ChalkProvider pageId="landing">
<ChalkText element={content.hero.h1} tag="h1" className="text-4xl font-bold" />
<ChalkText element={content.hero.body} tag="p" className="text-lg" />
<ChalkToolbar />
<CommentPanel />
<ReviewPanel />
</ChalkProvider>
);
}// src/app/api/chalk/content/route.ts
import { createContentHandler } from "chalk-edit/next";
const { GET, PUT } = createContentHandler({
contentDir: "src/content",
pageIdToSlug: (pageId) => pageId,
});
export { GET, PUT };// src/app/api/chalk/comments/route.ts
import { createCommentsHandler } from "chalk-edit/next";
const { GET, POST, PATCH, DELETE } = createCommentsHandler({
chalkDir: "src/content/.chalk",
});
export { GET, POST, PATCH, DELETE };// src/app/api/chalk/review/route.ts
import { createReviewHandler } from "chalk-edit/next";
const { GET, POST } = createReviewHandler({
contentDir: "src/content",
chalkDir: "src/content/.chalk",
});
export { GET, POST };Done. Your staging page is now an editing surface.
Three modes, toggled from a floating toolbar at the bottom of the page.
View -- Normal page. No editing UI. What the visitor sees.
Edit -- Every ChalkText becomes contentEditable. Click, type, save. The writer works on the real rendered page, not a doc.
Comment -- Click any text to leave a comment anchored to that specific element. The designer comments on real rendered copy in real layout, not a Figma approximation. Comments appear in a right sidebar with timestamps, resolve/reopen, and delete.
When the designer leaves comments, one button sends them all for AI review. Chalk does not call any AI API itself. Instead:
- Chalk saves a review request to disk (open comments + content snapshot).
- An external tool (Claude Code, a CI script, whatever) reads the request, reasons about all comments together, and writes edits back.
- Chalk polls for the edits and shows them inline with accept/reject per edit.
The AI sees every comment in context and produces coherent edits across the page. No more one-at-a-time revision rounds. The prompt is yours to build. Chalk gives you collectText() to flatten content into a map, and the comment data to work with.
The workflow problem isn't speed. It's the number of translations. Content gets written once and transcribed four times: doc, Figma layers, JSX, then back through the loop for revisions. Each translation introduces drift. The designer sees a mockup that doesn't match the build. The writer sees a doc that doesn't match the design. The developer gets ambiguous input and spends time reconciling what the writer meant, what the designer mocked up, and what's actually buildable.
Chalk eliminates the translations before dev. Every edit happens on the actual page. The JSON content file is the single source of truth. When the writer changes a headline, it's rendered immediately in the real layout. When the designer comments on spacing, they're looking at real CSS, not a Figma approximation. By the time the developer receives the content, it's been edited and reviewed on the real page. They import structured JSON, review it in their own environment, refine, and publish.
Pure functions. No React, no Node, no side effects.
import { findElement, updateElement, collectText, getAllElementIds } from "chalk-edit/core";
import { elementId, commentId, te, sectionFromId, pageFromId } from "chalk-edit/core";
import { TextElementSchema, CommentSchema, ReviewEditSchema } from "chalk-edit/core";| Function | What it does |
|---|---|
findElement(obj, id) |
Find a TextElement by ID anywhere in a nested object |
updateElement(obj, id, text) |
Update a TextElement's text by ID. Mutates in place. |
collectText(obj) |
Collect all TextElements into { id: text } map |
getAllElementIds(obj) |
Get all TextElement IDs from a nested object |
elementId(pageId, ...path) |
Generate a deterministic element ID |
commentId() |
Generate a random comment ID |
te(pageId, path, text) |
Shorthand to create a TextElement |
import {
ChalkProvider, // Context provider. Wraps your page.
ChalkText, // Smart renderer: editable in edit mode, plain otherwise.
ChalkToolbar, // Floating toolbar: mode toggle, save, review.
CommentPanel, // Right sidebar for comments.
ReviewPanel, // AI edit suggestions panel.
EditableText, // Low-level contentEditable wrapper.
useChalk, // Hook to access Chalk state and actions.
} from "chalk-edit/react";Chalk uses CSS custom properties. Set them on any parent element:
:root {
--chalk-accent: #2563eb;
--chalk-accent-hover: #1d4ed8;
--chalk-text: #1D2939;
}Factory functions that return Next.js route handlers.
| Factory | Returns | Options |
|---|---|---|
createContentHandler(opts) |
{ GET, PUT } |
contentDir, pageIdToSlug?, devOnly? |
createCommentsHandler(opts) |
{ GET, POST, PATCH, DELETE } |
chalkDir |
createReviewHandler(opts) |
{ GET, POST } |
contentDir, chalkDir, pageIdToSlug? |
Content writes are dev-only by default. Set devOnly: false to allow in production.
| Key | Action |
|---|---|
| V | View mode |
| E | Edit mode |
| C | Comment mode (toggles panel) |
| S | Save (edit mode, when dirty) |
| R | Request AI review (comment mode, with open comments) |
Shortcuts appear as tooltips when you hover any toolbar button.
Marker solves the same workflow problem for slide decks. Chalk solves it for web pages. Both share the same primitives (TextElement, Comment, ReviewEdit) and the pattern of element-level comments processed by AI in a single pass.
Marker is a CLI that owns the full rendering pipeline. Chalk is a component library that layers onto your existing pages.
MIT