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

scraps-ui

v0.5.1

Published

A UI library where everything is cut and torn paper

Readme

scraps

scraps, a UI library cut from paper

Demo: scraps.ogme01.com

A UI library where everything is cut and torn paper.

Every component is a real HTML element backed by a procedurally generated SVG scrap. Torn edges get a fibrous white fringe, scissor-cut edges get straight strokes with a little drift, and everything sits at a slightly crooked angle with a paper-grain texture and a drop shadow. Randomness is seeded (mulberry32): the same seed produces pixel-identical tears on any machine. Hover a scrap and it re-tears itself, quivering like a 90s cartoon.

Real HTML inputs underneath. Zero dependencies. Two files.

Install

<link rel="stylesheet" href="scraps.css">
<script src="scraps.js"></script>

Use

<button data-scrap="button" data-color="coral">rip it</button>
<label data-scrap="toggle"><input type="checkbox"> deckle edges</label>
<div data-scrap="card" data-color="kraft" data-tape>a note, taped to the mat</div>

<script>
  Scraps.init()               // tear everything with [data-scrap]
  Scraps.reseed('zine-04')    // re-tear the whole page from a new seed
  Scraps.tear(el)             // re-tear one element
  Scraps.setProgress(el, 64)  // move a progress bar
  Scraps.boil = false         // disable hover re-tearing globally
</script>

Components (data-scrap)

| type | element | edges | | --- | --- | --- | | button | <button> | cut | | chip | any inline element | cut | | card | any block element | torn | | field | <input>, <textarea>, <select>, <input type=range> | cut | | checkbox / radio / toggle | <label> wrapping an <input> | cut / torn | | progress | <div data-value="40" data-fill="coral"> | torn | | divider | <div> | torn | | dialog | native <dialog> (also sheets via data-side="right") | torn | | menu | a panel of buttons, opened by any data-menu="#id" trigger | torn | | tabs | [data-tab] buttons + [data-panel] panels | cut / torn | | accordion | native <details>/<summary> | torn | | table | a real <table> on ruled paper | torn | | avatar | <img> torn out along a seeded edge | torn | | skeleton | <div> that slowly re-tears while loading | torn | | alert | kraft paper, tape, and role="alert" | torn |

Selects open a paper dropdown instead of the native popup; the real <select> keeps the value, the change events, and the keyboard focus. Checkbox X marks land at a slightly different angle and position on every check. Tooltips need no component: any element with data-tip="text" grows a small scrap on hover and focus. Toasts are a call: Scraps.toast('glued.', { color: 'ink', duration: 4000 }). Breadcrumbs and pagination compose from chips; see the swatch book.

Attributes

  • data-color: white · kraft · coral · marigold · blue · ink
  • data-edge: torn | cut, overrides the type's default
  • data-seed: pin one element's tear so reseeds don't change it
  • data-rot: max crookedness in degrees
  • data-amp: tear amplitude in px
  • data-tape: (cards) adds a strip of translucent tape
  • data-boil: "false" keeps a scrap still under the cursor
  • data-fx: rip · fold · glue · shred, a paper effect played on click; also callable directly as Scraps.fx.rip(el) etc. Skipped under reduced motion.
  • data-tip: tooltip text, on any element
  • data-menu: on a trigger, the selector of a data-scrap="menu" panel
  • data-side: (dialogs) left | right turns the dialog into a sheet
  • data-active: (tabs) which data-tab starts selected

React

scraps-ui/react ships thin components over the same core (React is an optional peer dependency; vanilla users never load it):

import { ScrapButton, ScrapToggle, ScrapCard, reseed } from 'scraps-ui/react'
import 'scraps-ui/scraps.css'

<ScrapCard color="kraft" tape>
  <ScrapButton color="coral" fx="rip" onClick={save}>rip it</ScrapButton>
  <ScrapToggle defaultChecked>deckle edges</ScrapToggle>
</ScrapCard>

