Drawesome

Drawesome is a drawing toolbar for React. Seven pens, an eraser, SVG and PNG export, no dependencies beyond React. Two lines to drop into an app.

I work on a lot of creative tools, and many of them benefit from the ability to draw. Annotating a screenshot, marking up a frame, circling the bit that's wrong. I kept building the same thing over and over, so I finally made it a package.

Most of the work went into the toolbar.1 It changes shape rather than swapping panels: pick a colour and the row of pens becomes the palette, open the size controls and it becomes two sliders, minimise it and it rolls up into a disc with the tool you're holding still in it. Every state morphs smoothly into the next.


Getting started

npm install drawesome

It fills its parent container. Set a height on the wrapper.

import { Draw } from 'drawesome'
import 'drawesome/styles.css'
 
<div style={{ height: 480 }}>
  <Draw />
</div>

The tools

Pencil
Pen
Fineliner
Marker
Highlighter
Brush
Fountain Pen
Eraser

Each pen behaves like the thing it's named after. The pencil, pen and brush thin out the faster you move, while the fineliner and highlighter keep a constant width. The fountain pen responds to direction instead: thick one way, hairline the other. No two strokes come out quite the same.

The eraser takes away area rather than whole strokes, so you can rub out part of a line and keep the rest.


Making it yours

Drawesome is opinionated on purpose. I wanted the defaults to be the version you actually ship, so getting something that feels right doesn't require tuning anything at all. Everything below is for turning things off or moving them around.2

You can pick the tools and the order they sit in, and switch off anything you don't want:

<Draw
  tools={['pencil', 'marker', 'highlighter']}
  controls={{ undo: false, clear: false }}
/>

theme takes light, dark or auto, which follows the reader's system setting. The bar above is on auto, so if your system is set to dark mode, the bar will be too.

placement puts the bar on an edge, and inset and align control exactly where it sits. motion is how it arrives and leaves, coming in off whichever edge it's on. draggable lets people pick it up and move it themselves. swatches replaces the palette with your own colours.

Two props change how it's drawn: look="studio" lights the tools as objects instead of shading them flat, and depth sets how much shadow and sheen the bar gets. The demo above uses studio.

The canvas is up to you. Give background a colour, or set it to transparent and the component paints nothing at all, so whatever is behind it shows through. That's how the demo above sits on graph paper: the paper is just CSS on this page.

<div className="your-paper">
  <Draw background="transparent" />
</div>

The bar is only as wide as the tools in it, so on a phone the default set runs off both sides of the screen. A phone has far more height than width though, so under 680px the demo stands the bar up on the left edge and trims the toolset:

<Draw
  placement={narrow ? 'left' : 'bottom'}
  tools={narrow ? ['pencil', 'pen', 'marker', 'highlighter', 'brush'] : undefined}
  controls={narrow ? { undo: false, clear: false, opacity: false, custom: false } : undefined}
/>

controls lets you switch size and opacity off separately. That matters on the vertical rail, where two sliders stacked on top of each other end up taller than the canvas, so the phone demo keeps size and drops opacity. Turn both off and the button that opens them goes too.

chrome={false} removes the toolbar entirely and leaves you the surface. DrawSurface, Toolbar and the useDrawing hook are all exported separately if you'd rather lay the pieces out yourself.

To get the drawing out, put a ref on it. toSvg() returns a string, toPng() returns a file, and download() saves either. What you get back matches exactly what was on screen, erasing included.

await draw.current.download('sketch', 'png', 2)

Props

Surface

board{ w, h }element size
A fixed drawing area, in board units. Left off, the drawing is whatever size the element is.
backgroundstring"#ffffff"
Any CSS colour, "transparent" to paint nothing so whatever is behind shows through, or "checker".
initialStrokesStroke[][]
What to open with, for restoring something saved earlier.
onChange(strokes) => void
Fires on every finished stroke and every erase. Strokes are plain data, so you can store them as they are.
classNamestring
Passed to the root element, along with style.
styleCSSProperties

Tools

toolsPenId[]all seven
Which pens appear, and in what order.
eraserbooleantrue
ink"auto" | "shared" | "per-tool""auto"
Whether picking a colour changes every tool or only the one in hand. "auto" keeps the highlighter on its own and shares the rest.
swatchesstring[]18 built-in
Your own palette, clamped to what the bar can hold.
look"classic" | "studio""classic"
How the tools are drawn: shaded flat, or lit as objects.
gaugebooleanfalse
Print the current size on the barrel.

Chrome

chromebooleantrue
false leaves the surface and no toolbar, for bringing your own.
placement"bottom" | "left" | "right""bottom"
insetnumber | string20
How far the bar sits off its edge.
align"start" | "center" | "end""center"
Where along that edge it sits.
draggablebooleanfalse
Let people pick the bar up and put it somewhere else.
settings"bar" | "tool""bar"
Where size and opacity live.
controls{ color, size, opacity, undo, clear, custom, minimize }all true
size and opacity are separate; turn both off and the control goes. custom is the swatch that opens the hex field and spectrum, worth dropping on a phone.
depth"flat" | "soft" | "regular" | "strong""regular"
How physical the bar looks: shadow, the light down its face, and the sheen on its top edge, all stepped together.
motion"rise" | "none" | { in, out, duration }"rise"
How the bar arrives and leaves when chrome is switched. It comes in off whichever edge it is on.
theme"light" | "dark" | "auto""light"
"auto" follows the reader's system setting.
tooltipsboolean | TooltipOptionstrue
shortcutsbooleantrue
Single-key shortcuts. Turn them off where the page has its own.
startMinimizedbooleanfalse
Start collapsed. Good for pages where drawing is optional.
drawWhenMinimizedbooleanfalse
Keep the canvas live while the bar is a disc.

Ref handle

toSvg() => string
Exactly what is on screen, erasing and all.
toPng(scale?) => Promise<Blob>
scale multiplies the resolution: 2 or 3 for print.
download(name?, format?, scale?) => Promise<void>
getStrokes() => Stroke[]
setStrokes(strokes) => void
Replaces the drawing. Undo history goes with it.
undo() => void
redo() => void
clear() => void
getSize() => Board
The surface size the drawing is being made at.

Every pen has a keyboard shortcut, shown in its tooltip. E for the eraser, [ and ] for size, ⌘Z and ⇧⌘Z for undo and redo. Hold Shift while drawing and the stroke locks to the nearest of eight directions.


Potentially awesome

It's called Drawesome, which is a lot to live up to. For now it does the thing I made it for: the next time something I'm building needs drawing, I just drop it in.

I'll let you draw your own conclusions.

See what I did there

Footnotes

  1. Inspired by Apple's markup tools, and how far they take the skeuomorphism. Pens in a tray beat a row of icons.

  2. If you want to assemble a drawing tool from parts instead, tldraw or Excalidraw will suit you better.