Skip to main content
RapidDev - Software Development Agency
V0 TemplatesDev ToolIntermediate to customize

Microsoft Paint Clone V0 Template: Canvas Drawing App Starter Kit

The Microsoft Paint Clone v0 template is an HTML5 canvas drawing app built with Zustand state management, a full tool selector (Pencil, Eraser, Line, Rectangle, Circle, Fill, Text), and a 28-color palette with a react-colorful custom picker. Fork it in 5 minutes to get a working drawing widget — useful as a whiteboard component, annotation tool, or signature pad embedded in any Next.js app. The flood fill performance bug is real and documented in Gotchas.

Dev ToolIntermediate~5 minutes

Best for

Developers learning canvas APIs or building a simple drawing widget (whiteboard, annotation tool, signature pad) inside a Next.js app.

Stack

Next.jsTailwind CSSHTML5 Canvas APIZustandreact-colorfulshadcn/ui

A ready-made Microsoft Paint Clone UI you can fork, run, and customize with the prompt pack below.

What's actually inside

The honest engineer's breakdown — what the Microsoft Paint Clonetemplate does, how it's wired, and where it's opinionated.

The template's core is the DrawingCanvas component — an HTML5 Canvas element with onMouseDown, onMouseMove, and onMouseUp handlers that track the current path and render strokes using the 2D context API. The ToolSelector provides seven drawing modes: Pencil (freehand), Eraser (background-colored stroke), Line, Rectangle, Circle, Fill (flood fill), and Text. Each mode changes how the canvas context interprets mouse events.

The ColorPalette renders 28 Paint-style preset colors in a grid, backed by react-colorful for custom color input beyond the presets. BrushSizeSlider controls strokeWidth with a live circle preview next to the slider handle. The UndoRedoStack is a Zustand slice that captures canvas ImageData snapshots — the key correctness detail is that snapshots must be taken on mouseup (one per completed stroke), not on mousemove (hundreds of events per drag, which floods the stack with near-identical frames).

ZoomControls scale the canvas context transform for working on fine details. The ExportBar has a Save Image button (canvas.toDataURL('image/png') → download link) and a Clear Canvas button. Honest caveat: the flood fill algorithm in most canvas Paint clones — including this template — is a naive recursive implementation that overflows the call stack and freezes the browser on a large canvas. The iterative fix is straightforward and documented in Gotchas.

Key UI components

DrawingCanvas

Main HTML5 Canvas with mouse event handlers tracking freehand paths and rendered strokes

ToolSelector

Toolbar with seven drawing modes: Pencil, Eraser, Line, Rectangle, Circle, Fill, Text

ColorPalette

28-color Paint-style preset grid plus react-colorful for custom color input

BrushSizeSlider

Numeric slider controlling strokeWidth with a live circle preview next to the handle

UndoRedoStack

Zustand slice storing canvas ImageData snapshots for per-stroke undo (Ctrl+Z) and redo (Ctrl+Y)

ZoomControls

Zoom in/out buttons scaling the canvas context transform for detail work

ExportBar

Save Image button (canvas.toDataURL → download) and Clear Canvas button

Libraries it leans on

Zustand

Manages undo/redo ImageData snapshot array, selected tool state, and current stroke color

react-colorful

Custom color input for colors beyond the 28-color preset ColorPalette

shadcn/ui

Slider (brush size), Tooltip (tool hints), Button, and Separator components

Fork it and get it running

No env vars or backend needed — this is a fully client-side HTML5 canvas app. Fork, verify the DrawingCanvas responds to mouse events, and publish.

1

Fork the template in V0

Open https://v0.dev/chat/community/T58xe0hGtYx and click the 'Fork' button in the top-right of the chat. V0 copies the template into your workspace and opens the code editor with the Preview panel visible on the right.

Tip: Sign in to v0.dev first. The free plan is sufficient to fork and deploy.

You should see: The Microsoft Paint Clone template opens in your V0 workspace.

2

Verify mouse drawing in the Preview

