Animated Handdrawn UI Controls for JavaScript – Drawably

Category: Javascript , Recommended | September 10, 2026
AuthorDanilaa1
Last UpdateSeptember 10, 2026
LicenseMIT
Tags
Views0 views
Animated Handdrawn UI Controls for JavaScript – Drawably

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, and scribble buttons 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.

FunctionExpected 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)
  • solid
  • scribble

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.

  • idle
  • loading
  • error
  • success

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.

FunctionResult
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, default 1): Controls line wobble.
  • boil (number, default 0.3): Sets frame-to-frame displacement in pixels. Set 0 for 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 fallback 2): 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 PropertyDefaultPurpose
--drawably-stroke#2724d1Main pen color
--drawably-fill#2724d1Filled sketch color
--drawably-paper#fffPaper color
--drawably-width2Stroke width
--drawably-error#d12724Error and danger color
--drawably-success#188a42Success 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
  }
);
FunctionSignature
roughLineroughLine(x1, y1, x2, y2, options)
roughCircleroughCircle(cx, cy, radius, options)
roughEllipseroughEllipse(cx, cy, rx, ry, options)
roughArrowroughArrow(x1, y1, x2, y2, options)
roughRoundedRectroughRoundedRect(x, y, width, height, radius, options)
roughCheckmarkroughCheckmark(x, y, width, height, options)
scribbleFillscribbleFill(x, y, width, height, options)
variantsvariants(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

You Might Be Interested In:


Leave a Reply