Skip to content
HN On Hacker News ↗

GitHub - Danilaa1/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, 4 KB of JS and one stylesheet.

▲ 41 points • 20 comments • by ingve • 5w ago • HN discussion ↗

Pangram verdict · v3.3

We believe that this entire text is AI.

100 %

AI likelihood · overall

AI
0% human-written 100% AI-generated
SEGMENTS · HUMAN 0 of 1
SEGMENTS · AI 1 of 1
WORD COUNT 947
PEAK AI % 100% · §1
Analyzed
Sep 14
backend: pangram/v3.3
Segments scanned
1 windows
avg 947 words each
Distribution
0 / 100%
human / AI fraction
Verdict
AI
Pangram v3.3

Article text · 947 words · 1 segments analyzed

Human AI-generated
§1 AI · 100%

Hand-drawn UI controls. Every mount generates a fresh pen sketch from seeded randomness, and the stroke boils like an animated doodle. Zero dependencies, ~9 KB of JS gzipped (React wrappers add under 1 KB) and a 3 KB stylesheet. An optional pen font is a separate 31 KB. 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: 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. Composites Pieces built from the controls above. One seed reproduces every stroke in the piece; destroy() tears them all down. Function Markup Draws drawablyChip(el, opts) <label><span><input type="checkbox"></span> text</label> badge around the chip, sketched box on the input's wrapper drawablyTabs(el, opts) children are the tabs underline on the active tab; active index or aria-selected="true"; setActive(i) drawablyTooltip(tip, target, opts) two elements card around the tip, arrow to the target drawablyAlert(el, opts) optional [data-tag] child card around the alert, badge on the tag drawablySteps(el, opts) <ol> check-marked list drawablyKbd(el, opts) <kbd> badge with a steadier hand drawablyQuote(el, opts) first element child, optional <footer> highlight on the line, divider on the footer drawablyPager(el, opts) child <button>s outlined pages, solid current; active index or aria-current; setPage(i) import { DrawablyChip, DrawablyTabs, DrawablyTooltip, DrawablyAlert, DrawablySteps, DrawablyKbd, DrawablyQuote, DrawablyPager } from "drawably/react"; <DrawablyChip defaultChecked>pen</DrawablyChip> <DrawablyTabs active={tab}><span>a</span><span>b</span></DrawablyTabs> <DrawablyTooltip to={buttonRef}>undo</DrawablyTooltip> <DrawablyAlert><span data-tag>new</span> Import from Attio lands Friday</DrawablyAlert> <DrawablySteps><li>record</li><li>label</li></DrawablySteps> <DrawablyKbd>⌘K</DrawablyKbd> <DrawablyQuote><span>less, but better</span><footer>Rams</footer></DrawablyQuote> <DrawablyPager active={page}><button>‹</button><button>1</button><button>›</button></DrawablyPager> 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.