In the Preview tab, click the Pencil tool in the ToolSelector, then click and drag on the DrawingCanvas. You should see freehand strokes appear. Try switching to Rectangle mode — click and drag to confirm shape drawing. Click a color in the ColorPalette and verify the next stroke uses that color.

Tip: If the canvas is blank or unresponsive, see the Gotcha about the canvas ref not being attached on mount.

You should see: Freehand strokes appear on the DrawingCanvas when you click and drag with Pencil tool active.

3

Test the UndoRedoStack

Draw three separate strokes on the canvas (each with a distinct mousedown-drag-mouseup sequence). Press Ctrl+Z three times — each press should undo exactly one stroke. If undo jumps multiple steps, see the Gotcha about ImageData capture timing (snapshots should be taken on mouseup, not mousemove).

You should see: Ctrl+Z undoes one stroke at a time, step by step.

4

No env vars needed — skip the Vars panel

This template is fully client-side. Open the Vars panel and confirm it is empty. No backend, API keys, or environment variables are required for the base drawing app. If you later add Supabase Realtime for collaborative drawing, you will add NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY here.

You should see: Vars panel is empty; template functions without any configuration.

5

Deploy to Vercel

Click Share (top-right) → Publish tab → 'Publish to Production'. V0 builds and deploys to Vercel in 30–60 seconds. Open the live URL to verify the canvas drawing works outside the V0 sandbox, which sometimes restricts mouse events.

You should see: A live Vercel URL is returned; drawing works on the deployed version.

6

Export to GitHub to embed in a larger project

If you plan to embed the drawing canvas inside an existing app (a whiteboard component, annotation overlay, or signature pad), open the Git panel → Connect → enter a repo name. V0 creates branch v0/main-{hash} → open the PR on GitHub and merge to main. Then copy the DrawingCanvas component into your existing project's component tree.

You should see: GitHub repo created with the canvas code; ready for import into an existing Next.js project.

The prompt pack

Copy-paste these straight into v0's chat to customize the Microsoft Paint Clonetemplate. Each one names this template's own components — no generic filler.

1

Add a text tool with font controls

Adds a functional Text tool that lets users type directly on the canvas — one of the most commonly requested Paint features.

Quick win
Paste into v0 chat
When the Text tool in ToolSelector is active, clicking on the DrawingCanvas should place a positioned HTML textarea element overlaid on the canvas at the click coordinates using absolute CSS positioning. Style the textarea to be transparent with no border, matching the current color from ColorPalette as its text color. On the textarea's blur event or when the user presses Enter, call ctx.fillText() to render the text onto the DrawingCanvas at the original click coordinates, then remove the overlay textarea. Add a Text Size dropdown in the ToolSelector that controls the font size used in ctx.font.
2

Add a spray paint tool

Adds a spray paint effect that mimics aerosol can behavior — popular in Paint clones for texture and artistic effects.

Quick win
Paste into v0 chat
Add a Spray tool to the ToolSelector between Eraser and Line. When Spray is the active tool and the user holds the mouse button down, fire a setInterval at 30ms intervals. Each interval, scatter 20 random dots within a radius equal to the current BrushSizeSlider value: generate random offsets using Math.random() * radius and draw each dot as ctx.fillRect(x + randX, y + randY, 1, 1) using the current ColorPalette color. Clear the interval on mouseup. The effect should feel like actual spray paint — dense in the center, sparse at the edges.
3

Add a symmetry/mirror drawing mode

Adds symmetrical drawing modes that create kaleidoscope-like patterns from any freehand or shape stroke.

Medium
Paste into v0 chat
Add a Mirror toggle button in the ToolSelector (a horizontal flip icon). When Mirror mode is enabled, intercept every drawing event in the DrawingCanvas handlers: for each stroke point at (x, y) on the canvas, also draw an identical stroke at (canvasWidth - x, y) in the same operation. For circle and line tools, mirror the end point as well. Add a Vertical Mirror and Quad Mirror mode too — vertical mirrors at (x, canvasHeight - y), quad mirrors all four quadrants simultaneously. Show the active mirror mode as a highlighted badge on the Mirror button.
4

