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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@magic-spells/cart-panel

v0.3.1

Published

Accessible modal shopping cart dialog web component with Shopify integration, focus management, and smooth animations.

Readme

Cart Panel Web Component

A professional, highly-customizable modal shopping cart dialog built with Web Components. Features accessible modal interactions, smooth slide-in animations, real-time cart synchronization, and seamless integration with Shopify and other e-commerce platforms.

Live Demo

Features

  • 🛒 Complete cart modal - Slide-in panel with overlay and focus management
  • Accessibility-first - ARIA attributes, focus trapping, and keyboard navigation
  • 🔄 Real-time sync - Automatic cart updates via /cart.json and /cart/change.json APIs
  • 📡 Event-driven architecture - Rich event system with custom event emitter
  • 🎬 Smooth animations - CSS transitions with customizable timing and effects
  • 🔒 Body scroll locking - Prevents background scrolling when modal is open
  • 🎛️ Highly customizable - CSS custom properties and SCSS variables
  • 📱 Framework agnostic - Pure Web Components work with any framework
  • 🛒 Shopify-ready - Built specifically for Shopify cart integrations

Installation

npm install @magic-spells/cart-panel
// Import the component (includes cart-item automatically)
import '@magic-spells/cart-panel';

// Import styles (includes cart-item styles automatically)
import '@magic-spells/cart-panel/css';

Or include directly in your HTML:

<script src="https://unpkg.com/@magic-spells/cart-panel"></script>
<link rel="stylesheet" href="https://unpkg.com/@magic-spells/cart-panel/dist/cart-panel.css" />

Usage

<!-- Trigger button -->
<button aria-haspopup="dialog" aria-controls="my-cart" aria-expanded="false">
  Open Cart (3 items)
</button>

<!-- Cart modal dialog -->
<cart-dialog id="my-cart" aria-labelledby="cart-title">
  <cart-panel>
    <div class="cart-header">
      <h2 id="cart-title">Shopping Cart</h2>
      <button data-action="hide-cart" aria-label="Close cart">&times;</button>
    </div>

    <div class="cart-body">
      <!-- Cart items using @magic-spells/cart-item -->
      <cart-item data-key="shopify-line-item-123">
        <cart-item-content>
          <div class="product-info">
            <img src="product.jpg" alt="Product" />
            <div>
              <h4>Awesome T-Shirt</h4>
              <div class="price">$29.99</div>
            </div>
          </div>
          <div class="quantity-controls">
            <input type="number" data-cart-quantity value="1" min="1" />
            <button data-action="remove">Remove</button>
          </div>
        </cart-item-content>
        <cart-item-processing>
          <div>Processing...</div>
        </cart-item-processing>
      </cart-item>
    </div>

    <div class="cart-footer">
      <div class="cart-total">Total: $29.99</div>
      <button class="checkout-btn">Checkout</button>
    </div>
  </cart-panel>
</cart-dialog>

How It Works

The cart panel component creates a complete modal cart experience with three main elements:

  • cart-dialog: Main container managing modal state, focus trapping, and scroll locking
  • cart-overlay: Clickable backdrop that closes the modal when clicked
  • cart-panel: Sliding content area that contains the actual cart items and controls

The component automatically handles:

  • Opening when trigger buttons with aria-controls are clicked
  • Closing via close buttons, escape key, or overlay clicks
  • Fetching cart data from /cart.json on show
  • Updating cart items via /cart/change.json API calls
  • Managing cart item states and animations through integrated @magic-spells/cart-item
  • Filtering out cart items with _hidden property from display and calculations
  • Emitting events for cart updates and state changes

Configuration

Cart Dialog Attributes

| Attribute | Description | Required | | ----------------- | ----------------------------------------------- | ----------- | | id | Unique identifier referenced by trigger buttons | Yes | | aria-labelledby | References the cart title element | Recommended | | aria-modal | Set to "true" for proper modal semantics | Recommended |

Required HTML Structure

| Element | Description | Required | | ---------------- | -------------------------------------------- | -------- | | <cart-dialog> | Main modal container | Yes | | <cart-panel> | Sliding content area | Yes | | <cart-overlay> | Background overlay (auto-created if missing) | No |

