Skip to content

Repository files navigation

drawably

Hand-drawn UI controls. Every mount generates a fresh pen sketch from seeded randomness, and the stroke boils like an animated doodle. Zero dependencies, ~7 KB of JS gzipped (React wrappers add under 1 KB) and a 3 KB stylesheet. An optional pen font is a separate 31 KB.

Buttons, checkbox, radio and toggle drawn in a boiling pen stroke

Install

npm i drawably

Quick start

import { drawablyButton } from "drawably";
import "drawably/style.css";

drawablyButton(document.querySelector("#done"), { variant: "solid" });

React:

import { DrawablyButton } from "drawably/react";
import "drawably/style.css";

<DrawablyButton variant="solid" onClick={submit}>Done</DrawablyButton>

Each attach call returns a sketch handle:

const sketch = drawablyButton(el);
sketch.resketch();     // redraw with a new random seed
sketch.resketch(42);   // redraw with a specific seed
sketch.destroy();      // remove the SVG and all listeners

Buttons

Three variants: outline (default), solid, scribble. Buttons also carry a state machine for async work:

The four button states: idle, loading, error, success

const button = drawablyButton(el);
button.setState("loading");  // dims the button, boils faster
button.setState("error");    // redraws in red
button.setState("success");  // redraws in green
button.setState("idle");

In React, pass the state prop; the sketch stays put and only the state changes:

<DrawablyButton state={saving ? "loading" : "idle"}>Save</DrawablyButton>

Override the state colours with --drawably-error and --drawably-success. For secondary or destructive actions, set tone: "neutral" (warm grey) or tone: "danger" (red).

Controls

Function Element it expects
drawablyButton(el, opts) a <button>
drawablyCheckbox(el, opts) wrapper containing <input type="checkbox">
drawablyRadio(el, opts) wrapper containing <input type="radio">
drawablyToggle(el, opts) wrapper containing <input type="checkbox">
drawablyInput(el, opts) wrapper containing an <input>
drawablyTextarea(el, opts) wrapper containing a <textarea>
drawablySelect(el, opts) wrapper containing a <select>
drawablyDivider(el, opts) an <hr> or div
drawablyCard(el, opts) any block element
drawablyBadge(el, opts) any inline element
drawablyList(el, opts) a <ul> or <ol>; each <li> gets a sketched marker

Badges take variant: "outline" | "scribble"; lists take marker: "dash" | "check". Selects reserve the widest option's width so picking never shifts layout; in Chromium the options list gets a sketched frame and pen check (appearance: base-select), Safari and Firefox keep the OS popup.

The real inputs stay in the DOM, so keyboard, forms, labels and screen readers all work as usual. The sketch is an aria-hidden SVG layered underneath.

Every control has a React counterpart in drawably/react: DrawablyButton, DrawablyCheckbox, DrawablyRadio, DrawablyToggle, DrawablyInput, DrawablyTextarea, DrawablySelect, DrawablyDivider, DrawablyCard, DrawablyBadge, DrawablyList.

Text decoration

Annotate copy the way you would with a pen. Each attaches to an inline element and leaves its layout alone; use them on a word or a short phrase.

Function Draws
drawablyUnderline(el, opts) a rough line under the text, re-sketched on hover
drawablyHighlight(el, opts) a marker wash behind the text
drawablyCircle(el, opts) a hand-drawn ellipse looping around the text
drawablyArrow(from, to, opts) an arrow from one element to another
import { DrawablyUnderline, DrawablyHighlight, DrawablyCircle, DrawablyArrow } from "drawably/react";

<p>
  <DrawablyUnderline>Hand-drawn</DrawablyUnderline> UI, a{" "}
  <DrawablyHighlight>fresh sketch</DrawablyHighlight> on{" "}
  <DrawablyCircle>every mount</DrawablyCircle>.
</p>
<DrawablyArrow from={noteRef} to={buttonRef} />

A decoration that wraps onto several lines gets one drawing per line. The arrow's SVG is appended to <body> in document coordinates and redraws on resize. Anchors inside a scrolling container will drift as it scrolls.

Options

All controls take the same base options:

Option Default What it does
seed random Omit for a unique sketch per mount, pass a number for a reproducible one
roughness 1 Wobble of the base sketch
boil 0.3 Px of frame-to-frame flicker; 0 renders one static path
stroke, fill, paper pen blue / white Colours, set as --drawably-* custom properties
width 2 Stroke width in px

The colours are plain CSS custom properties, so a theme can set them once:

:root {
  --drawably-stroke: #1a1a1a;
  --drawably-fill: #1a1a1a;
}

Type is Inter when the page has it loaded, falling back to system-ui. The library loads no font unless you opt into the one below.

Motion

Strokes boil gently: three frames of the same sketch, micro-wobbled around a shared base, cycled by pure CSS at 1200ms. Hover or press re-sketches buttons, checkboxes, radios, toggles, underlines and circles; buttons also lift on hover and sink on press. prefers-reduced-motion freezes everything to a single static sketch, including the demo images above.

Font (optional)

Drawably Pen is the same strokes as a typeface: a–z, A–Z, digits and punctuation, built by the library's own pen code (font/) into a 31 KB TrueType. It is not loaded by style.css; nothing in the library needs it. If you want labels in the same hand as the chrome:

import "drawably/font.css";
.drawably-button {
  font-family: "Drawably Pen", Inter, sans-serif;
}

Build your own shapes

The rough renderer is exported. Each function returns an SVG path string, and variants produces the boil frames:

import { roughRoundedRect, roughLine, roughCircle, variants } from "drawably";

const frames = variants(
  (o) => roughRoundedRect(0, 0, 200, 100, 12, o),
  { seed: 7, roughness: 1, boil: 0.3 },
);
// three path strings — render them and cycle opacity

Also exported: roughEllipse, roughArrow, roughCheckmark, scribbleFill, and the seeded PRNG mulberry32 with randomSeed.

License

MIT.

About

Hand-drawn UI controls. Every mount generates a fresh pen sketch from seeded randomness, and the stroke boils like an animated doodle. Zero dependencies, 4 KB of JS and one stylesheet.

Resources

Stars

116 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages