npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

drawably

v0.3.10

Published

Hand-drawn UI controls. Every render a fresh pen sketch, boiling like a doodle.

Readme

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.