
drawably is a JavaScript UI library for creating animated hand-drawn buttons, form controls, cards, badges, lists, and text annotations.
It keeps native form elements in the DOM and draws aria-hidden SVG sketches around them.
Native controls continue to handle keyboard input, labels, form submission, and screen reader interaction.
The library generates a fresh sketch on each mount, while a numeric seed reproduces a specific drawing.
Features
- 10+ UI control attachers for buttons, form fields, dividers, cards, badges, and lists.
- Underline, highlight, circle, and arrow annotations for inline content and page elements.
- Random or reproducible drawings through numeric seeds and
resketch(). - Three-frame CSS boil animation with reduced-motion handling.
outline,solid, andscribblebuttons with idle, loading, error, and success states.- Stroke, fill, paper, line width, roughness, and boil configuration.
- Fixed select width based on the widest option, plus a sketched Chromium picker.
- React wrappers with TypeScript declarations.
- Rough-shape renderer and seeded PRNG helpers.
How To Use drawably
Installation
Install drawably from npm.
npm i drawably
Import the required module and stylesheet.
import { drawablyButton } from "drawably";
import "drawably/style.css";
Or directly load the stylesheet and ES module from a CDN.
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/drawably/style.css"
/>
<button id="save-button">Save Changes</button>
<script type="module">
import { drawablyButton } from "https://cdn.jsdelivr.net/npm/drawably/dist/index.js";
drawablyButton(document.querySelector("#save-button"));
</script>
Basic Usage
Buttons use the outline variant by default. Set variant to solid or scribble for the other drawing styles.
<button id="publish-button">Publish</button>
import { drawablyButton } from "drawably";
import "drawably/style.css";
drawablyButton(document.querySelector("#publish-button"), {
variant: "solid"
});
UI Controls
Checkbox, radio, toggle, input, textarea, and select functions expect a wrapper containing the required native field.
| Function | Expected Element |
|---|---|
drawablyButton(el, opts) | <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) | <hr> or <div> |
drawablyCard(el, opts) | Block element |
drawablyBadge(el, opts) | Inline element |
drawablyList(el, opts) | <ul> or <ol> |
A checkbox keeps its native input inside the wrapper.
<label>
<span id="newsletter-checkbox">
<input type="checkbox" name="newsletter" />
</span>
Send me product updates
</label>
import { drawablyCheckbox } from "drawably";
drawablyCheckbox(
document.querySelector("#newsletter-checkbox")
);
Button Variants, Tones, And States
Buttons accept three drawing variants:
outline(default)solidscribble
Secondary and destructive actions can use neutral and danger tones.
drawablyButton(document.querySelector("#cancel"), {
tone: "neutral"
});
drawablyButton(document.querySelector("#delete"), {
tone: "danger"
});
drawablyButton() returns a ButtonSketch with setState() for async actions.
const button = drawablyButton(
document.querySelector("#save-button"),
{
variant: "solid"
}
);
async function saveForm() {
button.setState("loading");
try {
await submitForm();
button.setState("success");
} catch (error) {
button.setState("error");
}
}
Available button states. Loading changes the cursor to progress, reduces button opacity, and speeds up the boil animation. Error and success use their corresponding theme colors.
idleloadingerrorsuccess
Text Annotations
drawably has four annotation functions. Wrapped inline text receives one sketch for each rendered line.
drawablyArrow() requires two valid anchor elements. Its SVG uses document coordinates and redraws on window resize. Anchors inside a scrolling container can drift while that container scrolls.
| Function | Result |
|---|---|
drawablyUnderline(el, opts) | Rough underline |
drawablyHighlight(el, opts) | Marker-style wash |
drawablyCircle(el, opts) | Hand-drawn ellipse |
drawablyArrow(from, to, opts) | Arrow between two elements |
<p> <span id="sketched-label">Hand-drawn UI</span> </p>
import { drawablyUnderline } from "drawably";
drawablyUnderline(
document.querySelector("#sketched-label")
);
Connect two elements with an arrow.
import { drawablyArrow } from "drawably";
drawablyArrow(
document.querySelector("#annotation"),
document.querySelector("#signup-button")
);
Configuration Options
seed(number, default random): Uses a reproducible numeric sketch seed.roughness(number, default1): Controls line wobble.boil(number, default0.3): Sets frame-to-frame displacement in pixels. Set0for one static drawing.stroke(string, CSS fallback#2724d1): Sets the pen stroke color.fill(string, CSS fallback#2724d1): Sets filled sketch areas.paper(string, CSS fallback#fff): Sets the paper color.width(number, CSS fallback2): Sets SVG stroke width.
drawablyButton(document.querySelector("#custom-button"), {
seed: 42,
roughness: 1.4,
boil: 0.2,
stroke: "#1f2937",
fill: "#1f2937",
paper: "#ffffff",
width: 2.5
});
Button Options
variant("outline" | "solid" | "scribble", default"outline"): Selects the drawing style.state("idle" | "loading" | "error" | "success", default"idle"): Sets the initial async state.tone("neutral" | "danger"): Uses the secondary or destructive color palette.
Badge Options
variant("outline" | "scribble", default"outline"): Selects the badge drawing style.
List Options
marker("dash" | "check", default"dash"): Selects the sketch marker for each list item.
Sketch Methods
Every attach function returns a Sketch handle.
const sketch = drawablyCard(
document.querySelector("#profile-card")
);
// Draw another random sketch.
sketch.resketch();
// Reproduce a specific sketch.
sketch.resketch(42);
// Remove the SVG and listeners.
sketch.destroy();
The base lifecycle API:
sketch.resketch(seed?); sketch.destroy();
Buttons return ButtonSketch, which has one additional method:
button.setState("loading");
button.setState("error");
button.setState("success");
button.setState("idle");
Styling With CSS Custom Properties
Set theme values on :root, a container, or an individual drawably host.
-root {
--drawably-stroke: #18181b;
--drawably-fill: #18181b;
--drawably-paper: #fafafa;
--drawably-width: 2;
--drawably-error: #c62828;
--drawably-success: #15803d;
}
| CSS Custom Property | Default | Purpose |
|---|---|---|
--drawably-stroke | #2724d1 | Main pen color |
--drawably-fill | #2724d1 | Filled sketch color |
--drawably-paper | #fff | Paper color |
--drawably-width | 2 | Stroke width |
--drawably-error | #d12724 | Error and danger color |
--drawably-success | #188a42 | Success color |
Optional Drawably Pen Font
Import the optional font stylesheet when controls should use Drawably Pen.
import "drawably/font.css";
Apply the font through CSS.
.drawably-button {
font-family: "Drawably Pen", Inter, sans-serif;
}
Motion And Reduced Motion
The normal boil effect cycles three related sketch frames every 1200ms. Set boil to 0 for one static drawing.
prefers-reduced-motion: reduce stops the boil animation and drawably control transitions.
drawablyCard(document.querySelector("#note"), {
boil: 0
});
Select And List Behavior
drawablySelect() measures its options during attachment and reserves enough width for the widest label. Reattach the select after changing its options.
Chromium browsers with appearance: base-select receive the sketched picker frame and check mark. Safari and Firefox retain their operating-system picker.
drawablyList() creates markers only for <li> elements present during attachment. Destroy the existing sketch and attach the list again after inserting or removing list items. Using an <ol> hides the browser’s numeric markers and replaces them with drawably markers.
React Integration
import {
DrawablyButton,
DrawablyCheckbox,
DrawablyRadio,
DrawablyToggle,
DrawablyInput,
DrawablyTextarea,
DrawablySelect,
DrawablyDivider,
DrawablyCard,
DrawablyBadge,
DrawablyList,
DrawablyUnderline,
DrawablyHighlight,
DrawablyCircle,
DrawablyArrow
} from "drawably/react";
import "drawably/style.css";
export default function Example() {
return (
<>
<DrawablyButton variant="solid">
Save
</DrawablyButton>
<label>
<DrawablyCheckbox />
Receive updates
</label>
<p>
This is <DrawablyHighlight>important</DrawablyHighlight>.
</p>
</>
);
}
Creating Custom Hand-Drawn Shapes
The main package exposes its rough renderer for custom SVG sketches.
import {
roughRoundedRect,
roughLine,
roughCircle,
variants
} from "drawably";
const frames = variants(
(options) =>
roughRoundedRect(
0,
0,
200,
100,
12,
options
),
{
seed: 7,
roughness: 1,
boil: 0.3
}
);
| Function | Signature |
|---|---|
roughLine | roughLine(x1, y1, x2, y2, options) |
roughCircle | roughCircle(cx, cy, radius, options) |
roughEllipse | roughEllipse(cx, cy, rx, ry, options) |
roughArrow | roughArrow(x1, y1, x2, y2, options) |
roughRoundedRect | roughRoundedRect(x, y, width, height, radius, options) |
roughCheckmark | roughCheckmark(x, y, width, height, options) |
scribbleFill | scribbleFill(x, y, width, height, options) |
variants | variants(generator, options, count = 3) |
RoughOptions has this structure:
{
seed: number;
roughness: number;
boil?: number;
boilSeed?: number;
}
The main package also exports two seeded-random helpers.
import {
mulberry32,
randomSeed
} from "drawably";
const random = mulberry32(42);
const seed = randomSeed();
mulberry32(seed); // Returns a seeded () => number function. randomSeed(); // Returns a random numeric seed.
Alternatives & Related Resources
- Animated Handdrawn Text Annotation Library – Rough Notation
- Hand-drawn Styled HTML Elements In CSS – Doodle.CSS
- Hand-drawn Style SVG Chart Library – chart.xkcd
- Cartoon XKCD Styled Chart Library – roughViz.js







