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

@madcoders/wishlist

v0.1.3

Published

Vanilla JS wishlist widget library with localStorage, backend sync, and cross-tab support

Readme

@madcoders/wishlist

Vanilla JS wishlist widget library with localStorage-first architecture, backend sync, and cross-tab/cross-browser synchronization.

Features

  • Zero dependencies — pure vanilla JS, no frameworks
  • localStorage-first — UI always renders from localStorage, instant and offline-ready
  • Widget-based — drop data-wishlist-* attributes in your HTML, the library does the rest
  • Cross-tab sync — BroadcastChannel with storage event fallback
  • Backend sync — optimistic UI with SharedWorker coordination for consistent state
  • Deferred loading — load with <script defer>, never blocks rendering
  • Multi-wishlist ready — data structure supports multiple wishlists (default list implemented)

Quick Start

1. Install

npm install @madcoders/wishlist

Or use the built files directly:

<link rel="stylesheet" href="path/to/wishlist.css">
<script defer src="path/to/wishlist.js"></script>

2. Configure

<script defer>
  document.addEventListener('DOMContentLoaded', () => {
    Wishlist.configure({
      apiBaseUrl: 'https://your-api.com/api',   // optional, for backend sync
      workerUrl: '/path/to/wishlist-sync-worker.js', // optional, for SharedWorker sync
      debug: false,
    });

    // For logged-in users:
    Wishlist.setUser('user-123');
  });
</script>

3. Add Widgets to Your HTML

Toggle Button (Product Listing / Product Detail Page)

<button class="wishlist-toggle"
        data-wishlist-toggle="product-123"
        data-wishlist-meta='{"name":"Product Name","image":"/img/product.jpg","price":29.99}'>
</button>

The library auto-injects the heart SVG icon and manages the wishlist-toggle--active class.

Counter Badge (Header)

<span data-wishlist-counter></span>

Displays the current item count. Updates automatically on add/remove.

Wishlist Overview (Wishlist Page)

<div data-wishlist-overview data-wishlist-list="default"></div>

Renders the full item list with quantity editing, remove buttons, and loading state during sync.

JavaScript API

// Configuration
Wishlist.configure({ apiBaseUrl, workerUrl, endpoints, transformRequest, transformResponse, debug })
Wishlist.setUser(userId)       // Enable backend sync for logged-in user
Wishlist.clearUser()           // Switch to anonymous mode (localStorage only)

// CRUD operations
Wishlist.add(productId, { listId?, qty?, meta? })
Wishlist.remove(productId, { listId? })
Wishlist.toggle(productId, { listId?, meta? })   // Add if absent, remove if present
Wishlist.updateQty(productId, qty, { listId? })
Wishlist.has(productId, { listId? })              // Returns boolean
Wishlist.getItems({ listId? })                    // Returns array of items
Wishlist.getCount({ listId? })                    // Returns number
Wishlist.clear({ listId? })

// Utilities
Wishlist.refresh()                          // Re-scan DOM for new widgets
Wishlist.sync()                             // Force backend sync
Wishlist.registerWidget(selector, Class)    // Register a custom widget

// Events
Wishlist.on(eventName, callback)
Wishlist.off(eventName, callback)

Events

All events are CustomEvent instances dispatched on document. Listen with document.addEventListener() or Wishlist.on().

| Event | When | detail | |---|---|---| | wishlist:added | Item added | { productId, listId, item } | | wishlist:removed | Item removed | { productId, listId } | | wishlist:changed | Any mutation | { listId, source } — source: "local", "tab", or "sync" | | wishlist:loading | Backend sync starts | { listId } | | wishlist:synced | Backend sync done | { listId } | | wishlist:sync-error | Backend sync failed | { listId, error } |

Example: Custom Event Listener

document.addEventListener('wishlist:added', (e) => {
  console.log(`Added ${e.detail.productId} to wishlist`);
  showToast(`${e.detail.item.meta.name} added to wishlist!`);
});

Widget HTML Attributes

| Attribute | Used On | Description | |---|---|---| | data-wishlist-toggle="<productId>" | <button> | Makes element a wishlist toggle for the given product | | data-wishlist-meta='<json>' | Toggle button | Product metadata to store (name, image, price) | | data-wishlist-list="<listId>" | Any widget | Which wishlist to use (default: "default") | | data-wishlist-counter | <span> | Displays item count | | data-wishlist-hide-empty="true" | Counter | Hide badge when count is 0 | | data-wishlist-overview | <div> | Renders full wishlist with edit/remove controls |