Interactive Elements

| Selector | Description | Event Triggered | | --------------------------- | ----------------------------------- | --------------------------- | | [aria-controls="cart-id"] | Trigger buttons to open cart | Opens modal | | [data-action="hide-cart"] | Close buttons inside modal | Closes modal | | [data-action="remove"] | Remove item buttons (via cart-item) | cart-item:remove | | [data-cart-quantity] | Quantity inputs (via cart-item) | cart-item:quantity-change |

Example:

<!-- Minimal cart modal -->
<cart-dialog id="simple-cart">
  <cart-panel>
    <h2>Cart</h2>
    <button data-action="hide-cart">Close</button>
    <!-- Cart content here -->
  </cart-panel>
</cart-dialog>

<!-- Complete cart with all features -->
<cart-dialog id="full-cart" aria-modal="true" aria-labelledby="cart-heading">
  <cart-overlay></cart-overlay>
  <cart-panel>
    <header class="cart-header">
      <h2 id="cart-heading">Shopping Cart</h2>
      <button data-action="hide-cart" aria-label="Close cart">×</button>
    </header>
    <div class="cart-content">
      <!-- Cart items will be rendered here -->
    </div>
    <footer class="cart-footer">
      <button class="checkout-btn">Checkout</button>
    </footer>
  </cart-panel>
</cart-dialog>

Customization

Styling

The component provides complete styling control through CSS custom properties and SCSS variables. Customize the modal appearance to match your design:

/* Customize modal positioning and sizing */
cart-dialog {
  --cart-panel-width: min(500px, 95vw);
  --cart-panel-z-index: 9999;
  --cart-overlay-z-index: 9998;
}

/* Customize overlay appearance */
cart-overlay {
  --cart-overlay-background: rgba(0, 0, 0, 0.3);
  --cart-overlay-backdrop-filter: blur(8px);
}

/* Customize panel styling */
cart-panel {
  --cart-panel-background: #ffffff;
  --cart-panel-shadow: -10px 0 30px rgba(0, 0, 0, 0.2);
  --cart-panel-border-radius: 12px 0 0 12px;
}

/* Customize animations */
cart-dialog {
  --cart-transition-duration: 400ms;
  --cart-transition-timing: cubic-bezier(0.25, 0.8, 0.25, 1);
}

/* Style your cart content layout */
cart-panel {
  display: flex;
  flex-direction: column;
}

.cart-header {
  padding: 1.5rem;
  border-bottom: 1px solid #eee;
  background: #f8f9fa;
}

.cart-content {
  flex: 1;
  overflow-y: auto;
  padding: 1rem;
}

.cart-footer {
  padding: 1.5rem;
  border-top: 1px solid #eee;
  background: #f8f9fa;
}

CSS Variables & SCSS Variables

The component supports both CSS custom properties and SCSS variables for maximum flexibility:

| CSS Variable | SCSS Variable | Description | Default | | -------------------------------- | ------------------------------- | ---------------------------- | ---------------------------- | | --cart-dialog-z-index | $cart-dialog-z-index | Base z-index for modal | 1000 | | --cart-overlay-z-index | $cart-overlay-z-index | Overlay layer z-index | 1000 | | --cart-panel-z-index | $cart-panel-z-index | Panel layer z-index | 1001 | | --cart-panel-width | $cart-panel-width | Width of the sliding panel | min(400px, 90vw) | | --cart-overlay-background | $cart-overlay-background | Overlay background color | rgba(0, 0, 0, 0.15) | | --cart-overlay-backdrop-filter | $cart-overlay-backdrop-filter | Overlay backdrop blur effect | blur(4px) | | --cart-panel-background | $cart-panel-background | Panel background color | #ffffff | | --cart-panel-shadow | $cart-panel-shadow | Panel box shadow | -5px 0 25px rgba(0,0,0,0.15) | | --cart-panel-border-radius | $cart-panel-border-radius | Panel border radius | 0 | | --cart-transition-duration | $cart-transition-duration | Animation duration | 350ms | | --cart-transition-timing | $cart-transition-timing | Animation timing function | cubic-bezier(0.4, 0, 0.2, 1) |

CSS Override Examples:

/* Dramatic slide-in effect */
.dramatic-cart {
  --cart-transition-duration: 600ms;
  --cart-transition-timing: cubic-bezier(0.68, -0.55, 0.265, 1.55);
  --cart-overlay-background: rgba(0, 0, 0, 0.4);
  --cart-overlay-backdrop-filter: blur(10px);
}

/* Subtle minimal styling */
.minimal-cart {
  --cart-panel-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  --cart-panel-border-radius: 8px;
  --cart-transition-duration: 200ms;
  --cart-overlay-background: rgba(0, 0, 0, 0.05);
}

/* Mobile-optimized full-width */
@media (max-width: 768px) {
  .mobile-cart {
    --cart-panel-width: 100vw;
    --cart-panel-border-radius: 0;
  }
}

SCSS Override Examples:

// Override SCSS variables before importing
$cart-panel-width: min(500px, 95vw);
$cart-transition-duration: 400ms;
$cart-overlay-background: rgba(0, 0, 0, 0.25);

// Import the component styles
@import '@magic-spells/cart-panel/scss';

// Or import the CSS and override with CSS custom properties
@import '@magic-spells/cart-panel/css';

.my-store cart-dialog {
  --cart-transition-duration: 400ms;
  --cart-panel-background: #f8f9fa;
}

JavaScript API

Methods

  • show(triggerElement): Open the cart modal and focus the first interactive element
  • hide(): Close the cart modal and restore focus to trigger element
  • getCart(): Fetch current cart data from /cart.json
  • updateCartItem(key, quantity): Update cart item quantity via /cart/change.json
  • refreshCart(): Refresh cart data and update UI components
  • on(eventName, callback): Add event listener using the event emitter
  • off(eventName, callback): Remove event listener

Events

The component emits custom events for cart state changes and data updates:

Modal Events:

  • cart-dialog:show - Modal has opened
  • cart-dialog:hide - Modal has started closing
  • cart-dialog:afterHide - Modal has finished closing animation

Cart Data Events:

  • cart-dialog:updated - Cart data updated after item change
  • cart-dialog:refreshed - Cart data refreshed from server
  • cart-dialog:data-changed - Any cart data change (unified event)

Cart Item Events (bubbled from cart-item components):

  • cart-item:remove - Remove button clicked: { cartKey, element }
  • cart-item:quantity-change - Quantity changed: { cartKey, quantity, element }

Programmatic Control

const cartDialog = document.querySelector('cart-dialog');

// Open/close cart
cartDialog.show(); // Open modal
cartDialog.hide(); // Close modal

// Cart data operations
const cartData = await cartDialog.getCart();
const updatedCart = await cartDialog.updateCartItem('item-key', 2);
await cartDialog.refreshCart();

// Event emitter pattern (recommended)
cartDialog
  .on('cart-dialog:show', (e) => {
    console.log('Cart opened by:', e.detail.triggerElement);
  })
  .on('cart-dialog:data-changed', (cartData) => {
    console.log('Cart updated:', cartData);
    // Update header cart count, etc.
  });

// Traditional event listeners (also supported)
cartDialog.addEventListener('cart-item:remove', (e) => {
  console.log('Remove requested:', e.detail.cartKey);

  // The component handles the API calls automatically
  // Just listen for the data changes
});

cartDialog.addEventListener('cart-item:quantity-change', (e) => {
  console.log('Quantity changed:', e.detail.quantity);
  // Component automatically syncs with Shopify
});

// Listen for all cart changes
cartDialog.on('cart-dialog:data-changed', (cartData) => {
  // Update your UI when cart changes
  updateCartBadge(cartData.item_count);
  updateCartTotal(cartData.total_price);
});

Performance & Architecture

The component is optimized for:

  • Smooth animations: CSS transforms and transitions for slide-in effects
  • Focus management: Automatic focus trapping with @magic-spells/focus-trap
  • Memory management: Proper event listener cleanup on disconnect
  • Scroll lock: Body scroll prevention with position restoration
  • API efficiency: Smart cart data fetching and caching
  • Event system: Centralized event handling with custom event emitter
  • Accessibility: Full ARIA support and keyboard navigation

Integration Examples

Line Item Properties

