
A draggable pen component built in Vue that lets visitors draw directly on any page, on a computer and on a phone alike. Strokes are stored in Supabase and synced across all visitors in real time via a broadcast channel, so drawings are shared and persistent.
Try it on the dedicated canvas, or the home page for a non persistent, local variant.
Features
- Drag and pick up the pen, then draw freely on the page
- On a touch screen, the pens of a page collect in one small toolbar in the bottom left corner: tap a pen to take it in hand, then one finger draws and two fingers scroll
- Multiple pen types: pencil, crayon, brush, eraser
- The pencil is drawn by the site, not taken from a font, so it shows everywhere and follows the stroke color
- Configurable stroke color, width, and tip offsets per instance
- Undo / redo with
Ctrl+Z/Ctrl+Y, or the undo button on a touch screen - Local storage mode for single-device persistence
- Cloud storage mode via Supabase realtime broadcast β strokes are shared across all open sessions on the same canvas ID
- The canvas covers a band of the page twice the height of the viewport, which the browser carries along, so scrolling stays native
dragAndDrawmode for a fixed pen that draws while scrolling

Usage
<DrawablePen
:cloudStorage="true"
penEmoji="π"
strokeColor="#111111"
:strokeWidth="3"
/>Multiple pens can be placed on the same page and will share the same canvas:
<DrawablePen :cloudStorage="true" penEmoji="π" />
<DrawablePen :cloudStorage="true" penEmoji="π" strokeColor="green" />
<DrawablePen :cloudStorage="true" penEmoji="ποΈ" strokeColor="red" :tipOffsetX="5" :tipOffsetY="43" />
<DrawablePen :cloudStorage="true" penEmoji="ποΈ" strokeColor="rgba(0,255,255,0.5)" :strokeWidth="25" />
<DrawablePen :cloudStorage="true" penEmoji="π§Ή" :eraserMode="true" :strokeWidth="80" />The canvas ID is the page path, so each page gets its own shared drawing.
The touch toolbar is on by default. A page that should stay without one passes :mobile="false" to its pens:
<DrawablePen :cloudStorage="true" :mobile="false" penEmoji="π" />
Pages using it
- bbo.do/: the home page has a drag-and-draw pen embedded in the background
- /notes/2605-canvas: a dedicated multi-pen canvas session
- /notes/2605-arte-digital-canvas: used during an Arte Digital session at EBA / UFRJ

How it works
Each completed stroke is saved to a shared allStrokes array and broadcast to other sessions via a Supabase realtime channel. Incoming strokes from other users are applied immediately and the canvas is redrawn:
function setupSupabaseSync() {
broadcastChannel = supabase
.channel(`drawing:${effectiveCloudStorageId}:strokes`, {
config: { broadcast: { self: false } },
})
.on('broadcast', { event: 'stroke_added' }, ({ payload }) => {
if (payload.stroke) {
allStrokes.push(payload.stroke)
redrawAll()
}
})
.on('broadcast', { event: 'stroke_removed' }, ({ payload }) => {
const index = allStrokes.findIndex(s =>
s.userId === payload.stroke.userId
&& s.timestamp === payload.stroke.timestamp
&& JSON.stringify(s.points) === JSON.stringify(payload.stroke.points),
)
if (index !== -1) {
allStrokes.splice(index, 1)
redrawAll()
}
})
.on('broadcast', { event: 'clear' }, () => {
allStrokes.length = 0
redrawAll()
})
.subscribe()
}When the mouse is released, the collected path points are packaged into a stroke object and broadcast to all connected clients:
function endDrawing() {
if (isDrawing.value && currentPath.length > 0) {
saveStroke({
points: [...currentPath],
color: currentStrokeColor.value,
width: currentStrokeWidth.value,
isEraser: props.eraserMode,
userId: currentUserId,
timestamp: Date.now(),
})
}
isDrawing.value = false
currentPath = []
}Source
The full source is part of this siteβs repository: DrawablePen.vue.