Every component renders its real HTML element and cleans up after itself on unmount. The full set: ScrapButton, ScrapChip, ScrapCard, ScrapDivider, ScrapInput, ScrapTextarea, ScrapSelect, ScrapRange, ScrapCheckbox, ScrapRadio, ScrapToggle, ScrapProgress, ScrapDialog (controlled via an open prop), ScrapTabs, ScrapAccordion, ScrapTable, ScrapAvatar, ScrapSkeleton, ScrapAlert, ScrapMenuPanel, plus a toast export and a tip prop on everything.

Any design system

Scraps.adapt papers components scraps knows nothing about: Base UI, Radix, shadcn, your own. You hand it a map from selector to paper and it does the rest, so no host component has to be edited or wrapped.

const stop = Scraps.adapt({
  '[data-slot="card"]': 'card',
  '[data-slot="button"]': 'button',
  '[data-slot="button"][data-disabled]': { type: 'button', color: 'kraft', boil: false },
  '[data-slot="badge"]': { type: 'badge', color: 'marigold' },
  '[data-slot="input"]': 'input',
  '[data-slot="dialog-content"]': 'dialog',
})

stop()   // every element handed back as plain markup

One observer watches the document, so paper reaches a dialog or a popover the moment it is portalled in, and comes off when it unmounts. Entries are read in order and the last match wins, so a state selector can override a plain one; only the attributes those selectors name are watched, so a component flipping data-disabled or data-open re-cuts its paper on the spot and nothing else costs anything.

The paper is attach() and nothing else. No wrapper nodes, no adopted children, no behavior: the host keeps its own markup, keyboard handling and accessibility, and scraps only supplies the tear. Elements that take no children, <input> and <textarea>, get the tear cut out of the element itself as a clip-path instead of laid behind it, and keep their own background.

Skins available: button, chip, badge, tab, card, panel, dialog, popover, menu, alert, field, input, box, checkbox, radio, switch, thumb, row, divider, separator, progress, fill, avatar, skeleton, tape. Each takes the usual color, edge, seed, rot, amp and boil options, plus reset: false to keep the host's own background, border, shadow and overflow.

In React, useScraps ties an adapter to a component's lifetime, which is all a theme switch needs:

import { useScraps } from 'scraps-ui/react'
import 'scraps-ui/scraps.css'

const PAPER = { '[data-slot="button"]': 'button', '[data-slot="card"]': 'card' }

function Theme ({ children }) {
  const { preset } = useTheme()
  useScraps(PAPER, { enabled: preset === 'scraps' })
  return children
}

Define the map outside the component so it stays referentially stable. To paper a single element by hand, Scraps.skin(el, 'card'), and in plain markup data-skin on a data-scrap element opts it out of the wrapping builders.

Tailwind

In a Tailwind v4 project, import the bridge instead of the plain stylesheet:

@import "tailwindcss";
@import "scraps-ui/tailwind.css";

The paper palette becomes Tailwind color tokens (bg-scrap-coral, text-scrap-ink, ...) and scraps reads the same tokens back, so overriding --color-scrap-* in your @theme recolors both your utilities and the paper.

shadcn

scraps hosts a shadcn registry, so you can copy the typed React components straight into your project and own the source:

npx shadcn@latest add https://scraps.ogme01.com/r/scraps.json

That installs the whole drawer (components in components/ui/, the shared hook in lib/scraps-core.ts, scraps-ui as the engine dependency). Single components work too: .../r/scrap-button.json, scrap-card, scrap-chip, scrap-field, scrap-choice, scrap-progress.

Theming

Override the CSS custom properties on :root (--sc-white, --sc-kraft, --sc-coral, --sc-marigold, --sc-blue, --sc-ink, --sc-fringe, --sc-shadow, --sc-text, --sc-text-light) to recolor the stock palette, or register entirely new paper colors:

Scraps.registerColors({
  brand: '#7A4FBF',
  night: { fill: '#101418', text: '#F6F1E4' },
})

Registered names become valid data-color values (and color props in React).

License

MIT