Replace ImageData undo with a command pattern

Reduces the undo stack's memory footprint from megabytes to bytes per step — critical if users draw hundreds of strokes in a session.

Medium
Paste into v0 chat
Refactor the UndoRedoStack Zustand slice from ImageData snapshots to a command pattern. Each draw action (a complete stroke from mousedown to mouseup) stores its parameters as a command object: { tool, points: [{x,y}], color, lineWidth, startX, startY, endX, endY }. Store these commands in an array in Zustand instead of ImageData pixel buffers. To undo, replay all commands except the last: clear the DrawingCanvas with ctx.clearRect() and re-execute every command from the array. This reduces memory from roughly 8MB per ImageData snapshot (1920×1080 at 4 bytes/pixel) to a few hundred bytes per stroke command.
5

Add real-time collaborative drawing via Supabase Realtime

Turns the single-user canvas into a shared whiteboard where multiple users see each other's drawing strokes in real time.

Advanced
Paste into v0 chat
Add a Supabase client using NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY from the Vars panel. On the DrawingCanvas onMouseMove handler (throttled to 30ms using a timestamp check), broadcast each drawing command as a Supabase Realtime channel message: supabase.channel('canvas').send({ type: 'broadcast', event: 'draw', payload: { tool, x, y, color, lineWidth } }). Subscribe to the same channel and, on receiving a remote draw event, execute the same drawing logic on the local DrawingCanvas for the remote user's coordinates. Show each connected user's cursor as a colored dot with their cursor position updated via a separate 'cursor' broadcast event, labeled with a random user ID generated on page load.

Gotchas when you extend it

The failures people actually hit when they push this template past its defaults — and the exact fix for each.

Canvas is blank when the component first mounts — ctx.getContext('2d') returns null

Why: The DrawingCanvas component may attempt to call canvasRef.current.getContext('2d') during render, before the canvas DOM element has been attached to the ref. The getContext call returns null, all subsequent ctx method calls silently fail, and the canvas stays blank.

Fix: Initialize the canvas context inside a useEffect with an empty dependency array. Add a null guard at the top: if (!canvasRef.current) return. This ensures the context is retrieved after the DOM element exists.

Fix prompt — paste into v0
Wrap the initial canvas setup in a useEffect with [] deps and add if (!canvasRef.current) return; at the top of the effect. Move all ctx initialization (getContext, setLineCap, setLineJoin) inside this effect.
Undo restores to wrong state — jumps multiple steps at once

Why: If the ImageData snapshot is captured inside the onMouseMove handler, hundreds of nearly identical snapshots are pushed to the UndoRedoStack per drag gesture. Each Ctrl+Z only undoes one snapshot, but the visible change is tiny (a fraction of one stroke), making it feel like undo is broken.

Fix: Move the ctx.getImageData() snapshot call from the onMouseMove handler to the onMouseUp handler. This captures exactly one snapshot per complete stroke, giving the expected one-undo-per-stroke behavior.

Fix prompt — paste into v0
Move the ctx.getImageData snapshot call from the onMouseMove handler to the onMouseUp handler in DrawingCanvas. This ensures one undo step corresponds to one complete stroke, not one mouse move event.
ReferenceError: document is not defined during Next.js build

Why: Canvas event listeners or DOM access added at module level (outside useEffect) execute during Next.js SSR where the document object doesn't exist. This causes a build-time crash, not a runtime error.

Fix: Add 'use client' to the DrawingCanvas component file header. Wrap all canvas initialization and event listener setup in useEffect. Never access document, window, or canvas APIs outside a useEffect or event handler.

Fix prompt — paste into v0
Add 'use client' directive to the DrawingCanvas component file. Wrap all document and canvas initialization inside useEffect hooks.
Flood fill (paint bucket) freezes or crashes the browser on large canvases

Why: A naive recursive flood fill algorithm calls itself for every adjacent pixel of the same color. On a 1920×1080 canvas this can mean 2 million recursive calls, overflowing the JavaScript call stack and either freezing the browser tab or throwing a 'Maximum call stack size exceeded' error.