CSS Classes

| Class | Description | |---|---| | .wishlist-toggle | Base class for toggle buttons | | .wishlist-toggle--active | Applied when item is in wishlist | | .wishlist-toggle--syncing | Applied during backend sync (heart blink animation) | | .wishlist-toggle__icon | The heart SVG icon |

All styles use BEM naming and are easily overridable.

CSS Themes

Three CSS themes are provided. Pick the one that matches your project:

| Theme | File | Description | |---|---|---| | Default | wishlist.css | Standalone, no dependencies | | Bootstrap | wishlist-bootstrap.css | Uses Bootstrap 5 CSS variables (--bs-*) | | Semantic UI | wishlist-semanticui.css | Matches Semantic UI / Fomantic UI conventions |

Usage

Via <link> tag — pick one:

<!-- Default (standalone) -->
<link rel="stylesheet" href="path/to/wishlist.css">

<!-- Bootstrap 5 (load after bootstrap.css) -->
<link rel="stylesheet" href="path/to/wishlist-bootstrap.css">

<!-- Semantic UI (load after semantic.css) -->
<link rel="stylesheet" href="path/to/wishlist-semanticui.css">

Via npm import — pick one:

import '@madcoders/wishlist/styles';              // default
import '@madcoders/wishlist/styles/bootstrap';    // Bootstrap 5
import '@madcoders/wishlist/styles/semanticui';   // Semantic UI

The Bootstrap theme uses var(--bs-*) custom properties so it automatically follows your Bootstrap theming (including dark mode). The Semantic UI theme uses Semantic's standard colors, border-radius, and shadow conventions.

SCSS Source Files

Raw SCSS files are included in the package for full customization. The structure is modular — import only the components you need.

scss/
├── _variables.scss                # All customizable variables with !default
├── _mixins.scss                   # Shared mixins
├── components/
│   ├── _index.scss                # All components (convenience import)
│   ├── _toggle.scss               # Heart toggle button
│   ├── _counter.scss              # Counter badge
│   ├── _overview.scss             # Wishlist overview container
│   ├── _item.scss                 # Wishlist item row (image, name, qty, remove)
│   └── _loading.scss              # Loading spinner
├── wishlist-default.scss          # Default theme entry
├── wishlist-bootstrap.scss        # Bootstrap 5 theme entry
└── wishlist-semanticui.scss       # Semantic UI theme entry

Branding / Custom Theme

Override variables before importing to customize:

// your-project.scss
$wishlist-color-active: #ff6b6b;     // your brand color for hearts
$wishlist-border-radius: 12px;       // rounder cards
$wishlist-font-weight-bold: 700;     // bolder names

@import '@madcoders/wishlist/scss/wishlist-default';

Key Branding Variables

| Variable | Default | Description | |---|---|---| | $wishlist-color-active | #e53e3e | Active heart color, badge background | | $wishlist-color-inactive | #ccc | Inactive heart fill | | $wishlist-focus-color | #4299e1 | Input focus ring color | | $wishlist-spinner-color | #4299e1 | Loading spinner color | | $wishlist-border-radius | 8px | Item card border radius | | $wishlist-border-color | #e2e8f0 | Border color for cards, inputs | | $wishlist-item-bg | #fff | Item card background | | $wishlist-text-primary | #1a202c | Product name text color | | $wishlist-text-secondary | #718096 | Price / label text color | | $wishlist-badge-border-radius | 10px | Badge pill shape | | $wishlist-item-image-size | 64px | Thumbnail size in overview |

See scss/_variables.scss for the full list of 40+ customizable variables.

Importing Individual Components

If you only need specific widgets, import them directly:

// Only the heart toggle and counter badge — no overview/item styles
@import '@madcoders/wishlist/scss/variables';
@import '@madcoders/wishlist/scss/mixins';
@import '@madcoders/wishlist/scss/components/toggle';
@import '@madcoders/wishlist/scss/components/counter';

Configurable Backend

Override API endpoint paths and data transforms to integrate with any backend:

Wishlist.configure({
  apiBaseUrl: 'https://my-api.com/v2',
  endpoints: {
    getList:     (listId) => ({ method: 'GET',    path: `/favorites/${listId}` }),
    replaceList: (listId) => ({ method: 'PUT',    path: `/favorites/${listId}` }),
    addItem:     (listId) => ({ method: 'POST',   path: `/favorites/${listId}/products` }),
    removeItem:  (listId, pid) => ({ method: 'DELETE', path: `/favorites/${listId}/products/${pid}` }),
    updateItem:  (listId, pid) => ({ method: 'PATCH',  path: `/favorites/${listId}/products/${pid}` }),
  },
  // Optional: transform data before sending / after receiving
  transformRequest: (action, data) => {
    if (action === 'addItem') return { sku: data.productId, quantity: data.qty };
    return data;
  },
  transformResponse: (action, data) => data,
});

