@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
storageevent 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/wishlistOr 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 UIThe 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 entryBranding / 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 concurrentlyOutput 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