Fix: Replace the recursive flood fill with an iterative queue-based implementation using a JavaScript array as a queue. Operate on raw pixel data from ctx.getImageData() rather than reading pixel colors one at a time per recursive call. Optionally limit fill to the visible bounding box for partial fills on large canvases.

Fix prompt — paste into v0
Replace the recursive flood fill algorithm in the Fill tool handler with an iterative queue-based approach: use ctx.getImageData() to get a Uint8ClampedArray of all pixels, then process the fill using an array-as-queue (shift/push) instead of recursive calls. This handles any canvas size without call stack overflow.

Template vs. custom — the honest call

A forked template gets you far, fast. Here's where it holds up, and where you'll outgrow it.

The template is enough when

  • You need a lightweight annotation or whiteboard widget embedded in a larger Next.js app.
  • You want a fun side project or portfolio piece with nostalgic MS Paint aesthetics.
  • You're learning HTML5 Canvas APIs and want a concrete, well-structured starting point.
  • You need a simple signature pad with PNG export for a form flow.

Go custom when

  • You need real-time collaborative whiteboarding at scale (Miro-like) with hundreds of concurrent users.
  • You need vector SVG output with editable path nodes rather than rasterized canvas strokes.
  • You need infinite canvas with panning and zooming beyond the fixed viewport.
  • You need shape recognition — converting freehand strokes to perfect geometric shapes.

If you need this drawing canvas with real-time collaboration, cloud storage, and multi-user sessions, RapidDev can extend the prototype to a production-ready whiteboard tool.

Frequently asked questions

Is the Microsoft Paint Clone v0 template free to use?

Yes. It is a free V0 community template. All libraries — Zustand, react-colorful, shadcn/ui — are MIT-licensed. The template uses only browser-native APIs (HTML5 Canvas) and Next.js, which is also MIT-licensed. No runtime costs.

Can I use this template commercially?

Yes. All component libraries used are MIT-licensed and V0 community forks give you full code ownership. You can embed this in a commercial SaaS, sell access to it, or use it as the basis for a paid whiteboard product without restriction.

Why does my fork show a blank canvas in the V0 Preview?

The most common cause is the canvas ref not being attached yet when the component first mounts. Ensure all canvas setup is inside a useEffect with [] deps and add if (!canvasRef.current) return at the top of the effect. Also check that 'use client' is on the DrawingCanvas component file, since SSR cannot access the canvas DOM element.

Why does undo jump multiple strokes instead of one at a time?

The ImageData snapshot is likely being captured on every mousemove event rather than on mouseup. That floods the undo stack with hundreds of nearly identical frames per stroke. Move the ctx.getImageData() call to the onMouseUp handler so exactly one snapshot is saved per completed stroke.

The flood fill (paint bucket) freezes my browser — how do I fix it?

The recursive flood fill implementation overflows the JavaScript call stack on large canvases. Replace it with an iterative queue-based algorithm that processes pixels using a JavaScript array. The prompt in the Gotchas section is the complete fix — paste it into V0 chat and it rewrites the fill algorithm.

Can I embed just the DrawingCanvas as a component inside an existing app?

Yes. Export to GitHub via the Git panel, then copy DrawingCanvas, ToolSelector, ColorPalette, BrushSizeSlider, and UndoRedoStack into your existing project. Install Zustand and react-colorful. Import the DrawingCanvas component where needed — it is self-contained with its own Zustand store.

Can RapidDev extend this to a real collaborative whiteboard?

Yes — RapidDev can add Supabase Realtime for live collaboration, user presence indicators, cloud canvas persistence, and session management. The Supabase Realtime prompt in the pack is the starting point; a full production whiteboard typically takes 2–3 weeks.

Outgrowing the template?

RapidDev turns v0 prototypes into production apps — real auth, database, and payments — at $13K–$25K.

Book a free consultation

30-min call. No commitment.

Want this built for you?

We ship production apps at a fixed price — $13K–$25K, 6–10 weeks, source code yours. You've seen what it takes; we do it every week.

Get a fixed-price quote

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.