The cart panel supports several Shopify line item properties for enhanced functionality:

Cart Item Filtering (_hide_in_cart)

Cart items can be hidden from display by setting the _hide_in_cart property. Hidden items are excluded from:

  • Cart item display and rendering
  • Cart count calculations
  • Subtotal calculations
// Example: Hide a cart item from display
{
  "items": [
    {
      "key": "item-123",
      "properties": {
        "_hide_in_cart": "true"  // Hide from cart display
      }
    }
  ]
}

Custom Templates (_cart_template)

Different cart item templates can be specified using the _cart_template property:

// Example: Use different templates for different item types
{
  "items": [
    {
      "key": "subscription-item",
      "properties": {
        "_cart_template": "subscription"  // Use subscription template
      }
    },
    {
      "key": "bundle-item",
      "properties": {
        "_cart_template": "bundle"  // Use bundle template
      }
    }
  ]
}

Then set up custom templates in JavaScript:

import { CartItem } from '@magic-spells/cart-panel';

// Set up different templates
CartItem.setTemplate('subscription', (itemData, cartData) => {
  return `
    <div class="subscription-item">
      <div class="recurring-badge">🔄 Subscription</div>
      <h4>${itemData.product_title}</h4>
      <div class="price">$${(itemData.price / 100).toFixed(2)} every month</div>
      <quantity-modifier value="${itemData.quantity}"></quantity-modifier>
    </div>
  `;
});

CartItem.setTemplate('bundle', (itemData, cartData) => {
  return `
    <div class="bundle-item">
      <div class="bundle-badge">📦 Bundle Deal</div>
      <h4>${itemData.product_title}</h4>
      <div class="savings">Save 20%!</div>
      <div class="price">$${(itemData.price / 100).toFixed(2)}</div>
    </div>
  `;
});

Item Grouping (_group_id and _group_role)

Items can be grouped together using _group_id and _group_role properties. This is commonly used for bundle products where multiple items should be displayed as a single unit.

Use Cases:

  • Bundle products (main product + accessories)
  • Gift with purchase promotions
  • Subscription boxes with multiple items
  • Product kits and sets

How it works:

  1. All items in a group share the same _group_id (a unique identifier like a UUID)
  2. One item has _group_role: "parent" (typically with _cart_template: "bundle")
  3. Other items have _group_role: "child" (typically with _hide_in_cart: true)
  4. The bundle template renders all grouped items together in one display

Example usage:

// Bundle: T-shirt + Hat + Sticker (shown as one item in cart)
{
  "items": [
    {
      "key": "bundle-parent",
      "properties": {
        "_group_id": "Q6RT1B48",
        "_group_role": "parent",
        "_cart_template": "bundle"
      }
    },
    {
      "key": "bundle-child-1",
      "properties": {
        "_group_id": "Q6RT1B48", 
        "_group_role": "child",
        "_hide_in_cart": "true"
      }
    },
    {
      "key": "bundle-child-2", 
      "properties": {
        "_group_id": "Q6RT1B48",
        "_group_role": "child", 
        "_hide_in_cart": "true"
      }
    }
  ]
}

Bundle template example:

CartItem.setTemplate('bundle', (itemData, cartData) => {
  // Find all items in this group
  const groupId = itemData.properties._group_id;
  const groupItems = cartData.items.filter(item => 
    item.properties?._group_id === groupId
  );
  
  return `
    <div class="bundle-item">
      <div class="bundle-badge">📦 Bundle Deal</div>
      <h4>${itemData.product_title}</h4>
      <div class="bundle-contents">
        ${groupItems.map(item => `
          <div class="bundle-item-detail">
            • ${item.product_title} (${item.quantity})
          </div>
        `).join('')}
      </div>
      <div class="bundle-price">$${(groupItems.reduce((sum, item) => sum + item.line_price, 0) / 100).toFixed(2)}</div>
    </div>
  `;
});

Subtotal Exclusion (_ignore_price_in_subtotal)

Items can be excluded from subtotal calculations using the _ignore_price_in_subtotal property. This is useful for promotional items that receive automatic discounts at checkout.

Use Cases:

  • Gift with purchase items (free items that show $0 at checkout)
  • Promotional items with automatic discounts applied later
  • Service fees handled by other systems
  • Items with complex pricing logic

