A real-time collaborative document editor built with React 19, TipTap, Yjs, and Liveblocks.
- Rich text editing — bold, italic, underline, strikethrough, font family, font size, text colour, highlight, headings, bullet and ordered lists, text alignment
- Images — insert local images via file picker
- Tables — insert and manage tables with contextual row/column controls
- Real-time collaboration — up to 5 users per document with live cursors and named presence
- Auto-save — document state persisted automatically via Liveblocks (debounced 1s after last edit)
- Share via link — each document has a unique URL; share it to invite collaborators
- Static header and footer — displays document title and date (header), doc ID and page number (footer)
- Accessible toolbar — SVG icons with tooltips on every button, ARIA roles and labels throughout, keyboard-navigable
| Concern | Library |
|---|---|
| UI framework | React 19 + Vite 6 |
| Rich text editor | TipTap 3 (ProseMirror) |
| Collaboration (CRDT) | Yjs |
| Real-time relay + persistence | Liveblocks |
| Routing | React Router 7 |
| Styling | CSS Modules |
| Testing | Vitest + React Testing Library |
- Node.js 18+
- A Liveblocks account (free tier is sufficient)
git clone <repo-url>
cd google-doc
npm installCreate a .env.local file in the project root:
VITE_LIVEBLOCKS_PUBLIC_KEY=pk_your_key_here
Get your public key from the Liveblocks dashboard under API Keys.
npm run devOpen http://localhost:5173. Create a new document from the home page, or navigate directly to /doc/<any-id>.
Copy the URL from your browser and send it to a collaborator. Up to 5 users can edit the same document simultaneously.
| Command | Description |
|---|---|
npm run dev |
Start the Vite dev server |
npm run build |
Production build |
npm run preview |
Preview the production build locally |
npm test |
Run tests in watch mode |
npm run coverage |
Run tests with V8 coverage report |
src/
├── components/
│ ├── AlignmentGroup/ Text alignment buttons
│ ├── ColorSwatch/ Text colour and highlight pickers
│ ├── DocFooter/ Static page footer
│ ├── DocHeader/ Static page header
│ ├── EditorProvider/ Wires editor, presence, auto-save
│ ├── FontFamilySelect/ Font family dropdown
│ ├── FontSizeSelect/ Font size dropdown
│ ├── HeadingSelect/ Heading level dropdown
│ ├── ImageButton/ Image file picker
│ ├── ListButtons/ Bullet and ordered list toggles
│ ├── PageCanvas/ A4-style document canvas
│ ├── PresenceBar/ Connected users and sync status
│ ├── SaveStatus/ Auto-save indicator
│ ├── TableControls/ Insert and manage tables
│ ├── Toolbar/ Full formatting toolbar
│ ├── ToolbarButton/ Accessible toggle button primitive
│ ├── ToolbarDivider/ Visual separator
│ └── icons/ Inline SVG icon components
├── extensions/
│ └── fontSize.js Re-exports FontSize from @tiptap/extension-text-style
├── hooks/
│ ├── useAutoSave.js Debounced save-status from Yjs update events
│ ├── useCollabEditor.js TipTap editor with all extensions wired up
│ ├── useDoc.js Yjs doc + LiveblocksYjsProvider lifecycle
│ └── useIdentity.js Persistent random display name + colour
├── lib/
│ └── liveblocks.js Liveblocks client and room context
├── pages/
│ ├── EditorPage/ Document route (/doc/:docId)
│ ├── HomePage/ Landing page with new-doc button
│ └── RoomFullPage/ Shown when > 5 users join a room
└── test/
└── setup.js Vitest + jest-dom setup
All source files maintain 100% coverage (statements, branches, functions, lines) enforced via Vitest's V8 provider with thresholds set in vite.config.js.
All files | 100% Stmts | 100% Branch | 100% Funcs | 100% Lines
- Images are session-local. Inserted images use
URL.createObjectURL, which means they are not synced to other collaborators and do not survive a page refresh. A backend storage layer would be required for persistent image sharing. - Identity is browser-local. Each user's display name and cursor colour are generated randomly on first visit and stored in
localStorage. Clearing storage resets the identity. - Room capacity is enforced client-side at 5 users via Liveblocks presence. This is a soft limit suitable for a prototype.