Most UI kits try hard to look neutral. PlopUI goes the other way: wobbly sticker buttons, washi-tape tags, a hand-lettered marker font, colors picked by instinct. The whole thing looks like it was drawn on the back of a receipt.

It has one rule: break the visual rulebook, never the accessibility one.

The PlopUI home page: a hand-drawn headline, sticker buttons and doodled characters on paper

The drawing sits on top, never instead

A PlopUI button is a real <button>. A checkbox is a real checkbox. The hand-drawn outlines, shadows and doodles are decoration layered over native elements, so the browser and screen readers still know exactly what everything is.

That means the boring stuff just works:

  • Tab, Enter and Space do what you expect. Select and Menu also handle arrow keys, Home, End, type-to-jump and Escape.
  • Every interactive component has a keyboard test, and the docs site runs end-to-end tests in Chromium, Firefox and WebKit.
  • Status never relies on color alone. The error character (Bix) winces and sweats, so the meaning reads even if you can't tell red from green.

Crooked, but the same crooked everywhere

The hardest part of a "random" look is server rendering. If a button's tilt comes from Math.random(), the server draws one angle and the browser draws another. React complains about a hydration mismatch, and the button visibly jumps.

So nothing in PlopUI calls Math.random() while rendering. Every tilt, wobble and jitter comes from a seed:

// hashString is FNV-1a: it turns a string like "Post sketch" into a 32-bit number. /** A deterministic random generator: same seed, same sequence, values in [0, 1). */ export function seed(value: string | number): () => number { let x = typeof value === "number" ? value >>> 0 || 1 : hashString(value) || 1; return () => { // mulberry32 x = (x + 0x6d2b79f5) >>> 0; let t = x; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } /** A value in [-range, range], rounded so it serializes identically on server and client. */ export function seededTilt(value: string | number, range = 2.5): number { const n = seed(value)(); return Math.round((n * 2 - 1) * range * 100) / 100; } 

Same seed in, same crooked line out, on the server and in the browser. The rounding matters too: two decimals means the number is written identically in the server HTML and in React's props.

Built for the App Router

The package never imports next/*, so it works in any React 19 app. In Next.js, every interactive component already carries "use client", and the static ones (a timeline, a plain tag) stay Server Components and ship no client JavaScript.

It's also light where it counts: everything is tree-shakeable, and a plain Button is about 5.6 kB gzipped.

The wobble knows when to chill

Every animation respects prefers-reduced-motion. Turn it on and the motion stops, while state changes still show, just instantly. Looping scenes pause while they're off screen, which took the docs site's idle CPU on a throttled phone from 12% to 0%. And the animated footer has a pause button, because anything that moves for more than five seconds should.

Three packages

  • @plop-ui/react: the components, hooks and characters.
  • @plop-ui/tokens: colors with measured contrast, type, motion and five paper surfaces as CSS variables. There's also a tokens.json for Figma (Tokens Studio).
  • @plop-ui/fonts: Sketchpop, the marker font, under the SIL Open Font License. Every letter is drawn three times and rotates through calt, so a word like "Shipped!" never repeats the same p twice.

Try it

npm i @plop-ui/react @plop-ui/fonts 
import "@plop-ui/fonts/fonts.css"; import "@plop-ui/react/styles.css"; import { PlopProvider, Button, toast } from "@plop-ui/react"; export function App() { return ( <PlopProvider surface="poster"> <Button buddy onClick={() => toast.success("Sketch posted", { buddy: true })}> Post sketch </Button> </PlopProvider> ); } 

Or skip the install and play with it first: the Playground lets you stick components onto paper and copy the code.

What it's for

Creative tools, learning apps, games, portfolios, side projects: anything that should feel made by a person. It's not the right pick for a dense enterprise dashboard where every pixel has to stay quiet.

PlopUI is free and MIT-licensed: plopui.com · GitHub. Issues and pull requests are welcome, and a few are labeled "good first issue". If something breaks, tell me. I'll fix it and thank you by name.