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

react-page-decorator

v0.3.1

Published

Drag-and-drop visual placement of images/illustrations anywhere on a React page. Dev editor + ships-in-code renderer, responsive per-breakpoint placements, pluggable persistence (localStorage / REST file).

Readme

react-page-decorator

Drop images / illustrations / PNG icons anywhere on a React page by dragging them, then save. Ships two halves:

  • Renderer — paints saved decorations over the page (runs in production).
  • Dev editor — a drag/resize/rotate overlay; saving is delegated to a pluggable storage adapter.

Features: absolute placement as page-percentages (responsive), per-breakpoint placements (a different size and area on mobile vs desktop), drag-to-move, corner-resize, corner-rotate, anchor a decoration to any element (click-to-pick selector), an image-path picker, optional next/image optimization, and inline-styled UI (no Tailwind or CSS import required — works in any React project).

Install

pnpm add react-page-decorator

react / react-dom are peer dependencies (>=18).

Zero-config (one line)

Render <DecorationAuto /> once — typically in your root layout. No CSS, no adapter, no position: relative, no props. It portals a full-page click-through layer into <body>, defaults to localStorage, and auto-opens the editor on ?edit=1 in development.

// app/layout.tsx (Next.js) — or your top-level App component
import { DecorationAuto } from "react-page-decorator";

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <DecorationAuto />
      </body>
    </html>
  );
}

Run dev, open ?edit=1, place images, Save → they persist to localStorage.

An npm package can't render itself — bundlers only include code you import — so this one mount is the minimum. Everything else is automatic. For server-rendered decorations or file-backed (commit-to-code) storage, use <DecorationCanvas> directly (below).

Concepts

A Decoration stores a Placement per breakpoint (desktop is required, mobile optional). The renderer resolves the placement for the current window width. You provide:

  • items — the saved decorations (e.g. imported from a JSON file you commit).
  • adapter — how to load/save (see below).
  • enabled — whether the editor is active (your call, e.g. dev + ?edit=1).

Quick start (any React app, zero backend)

localStorageAdapter persists in the browser — nothing else to set up. Omit enabled and the editor auto-opens on ?edit=1 in development:

"use client";
import { DecorationCanvas, localStorageAdapter } from "react-page-decorator";

const adapter = localStorageAdapter("my-site-decorations");

export function PageDecorations() {
  // Mount inside a `position: relative` element that wraps the whole page.
  return <DecorationCanvas adapter={adapter} />;
}

Open ?edit=1, place images, Save. They reload from localStorage next visit.

Edit mode: with enabled omitted, the editor turns on automatically when the URL has ?edit=1 and it's not a production build. Pass an explicit enabled={…} to control it yourself (e.g. behind your own flag).

Commit-to-code (Next.js App Router)

Persist to a JSON file so placements ship in your repo. Use restAdapter + a tiny route handler, and feed the committed file as items for SSR/prod.

// components/PageDecorations.tsx
"use client";
import { useEffect, useState } from "react";
import { DecorationCanvas, normalizeMany, restAdapter } from "react-page-decorator";
import saved from "@/data/decorations.json";

const ITEMS = normalizeMany(saved);
const adapter = restAdapter({ saveUrl: "/api/dev/decorations" });

export default function PageDecorations() {
  const [enabled, setEnabled] = useState(false);
  useEffect(() => {
    if (process.env.NODE_ENV !== "production") {
      setEnabled(new URLSearchParams(location.search).get("edit") === "1");
    }
  }, []);
  return <DecorationCanvas items={ITEMS} adapter={adapter} enabled={enabled} />;
}
// app/api/dev/decorations/route.ts
import { mkdir, writeFile } from "fs/promises";
import path from "path";
import { NextResponse } from "next/server";
import { normalizeMany } from "react-page-decorator/model"; // server-safe subpath

export async function POST(req: Request) {
  if (process.env.NODE_ENV === "production") {
    return NextResponse.json({ error: "disabled" }, { status: 403 });
  }
  const items = normalizeMany(await req.json().catch(() => null));
  const file = path.join(process.cwd(), "data", "decorations.json");
  await mkdir(path.dirname(file), { recursive: true });
  await writeFile(file, `${JSON.stringify(items, null, 2)}\n`, "utf8");
  return NextResponse.json({ ok: true, count: items.length });
}