Usage:

// Gift with purchase item - shows in cart but excluded from subtotal
{
  "key": "gift-item",
  "properties": {
    "_ignore_price_in_subtotal": "true"
  }
}

Implementation: The cart panel automatically excludes these items when calculating visible subtotals, but they remain in the cart for Shopify's checkout process where discounts are applied.

Supported Properties

| Property | Purpose | Example Values | | -------------------------- | --------------------------------------------- | -------------------------------------- | | _hide_in_cart | Hide items from cart display | "true", true | | _cart_template | Specify custom template for rendering | "subscription", "bundle", "gift" | | _group_id | Group items together with shared UUID | "Q6RT1B48", "ABC123XYZ" | | _group_role | Role within a group | "parent", "child" | | _ignore_price_in_subtotal | Exclude from subtotal calculations | "true", true |

These properties follow Shopify's line item properties pattern and are commonly used for gift-with-purchase items, subscription products, bundles, and other special cart items.

Shopify Integration

The cart panel automatically integrates with Shopify's AJAX Cart API. Simply add the component to your theme and it handles all cart operations:

<!-- In your Shopify theme layout -->
<button
  aria-haspopup="dialog"
  aria-controls="shopify-cart"
  aria-expanded="false"
  class="cart-trigger">
  Cart ({{ cart.item_count }})
</button>

<cart-dialog id="shopify-cart" aria-labelledby="cart-heading">
  <cart-panel>
    <header class="cart-header">
      <h2 id="cart-heading">Your Cart</h2>
      <button data-action="hide-cart" aria-label="hide cart">X</button>
    </header>

    <div class="cart-content">
      <!-- Cart items will be populated automatically in javascript -->
    </div>

    <footer class="cart-footer">
      <div class="cart-total"></div>
      <a href="/checkout" class="button"> Checkout </a>
    </footer>
  </cart-panel>
</cart-dialog>

<script>
  // Optional: Listen for cart updates to sync with other UI elements
  document.querySelector('cart-dialog').on('cart-dialog:data-changed', (cartData) => {
    // Update cart count in header
    document.querySelector('.cart-trigger').textContent = `Cart (${cartData.item_count})`;

    // Update cart total
    document.querySelector('[data-cart-total]').textContent = new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'USD',
    }).format(cartData.total_price / 100);
  });
</script>

Vanilla JavaScript Integration

// Example for non-Shopify platforms
class CustomCartManager {
  constructor() {
    this.cartDialog = document.querySelector('cart-dialog');
    this.setupEventListeners();
  }

  setupEventListeners() {
    // Listen for cart data changes
    this.cartDialog.on('cart-dialog:data-changed', (cartData) => {
      this.updateCartUI(cartData);
    });

    // Override default cart operations for custom API
    this.cartDialog.getCart = this.customGetCart.bind(this);
    this.cartDialog.updateCartItem = this.customUpdateCartItem.bind(this);
  }

  async customGetCart() {
    try {
      const response = await fetch('/api/cart');
      return await response.json();
    } catch (error) {
      console.error('Failed to fetch cart:', error);
      return { error: true, message: error.message };
    }
  }

  async customUpdateCartItem(itemId, quantity) {
    try {
      const response = await fetch('/api/cart/update', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ itemId, quantity }),
      });

      if (!response.ok) throw new Error(response.statusText);

      // Return updated cart data
      return this.customGetCart();
    } catch (error) {
      console.error('Failed to update cart:', error);
      return { error: true, message: error.message };
    }
  }

  updateCartUI(cartData) {
    // Update cart count in navigation
    const cartCount = document.querySelector('.cart-count');
    if (cartCount) {
      cartCount.textContent = cartData.items?.length || 0;
    }

    // Update cart total display
    const cartTotal = document.querySelector('.cart-total-display');
    if (cartTotal && cartData.total) {
      cartTotal.textContent = cartData.total;
    }
  }
}

// Initialize
new CustomCartManager();

Browser Support

  • Chrome 54+
  • Firefox 63+
  • Safari 10.1+
  • Edge 79+

All modern browsers with Web Components support.

License

MIT