Appearance
Integration Examples
End-to-end patterns for integrating SaveLayer into different storefronts. For individual method references, see SDK Code Examples.
Liquid template: product card save button
Add a save/unsave button to any product card in your Shopify theme. Requires the SaveLayer theme app embed to be enabled.
Snippet file
Create snippets/savelayer-heart.liquid:
liquid
{% comment %}
SaveLayer heart button.
Usage: {% render 'savelayer-heart', product: product %}
{% endcomment %}
<button
type="button"
class="sl-heart"
aria-label="Toggle wishlist"
aria-pressed="false"
data-sl-toggle
data-context="wishlist"
data-entity-type="product"
data-entity-gid="gid://shopify/Product/{{ product.id }}"
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78L12 21.23l8.84-8.84a5.5 5.5 0 0 0 0-7.78z"/>
</svg>
</button>JavaScript (inline or in a theme JS file)
js
document.addEventListener('DOMContentLoaded', () => {
// Toggle on click
document.addEventListener('click', async (e) => {
const btn = e.target.closest('[data-sl-toggle]');
if (!btn || !window.SaveLayer) return;
btn.disabled = true;
const result = await window.SaveLayer.toggle({
context: btn.dataset.context,
entityType: btn.dataset.entityType,
entityGid: btn.dataset.entityGid,
source: 'theme-card',
});
btn.disabled = false;
if (result.ok) {
const saved = result.data.state === 'active';
btn.classList.toggle('sl-heart--active', saved);
btn.setAttribute('aria-pressed', String(saved));
} else if (result.error.code === 'AUTH_REQUIRED') {
window.location.href = '/account/login';
}
});
// Set initial state for visible hearts
document.querySelectorAll('[data-sl-toggle]').forEach(async (btn) => {
if (!window.SaveLayer) return;
const result = await window.SaveLayer.isSaved({
context: btn.dataset.context,
entityType: btn.dataset.entityType,
entityGid: btn.dataset.entityGid,
});
if (result.ok && result.data.saved) {
btn.classList.add('sl-heart--active');
btn.setAttribute('aria-pressed', 'true');
}
});
});CSS
css
.sl-heart {
background: none;
border: none;
cursor: pointer;
color: #999;
transition: color 0.2s;
}
.sl-heart:hover {
color: #e74c3c;
}
.sl-heart--active {
color: #e74c3c;
}
.sl-heart--active svg {
fill: currentColor;
}Usage in a product card
liquid
{% for product in collection.products %}
<div class="product-card">
<a href="{{ product.url }}">{{ product.title }}</a>
{% render 'savelayer-heart', product: product %}
</div>
{% endfor %}Headless React / Next.js: wishlist component
For headless storefronts, use the SaveLayerSdk class from @savelayer/sdk directly. Headless stores authenticate through the direct API with JWT tokens instead of the app proxy.
SDK setup
tsx
// lib/savelayer.ts
import { SaveLayerSdk } from '@savelayer/sdk';
export const savelayer = new SaveLayerSdk({
apiBase: process.env.NEXT_PUBLIC_SAVELAYER_API_BASE!,
channel: 'headless',
});Wishlist hook
tsx
// hooks/use-wishlist.ts
import { useState, useEffect, useCallback } from 'react';
import { savelayer } from '../lib/savelayer';
interface WishlistItem {
id: string;
entityType: string;
entityGid: string;
status: string;
savedAt?: string;
}
export function useWishlist(context = 'wishlist') {
const [items, setItems] = useState<WishlistItem[]>([]);
const [loading, setLoading] = useState(true);
const [hasNextPage, setHasNextPage] = useState(false);
const [cursor, setCursor] = useState<string | null>(null);
const fetchItems = useCallback(async (pageCursor?: string) => {
setLoading(true);
const result = await savelayer.list({
context,
limit: 20,
...(pageCursor ? { cursor: pageCursor } : {}),
});
if (result.ok) {
if (pageCursor) {
setItems((prev) => [...prev, ...result.data.items]);
} else {
setItems(result.data.items);
}
setHasNextPage(result.data.pageInfo.hasNextPage);
setCursor(result.data.pageInfo.endCursor);
}
setLoading(false);
}, [context]);
useEffect(() => {
fetchItems();
}, [fetchItems]);
const loadMore = () => {
if (cursor) fetchItems(cursor);
};
const removeItem = async (entityGid: string) => {
const result = await savelayer.remove({
context,
entityType: 'product',
entityGid,
});
if (result.ok) {
setItems((prev) => prev.filter((i) => i.entityGid !== entityGid));
}
};
return { items, loading, hasNextPage, loadMore, removeItem, refetch: () => fetchItems() };
}Wishlist page component
tsx
// components/WishlistPage.tsx
import { useWishlist } from '../hooks/use-wishlist';
export function WishlistPage() {
const { items, loading, hasNextPage, loadMore, removeItem } = useWishlist();
if (loading && items.length === 0) {
return <p>Loading your wishlist...</p>;
}
if (items.length === 0) {
return <p>Your wishlist is empty.</p>;
}
return (
<div>
<h1>My Wishlist</h1>
<ul>
{items.map((item) => (
<li key={item.id}>
<span>{item.entityGid}</span>
<button onClick={() => removeItem(item.entityGid)}>Remove</button>
</li>
))}
</ul>
{hasNextPage && (
<button onClick={loadMore} disabled={loading}>
{loading ? 'Loading...' : 'Load more'}
</button>
)}
</div>
);
}Toggle button component
tsx
// components/SaveButton.tsx
import { useState, useEffect } from 'react';
import { savelayer } from '../lib/savelayer';
interface SaveButtonProps {
entityGid: string;
entityType?: string;
context?: string;
}
export function SaveButton({
entityGid,
entityType = 'product',
context = 'wishlist',
}: SaveButtonProps) {
const [saved, setSaved] = useState(false);
const [busy, setBusy] = useState(false);
useEffect(() => {
savelayer
.isSaved({ context, entityType, entityGid })
.then((r) => {
if (r.ok) setSaved(r.data.saved);
});
}, [context, entityType, entityGid]);
const handleToggle = async () => {
setBusy(true);
const result = await savelayer.toggle({
context,
entityType,
entityGid,
source: 'headless',
});
if (result.ok) {
setSaved(result.data.state === 'active');
}
setBusy(false);
};
return (
<button onClick={handleToggle} disabled={busy} aria-pressed={saved}>
{saved ? 'Saved' : 'Save'}
</button>
);
}Custom event handling for analytics
Use SaveLayer.on() to forward save events to your analytics provider. These listeners fire in the browser after successful API calls, alongside Shopify's customer events pipeline.
Google Analytics 4 (gtag)
js
window.SaveLayer?.on('savelayer:item_saved', (payload) => {
gtag('event', 'add_to_wishlist', {
items: [{
item_id: payload.entityGid,
item_category: payload.entityType,
}],
});
});
window.SaveLayer?.on('savelayer:item_removed', (payload) => {
gtag('event', 'remove_from_wishlist', {
items: [{
item_id: payload.entityGid,
item_category: payload.entityType,
}],
});
});Generic analytics wrapper
js
function trackSaveLayerEvents() {
if (!window.SaveLayer) return;
const events = [
'savelayer:item_saved',
'savelayer:item_removed',
'savelayer:item_toggled',
'savelayer:list_viewed',
];
events.forEach((event) => {
window.SaveLayer.on(event, (payload) => {
// Replace with your analytics call
console.log('[SaveLayer]', event, payload);
// Example: send to a custom endpoint
navigator.sendBeacon('/analytics/collect', JSON.stringify({
event,
payload,
timestamp: Date.now(),
}));
});
});
}
// Call after DOM ready
document.addEventListener('DOMContentLoaded', trackSaveLayerEvents);Combining with Shopify Web Pixels
SDK on() listeners run in the storefront page context. For sandbox-isolated analytics (ad pixels, third-party scripts), use Shopify Web Pixels instead — see Customer Events for setup. Both channels receive the same event payloads, so choose based on your analytics provider's requirements:
| Channel | Runs in | Best for |
|---|---|---|
SaveLayer.on() | Page context | First-party analytics, UI updates, custom dashboards |
Web Pixel analytics.subscribe() | Sandbox | Ad platforms (Meta, Google Ads), third-party analytics |