Import model helpers from react-page-decorator/model in server code — it has no client components, so it won't pull React UI into your server bundle.

Optional: image picker

Pass assets (a string[] of image URLs) or assetsLoader (async) to populate the path field's autocomplete. In Next, point an assetsLoader at a dev route that lists files under public/.

Optional: optimized images (next/image)

By default decorations render as a lazy-loaded <img>. Pass renderImage to swap in an optimized component. The editor records each image's aspect ratio, so height is supplied for exact sizing (no distortion / layout shift):

import Image from "next/image";
import type { RenderImage } from "react-page-decorator";

const renderImage: RenderImage = ({ src, width, height, style }) =>
  height ? (
    <Image src={src} alt="" width={width} height={height} style={style} />
  ) : (
    <img src={src} alt="" loading="lazy" decoding="async" style={style} />
  );

<DecorationCanvas items={ITEMS} adapter={adapter} renderImage={renderImage} />;

height is only present once a decoration has been opened in the editor (which captures the aspect ratio) and saved. Until then the fallback <img> is used.

Using the editor

Open your page with ?edit=1 (dev). A panel appears bottom-right:

  • Add — type or pick an image path, click Add. It drops at the center of your current view, already selected.
  • Move — drag the image body.
  • Resize — drag the square handle (bottom-right corner).
  • Rotate — drag the round handle (top-right corner).
  • Tune — the panel edits width, rotation, opacity and z-index of the selected decoration; Delete removes it.
  • Responsive — the chip shows the active breakpoint (Desktop/Mobile). Resize your window narrow (<768px) to edit the mobile placement; mobile inherits desktop until first edited at mobile width.
  • Save — persists the whole set via your adapter.

Anchoring to an element

By default a decoration is positioned relative to the whole page. To pin it to a specific element (so it stays put as content above it reflows), give it an anchor:

  1. Select the decoration, then click Pick in the panel.
  2. Hover the page — elements highlight; click the one you want. A CSS selector is generated and stored as the decoration's anchor. (Or type a selector directly; blank = whole page. Esc cancels picking.)

The image is then portaled into that element and positioned as a percentage of its box. Anchored decorations render client-side (portals need the live DOM), so they appear just after hydration — see Rendering below.

Rendering: instant vs. deferred

Whether a decoration is in the initial server HTML (instant) or appears just after hydration (a brief pop-in) depends on how it's set up:

| Setup | Instant (SSR)? | | --- | --- | | DecorationCanvas + committed items (a JSON file), page-relative | ✅ yes | | localStorageAdapter | ❌ data lives in the browser, not the server | | DecorationAuto | ❌ it self-mounts via a client-only portal | | Anchored decoration | ❌ portals are client-only |

For fully instant images: use <DecorationCanvas> with committed items, mounted inside a position: relative wrapper, and keep the decoration page-relative (no anchor). That's the only combination the server can render up front — localStorage and portals are inherently client-side.

API

| Export | What | | --- | --- | | DecorationAuto | Zero-config: self-mounts to <body>, localStorage by default, auto-opens on ?edit=1 (client-rendered). | | DecorationCanvas | The main component: renderer + editor + adapter wiring. Props: items, adapter, enabled?, assets?/assetsLoader?, renderImage?. SSR-friendly. | | Decorations | Renderer only (if you want to gate the editor yourself). | | localStorageAdapter(key?) / restAdapter({ saveUrl, loadUrl?, init? }) | Storage adapters. | | renderImage prop + RenderImage type | Swap the default <img> for an optimized component (e.g. next/image). | | normalizeMany / normalizeDecoration | Coerce stored JSON into valid Decoration[]. | | resolveDecoration, decorationStyle, breakpointForWidth, BREAKPOINTS | Lower-level helpers. | | Types: Decoration, Placement, ResolvedDecoration, BreakpointKey, StorageAdapter | |

Notes

  • Mount the canvas inside a position: relative page wrapper so the absolute layer spans the whole page.
  • Decorations are pointer-events: none in view mode — they never block clicks.
  • Edit the breakpoint you want by resizing the window; the panel shows which one is active. Mobile inherits desktop until first edited at mobile width.