Default endpoints match the PoC backend (/wishlists/:listId/items). Override only the ones you need.

Custom Widgets

Register a widget class

class RecentlyAddedWidget {
  constructor(el) {
    this.el = el;
    document.addEventListener('wishlist:changed', () => this.render());
    this.render();
  }
  render() {
    const items = Wishlist.getItems().slice(-3);
    this.el.innerHTML = items.map(i => `<span>${i.meta.name}</span>`).join(', ');
  }
  destroy() {}
}

Wishlist.registerWidget('[data-wishlist-recent]', RecentlyAddedWidget);
<div data-wishlist-recent></div>

Data-only integration (no widget)

Use the JS API and events directly without registering widgets:

document.addEventListener('wishlist:changed', () => {
  const count = Wishlist.getCount();
  myCustomHeader.updateBadge(count);
});

Architecture

Modes

Anonymous mode (default): All data stored in localStorage only. No network requests.

Logged-in mode (Wishlist.setUser(id)): localStorage remains the source of truth for UI. Backend sync runs in the background via SharedWorker. Optimistic updates — UI updates immediately, sync happens asynchronously.

Cross-Tab Synchronization

Same-origin tabs sync via BroadcastChannel (97% browser support). Falls back to the storage event for older browsers. Changes in one tab appear instantly in all other tabs.

Backend Synchronization

When workerUrl is provided and SharedWorker is supported, a single SharedWorker coordinates all backend communication:

  • Periodic polling (every 30s) ensures eventual consistency
  • Debounced mutations (500ms) batch rapid changes
  • Single coordinator — only the worker talks to the backend, preventing race conditions
  • Event-based updates — worker pushes updates to all connected tabs

Falls back to direct fetch() calls when SharedWorker is unavailable.

localStorage Schema

Key: wishlist_data

{
  "_version": 1,
  "_lastSyncedAt": "2026-03-22T10:00:00Z",
  "_userId": "user-123",
  "lists": {
    "default": {
      "id": "default",
      "name": "My Wishlist",
      "items": {
        "product-123": {
          "productId": "product-123",
          "qty": 1,
          "addedAt": "2026-03-22T09:30:00Z",
          "meta": { "name": "Product", "image": "/img/p.jpg", "price": 29.99 }
        }
      }
    }
  }
}

Backend API Contract

If you implement your own backend, it should expose these endpoints. Auth is via X-User-Id header (replace with your auth mechanism).

| Method | Path | Body | Response | |---|---|---|---| | GET | /api/wishlists/:listId | — | { id, name, items: { [productId]: item } } | | PUT | /api/wishlists/:listId | { items } | { id, name, items } | | POST | /api/wishlists/:listId/items | { productId, qty, meta } | { item } | | DELETE | /api/wishlists/:listId/items/:productId | — | 204 | | PATCH | /api/wishlists/:listId/items/:productId | { qty } | { item } |

Dynamic Content

For SPAs or pages with infinite scroll, call Wishlist.refresh() after injecting new product elements into the DOM. The library also uses MutationObserver to auto-detect new widgets, but refresh() is more reliable for batch insertions.

// After loading more products via AJAX:
container.innerHTML += newProductsHtml;
Wishlist.refresh();

Build

nvm use          # Uses Node 22 from .nvmrc
npm install
npm run build    # Production build
npm run dev      # Watch mode
npm run server   # Start PoC backend on :3000
npm run demo     # Watch + server concurrently

Output Files

| File | Format | Usage | |---|---|---| | dist/wishlist.js | IIFE | <script defer src="..."> | | dist/wishlist.esm.mjs | ESM | import { Wishlist } from '...' | | dist/wishlist-sync-worker.js | IIFE | SharedWorker for backend sync | | dist/wishlist.css | CSS | Default theme (standalone) | | dist/wishlist-bootstrap.css | CSS | Bootstrap 5 theme | | dist/wishlist-semanticui.css | CSS | Semantic UI theme |

Browser Support

  • All modern browsers (Chrome, Firefox, Safari, Edge)
  • BroadcastChannel: 97% (fallback to storage event)
  • SharedWorker: ~51% (fallback to direct fetch)
  • localStorage: universal