Appearance
SDK Code Examples
Practical, copy-pasteable examples for every window.SaveLayer method. The SDK is injected by the theme app embed and talks to SaveLayer through Shopify's signed app proxy.
All methods return a JSON envelope: { ok: true, data: ... } on success or { ok: false, error: { code, message, retryable } } on failure.
Quick start
A minimal save button in five lines:
js
document.querySelector('.save-btn')?.addEventListener('click', async () => {
const result = await window.SaveLayer.save({
context: 'wishlist',
entityType: 'product',
entityGid: 'gid://shopify/Product/123456789',
source: 'theme',
});
console.log(result); // { ok: true, data: { id, state: 'active' } }
});SaveLayer.save()
Save an item to a list. Creates the list if it does not exist.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
context | string | Yes | List identifier, e.g. 'wishlist' or 'favorites' |
entityType | string | Yes | One of: product, variant, collection, metaobject, article, page, configuration |
entityGid | string | Yes | Shopify global ID, e.g. 'gid://shopify/Product/123' |
source | string | Yes | Where the save originated, e.g. 'theme', 'pdp', 'collection-page' |
Example: save button on a product page
js
const saveBtn = document.getElementById('savelayer-save');
saveBtn?.addEventListener('click', async () => {
const result = await window.SaveLayer.save({
context: 'wishlist',
entityType: 'product',
entityGid: saveBtn.dataset.entityGid,
source: 'pdp',
});
if (result.ok) {
saveBtn.textContent = 'Saved!';
} else {
console.error(result.error.code, result.error.message);
}
});Success response
json
{
"ok": true,
"data": { "id": "abc123", "state": "active" }
}Duplicate save (409)
json
{
"ok": false,
"error": {
"code": "DUPLICATE_SAVE",
"message": "The item is already saved.",
"retryable": false
}
}SaveLayer.remove()
Remove a saved item from a list (soft-delete).
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
context | string | Yes | List identifier |
entityType | string | Yes | Entity type |
entityGid | string | Yes | Shopify global ID |
Example: remove button in a wishlist
js
async function removeItem(entityGid) {
const result = await window.SaveLayer.remove({
context: 'wishlist',
entityType: 'product',
entityGid,
});
if (result.ok) {
// Remove the item's DOM node
document.querySelector(`[data-gid="${entityGid}"]`)?.remove();
} else if (result.error.code === 'ITEM_NOT_FOUND') {
// Already removed — clean up the UI anyway
document.querySelector(`[data-gid="${entityGid}"]`)?.remove();
}
}Success response
json
{
"ok": true,
"data": { "id": "abc123", "state": "removed" }
}SaveLayer.toggle()
Toggle an item: saves it if not saved, removes it if already saved. Ideal for heart icons.
Parameters: Same as save() (includes source).
Example: heart icon toggle
js
const heart = document.querySelector('.sl-heart');
heart?.addEventListener('click', async () => {
const result = await window.SaveLayer.toggle({
context: 'wishlist',
entityType: 'product',
entityGid: heart.dataset.entityGid,
source: 'collection-page',
});
if (result.ok) {
const isSaved = result.data.state === 'active';
heart.classList.toggle('sl-heart--active', isSaved);
heart.setAttribute('aria-pressed', String(isSaved));
}
});Success response (toggled on)
json
{
"ok": true,
"data": { "id": "abc123", "state": "active" }
}Success response (toggled off)
json
{
"ok": true,
"data": { "id": "abc123", "state": "removed" }
}SaveLayer.list()
Fetch saved items for a given list context. Supports cursor-based pagination.
Parameters
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
context | string | Yes | — | List identifier |
limit | number | No | 20 | Items per page (1–100) |
cursor | string | No | — | Pagination cursor from a previous response |
Example: render a wishlist page
js
async function renderWishlist() {
const container = document.getElementById('wishlist-items');
const result = await window.SaveLayer.list({ context: 'wishlist', limit: 10 });
if (!result.ok) {
container.innerHTML = '<p>Could not load your wishlist.</p>';
return;
}
const { items, pageInfo } = result.data;
if (items.length === 0) {
container.innerHTML = '<p>Your wishlist is empty.</p>';
return;
}
container.innerHTML = items
.map(
(item) =>
`<div data-gid="${item.entityGid}">
<span>${item.entityType}: ${item.entityGid}</span>
<button onclick="removeItem('${item.entityGid}')">Remove</button>
</div>`
)
.join('');
// Show "Load more" if there are more pages
if (pageInfo.hasNextPage) {
const btn = document.createElement('button');
btn.textContent = 'Load more';
btn.addEventListener('click', () => loadMore(pageInfo.endCursor));
container.appendChild(btn);
}
}
async function loadMore(cursor) {
const result = await window.SaveLayer.list({
context: 'wishlist',
limit: 10,
cursor,
});
// Append items to the existing list...
}Success response
json
{
"ok": true,
"data": {
"items": [
{
"id": "abc123",
"entityType": "product",
"entityGid": "gid://shopify/Product/123",
"status": "active",
"savedAt": "2026-03-15T10:30:00Z"
}
],
"pageInfo": {
"hasNextPage": true,
"endCursor": "eyJpZCI6ImFiYzEyMyJ9"
}
}
}SaveLayer.isSaved()
Check whether an item is currently saved. Useful for setting initial UI state on page load.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
context | string | Yes | List identifier |
entityType | string | Yes | Entity type |
entityGid | string | Yes | Shopify global ID |
Example: set initial heart state on page load
js
document.addEventListener('DOMContentLoaded', async () => {
const hearts = document.querySelectorAll('[data-sl-check]');
for (const heart of hearts) {
const result = await window.SaveLayer.isSaved({
context: 'wishlist',
entityType: heart.dataset.entityType,
entityGid: heart.dataset.entityGid,
});
if (result.ok && result.data.saved) {
heart.classList.add('sl-heart--active');
heart.setAttribute('aria-pressed', 'true');
}
}
});Batch checking
For collection pages with many products, consider batching isSaved calls or using list() to fetch all saved items at once and comparing client-side.
Success response
json
{
"ok": true,
"data": { "saved": true }
}SaveLayer.on()
Subscribe to SDK events. The SDK emits customer events after successful operations.
Parameters
| Field | Type | Description |
|---|---|---|
event | string | Event name (see table below) |
listener | function | Callback receiving the event payload |
Returns: An unsubscribe function.
Available events
| Event | Fires after | Payload |
|---|---|---|
savelayer:item_saved | Successful save() | entityGid, entityType, context, listHandle |
savelayer:item_removed | Successful remove() | entityGid, entityType, context, listHandle |
savelayer:item_toggled | Successful toggle() | Same fields + saved (boolean) |
savelayer:list_viewed | Successful list() | context, itemCount |
Example: update a save counter badge
js
let saveCount = 0;
const badge = document.getElementById('wishlist-count');
const unsub = window.SaveLayer.on('savelayer:item_saved', (payload) => {
saveCount++;
badge.textContent = saveCount;
});
window.SaveLayer.on('savelayer:item_removed', (payload) => {
saveCount = Math.max(0, saveCount - 1);
badge.textContent = saveCount;
});
// Call unsub() to stop listeningExample: log all toggle events
js
window.SaveLayer.on('savelayer:item_toggled', (payload) => {
console.log(
payload.saved ? 'Saved' : 'Removed',
payload.entityType,
payload.entityGid
);
});Error handling
All SDK methods return a JSON envelope. Errors include a machine-readable code, a human-readable message, and a retryable boolean.
Common error codes
| Code | HTTP | Meaning | Retryable |
|---|---|---|---|
AUTH_REQUIRED | 401 | Customer is not logged in | No |
DUPLICATE_SAVE | 409 | Item is already saved | No |
ITEM_NOT_FOUND | 404 | Item or list does not exist | No |
VALIDATION_ERROR | 400 | Invalid request payload | No |
RATE_LIMITED | 429 | Too many requests | Yes |
PLAN_LIMIT_REACHED | 402 | Merchant's plan limit reached | No |
SHOPIFY_UPSTREAM_ERROR | 502 | Shopify returned an error | Yes |
INTERNAL_ERROR | 500 | Unexpected server error | Yes |
Example: robust save with error handling
js
async function safeSave(entityGid) {
if (!window.SaveLayer) {
console.warn('SaveLayer SDK not loaded');
return;
}
const result = await window.SaveLayer.save({
context: 'wishlist',
entityType: 'product',
entityGid,
source: 'theme',
});
if (result.ok) {
return { saved: true };
}
switch (result.error.code) {
case 'AUTH_REQUIRED':
// Redirect to login
window.location.href = '/account/login';
break;
case 'DUPLICATE_SAVE':
// Already saved — treat as success
return { saved: true };
case 'RATE_LIMITED':
// Wait and retry
await new Promise((r) => setTimeout(r, 2000));
return safeSave(entityGid);
case 'PLAN_LIMIT_REACHED':
alert('The store has reached its save limit. Please try again later.');
break;
default:
console.error('SaveLayer error:', result.error.code, result.error.message);
}
return { saved: false };
}Example: network error handling
js
async function resilientToggle(entityGid) {
try {
const result = await window.SaveLayer.toggle({
context: 'wishlist',
entityType: 'product',
entityGid,
source: 'theme',
});
return result;
} catch (err) {
// Network failure (fetch rejected) — not an API error envelope
console.error('Network error:', err);
return { ok: false, error: { code: 'NETWORK_ERROR', message: err.message } };
}
}