Get the Most From Your Products
Rotating tips and recipes for what they actually get.

What it does
Rotating tips, recipes and tutorials based on what each subscriber actually gets.
Why it exists
For merchants who want happier subscribers who get more out of every order. Tips follow the products on the subscription when the block can detect them.
The code
Paste it as-is, or download get-the-most.html<!--
Skio Portal Custom Block: "Get More From Your Product"
=======================================================
WHAT THIS IS
A self-contained block for the Skio subscription portal editor. Paste this
whole file into one custom-block field. It shows a small rotating carousel
of tips/tricks/recipes/use-cases, personalized to the product(s) on the
customer's subscription when it can detect them, and falls back to a
generic tip pool when it can't.
HOW PERSONALIZATION WORKS (read this before editing)
There is no confirmed `data-skio-name` attribute for "product title" in the
Skio knowledge base this block was built from — only header-level fields
(interval, price, next date, etc.) were confirmed. So detection here is
deliberately best-effort and DOM-only (no API calls, no secrets):
1. It tries a short list of plausible selectors/attributes that Skio's
portal or a subscription line item is likely to use for a product
name (see CANDIDATE_PRODUCT_SELECTORS below).
2. It matches whatever text it finds against the keys in TIP_LIBRARY
below (case-insensitive, substring match both directions).
3. If nothing is found, or nothing matches, it just shows the `generic`
tip pool. The block never hides itself or breaks if detection fails.
If you have (or can capture) the real rendered DOM of a subscription line
item, add its real selector to CANDIDATE_PRODUCT_SELECTORS and it will be
tried first — real markup should always win over the guesses.
HOW TO FILL IN REAL CONTENT
Scroll to the `TIP_LIBRARY` object in the <script> block. Each key is a
product name (or a distinctive substring of one — e.g. "Vitamin C Serum"
will match a purchased product literally titled "Vitamin C Serum 30ml").
Each value is an array of tip objects. Add as many products/tips as you
like — no other code needs to change. A `generic` array is always shown
to customers whose product couldn't be matched (or mixed in occasionally
for variety even when a match is found).
ROTATION
Auto-advances every 6s. Pauses on hover/focus/tab-hidden. If the visitor
has prefers-reduced-motion enabled, auto-advance is disabled entirely
(manual arrows/dots still work, and one full tip is always shown) rather
than leaving anything half-animated or blank.
-->
<div class="skio-get-more" data-skio-get-more-root>
<style>
/* Scoped to .skio-get-more so nothing leaks onto the rest of the portal. */
.skio-get-more {
container-type: inline-size;
container-name: skio-get-more;
box-sizing: border-box !important;
width: 100% !important;
margin: 1.5rem 0 !important;
padding: clamp(0.875rem, 3cqw, 1.5rem) !important;
border-radius: 12px !important;
border: 1px solid var(--skio-gm-border, #e5e7eb) !important;
background: var(--skio-gm-bg, #fafafa) !important;
font-family: inherit !important;
color: var(--skio-gm-text, #111827) !important;
position: relative !important;
}
.skio-get-more *,
.skio-get-more *::before,
.skio-get-more *::after {
box-sizing: border-box !important;
}
.skio-get-more__eyebrow {
display: block !important;
font-size: clamp(0.6875rem, 2.2cqw, 0.75rem) !important;
font-weight: 700 !important;
letter-spacing: 0.06em !important;
text-transform: uppercase !important;
color: var(--skio-gm-accent, #6d28d9) !important;
margin: 0 0 0.35rem 0 !important;
}
.skio-get-more__heading {
margin: 0 0 0.9rem 0 !important;
font-size: clamp(1rem, 4cqw, 1.25rem) !important;
line-height: 1.3 !important;
font-weight: 700 !important;
color: var(--skio-gm-heading, #111827) !important;
}
.skio-get-more__viewport {
position: relative !important;
overflow: hidden !important;
border-radius: 10px !important;
background: var(--skio-gm-card-bg, #ffffff) !important;
border: 1px solid var(--skio-gm-card-border, #ececef) !important;
}
.skio-get-more__track {
display: flex !important;
width: 100% !important;
}
/* Instant swap by default is done via JS re-render; when we DO animate
(motion allowed), we animate opacity/transform on the active card
rather than sliding a wide flex track, so nothing needs measuring. */
.skio-get-more__card {
display: flex !important;
flex-direction: column !important;
gap: 0.6rem !important;
width: 100% !important;
padding: clamp(0.9rem, 4cqw, 1.25rem) !important;
opacity: 1 !important;
transform: translateY(0) !important;
}
.skio-get-more__card[data-state="entering"] {
animation: skio-gm-fade-in 320ms ease both !important;
}
@keyframes skio-gm-fade-in {
from { opacity: 0 !important; transform: translateY(4px) !important; }
to { opacity: 1 !important; transform: translateY(0) !important; }
}
@media (prefers-reduced-motion: reduce) {
.skio-get-more__card[data-state="entering"] {
animation: none !important;
}
}
.skio-get-more__card-top {
display: flex !important;
align-items: flex-start !important;
gap: 0.75rem !important;
}
.skio-get-more__icon {
flex: none !important;
width: clamp(2rem, 8cqw, 2.5rem) !important;
height: clamp(2rem, 8cqw, 2.5rem) !important;
border-radius: 9px !important;
background: var(--skio-gm-icon-bg, #f3e8ff) !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
font-size: clamp(1rem, 5cqw, 1.25rem) !important;
line-height: 1 !important;
align-self: flex-start !important;
}
.skio-get-more__card-body {
min-width: 0 !important;
}
.skio-get-more__tag {
display: inline-block !important;
font-size: 0.6875rem !important;
font-weight: 700 !important;
text-transform: uppercase !important;
letter-spacing: 0.04em !important;
color: var(--skio-gm-tag-text, #6d28d9) !important;
background: var(--skio-gm-tag-bg, #f3e8ff) !important;
border-radius: 999px !important;
padding: 0.1rem 0.5rem !important;
margin: 0 0 0.35rem 0 !important;
}
.skio-get-more__tip-title {
margin: 0 0 0.25rem 0 !important;
font-size: clamp(0.9375rem, 3.6cqw, 1.0625rem) !important;
font-weight: 700 !important;
color: var(--skio-gm-heading, #111827) !important;
}
.skio-get-more__tip-body {
margin: 0 !important;
font-size: clamp(0.8125rem, 3.2cqw, 0.9375rem) !important;
line-height: 1.5 !important;
color: var(--skio-gm-body, #4b5563) !important;
}
.skio-get-more__cta {
align-self: flex-start !important;
display: inline-flex !important;
align-items: center !important;
gap: 0.3rem !important;
margin-top: 0.15rem !important;
font-size: 0.8125rem !important;
font-weight: 700 !important;
text-decoration: none !important;
color: var(--skio-gm-accent, #6d28d9) !important;
border-bottom: 1px solid currentColor !important;
padding-bottom: 1px !important;
}
.skio-get-more__cta:hover {
opacity: 0.75 !important;
}
.skio-get-more__controls {
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
gap: 0.75rem !important;
margin-top: 0.75rem !important;
}
.skio-get-more__arrows {
display: flex !important;
gap: 0.4rem !important;
}
.skio-get-more__arrow-btn {
appearance: none !important;
border: 1px solid var(--skio-gm-card-border, #e5e7eb) !important;
background: var(--skio-gm-card-bg, #ffffff) !important;
color: var(--skio-gm-heading, #111827) !important;
width: 2rem !important;
height: 2rem !important;
border-radius: 999px !important;
cursor: pointer !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
font-size: 0.9rem !important;
line-height: 1 !important;
padding: 0 !important;
}
.skio-get-more__arrow-btn:hover {
background: var(--skio-gm-icon-bg, #f3e8ff) !important;
}
.skio-get-more__dots {
display: flex !important;
align-items: center !important;
gap: 0.4rem !important;
}
.skio-get-more__dot {
appearance: none !important;
border: none !important;
cursor: pointer !important;
padding: 0 !important;
width: 0.5rem !important;
height: 0.5rem !important;
border-radius: 999px !important;
background: var(--skio-gm-dot, #d8d3e0) !important;
}
.skio-get-more__dot[aria-current="true"] {
background: var(--skio-gm-accent, #6d28d9) !important;
width: 1.1rem !important;
}
/* Focus-visible states on every interactive element in this block. */
.skio-get-more__arrow-btn:focus-visible,
.skio-get-more__dot:focus-visible,
.skio-get-more__cta:focus-visible {
outline: 2px solid var(--skio-gm-accent, #6d28d9) !important;
outline-offset: 2px !important;
}
/* Narrow-container layout (e.g. rendered inside a drawer/panel). */
@container skio-get-more (max-width: 360px) {
.skio-get-more__card-top {
flex-direction: column !important;
}
.skio-get-more__icon {
width: 2rem !important;
height: 2rem !important;
}
}
</style>
<span class="skio-get-more__eyebrow">Get More From Your Product</span>
<h3 class="skio-get-more__heading" data-skio-gm-heading>Tips & ideas for you</h3>
<div class="skio-get-more__viewport">
<div class="skio-get-more__track" data-skio-gm-track></div>
</div>
<div class="skio-get-more__controls">
<div class="skio-get-more__arrows">
<button
type="button"
class="skio-get-more__arrow-btn"
data-skio-gm-prev
aria-label="Show previous tip"
>‹</button>
<button
type="button"
class="skio-get-more__arrow-btn"
data-skio-gm-next
aria-label="Show next tip"
>›</button>
</div>
<div class="skio-get-more__dots" data-skio-gm-dots role="tablist" aria-label="Tip navigation"></div>
</div>
<script>
(function () {
"use strict";
/* ------------------------------------------------------------------
* 1. CONTENT — fill this in with real tips per product.
* Key = product name or a distinctive substring of one.
* "generic" is always available as a fallback / variety mix-in.
* ------------------------------------------------------------------ */
var TIP_LIBRARY = {
generic: [
{
emoji: "✨",
tag: "Get more from it",
title: "Getting the most out of your subscription",
body: "Small habits make a big difference — using your product consistently, on schedule, is the single best way to see results.",
ctaText: null,
ctaHref: null
},
{
emoji: "📅",
tag: "Tip",
title: "Line your order up with how you actually use it",
body: "If a delivery ever arrives too early or too late, you can adjust your next order date or frequency any time from this portal.",
ctaText: null,
ctaHref: null
}
]
/*
"Vitamin C Serum": [
{
emoji: "☀️",
tag: "Pro tip",
title: "Apply before sunscreen, not after",
body: "Vitamin C works best on clean skin, before moisturizer and SPF, so it can fully absorb.",
ctaText: "See the full routine",
ctaHref: "https://example.com/routines/vitamin-c"
},
{
emoji: "🧪",
tag: "Did you know?",
title: "Store it away from direct sunlight",
body: "Vitamin C degrades faster when it's warm or exposed to light — a cabinet keeps it working longer than a sunny windowsill.",
ctaText: null,
ctaHref: null
}
],
"Protein Powder": [
{
emoji: "🥤",
tag: "Recipe",
title: "Try it in overnight oats",
body: "Stir a scoop in with oats, milk, and chia seeds the night before for a grab-and-go breakfast.",
ctaText: "Get the recipe",
ctaHref: "https://example.com/recipes/overnight-oats"
}
]
*/
};
/* ------------------------------------------------------------------
* 2. PRODUCT DETECTION — DOM-only, best-effort, never blocks render.
* None of these selectors are confirmed against a real rendered
* Skio subscription-line DOM capture. If you get one, put the
* real selector FIRST in this list.
* ------------------------------------------------------------------ */
var CANDIDATE_PRODUCT_SELECTORS = [
"[data-skio-name='subscription-line-title']",
"[data-skio-name='subscription-line-product-title']",
"[data-skio-name='line-item-title']",
"[data-skio-name='product-title']",
"[data-skio-name*='product'][data-skio-name*='title']",
"[data-skio-name*='line'][data-skio-name*='title']"
];
function collectCandidateProductNames() {
var names = [];
try {
CANDIDATE_PRODUCT_SELECTORS.forEach(function (sel) {
document.querySelectorAll(sel).forEach(function (el) {
var text = (el.textContent || "").trim();
if (text) names.push(text);
});
});
} catch (err) {
/* A selector engine quirk shouldn't take the block down. */
console.warn("[skio-get-more] product detection selector failed:", err);
}
return names;
}
function matchTipLibraryKeys(candidateNames) {
var matched = [];
var libraryKeys = Object.keys(TIP_LIBRARY).filter(function (k) {
return k !== "generic";
});
candidateNames.forEach(function (name) {
var lowerName = name.toLowerCase();
libraryKeys.forEach(function (key) {
var lowerKey = key.toLowerCase();
var isMatch =
lowerName.indexOf(lowerKey) !== -1 || lowerKey.indexOf(lowerName) !== -1;
if (isMatch && matched.indexOf(key) === -1) {
matched.push(key);
}
});
});
return matched;
}
function buildTipPool(matchedKeys) {
var pool = [];
matchedKeys.forEach(function (key) {
pool = pool.concat(TIP_LIBRARY[key]);
});
// Mix in generic tips too — either as the whole pool (no match) or
// as variety alongside matched, product-specific tips.
pool = pool.concat(TIP_LIBRARY.generic);
return pool.length ? pool : TIP_LIBRARY.generic;
}
/* ------------------------------------------------------------------
* 3. WAIT FOR THE PAGE'S SKIO ELEMENTS (they may render after this
* script runs). Reuse a shared observer if one already exists on
* the page, per Skio portal convention; otherwise install one,
* guarded so multiple blocks never double-install it. The block
* always renders immediately with whatever it can detect right
* now (generic tips if nothing yet) — it never sits blank waiting
* on detection — and quietly upgrades in place if a product shows
* up in the DOM shortly after, within a short grace window.
* ------------------------------------------------------------------ */
function getSharedSkioDetectObserver() {
if (!window.__skioDetect) {
window.__skioDetect = (function () {
var watchers = [];
var observer = new MutationObserver(function () {
watchers.forEach(function (fn) {
try {
fn();
} catch (e) {
console.warn("[skio-get-more] shared __skioDetect watcher failed:", e);
}
});
});
observer.observe(document.documentElement, { childList: true, subtree: true });
return {
watch: function (fn) {
watchers.push(fn);
}
};
})();
}
return window.__skioDetect;
}
function watchForLateProductMatch(onFirstMatch, timeoutMs) {
var settled = false;
var timeout = typeof timeoutMs === "number" ? timeoutMs : 2500;
function tryMatch() {
if (settled) return;
var matched = matchTipLibraryKeys(collectCandidateProductNames());
if (matched.length) {
settled = true;
onFirstMatch(matched);
}
}
getSharedSkioDetectObserver().watch(tryMatch);
window.setTimeout(function () {
settled = true;
}, timeout);
}
/* ------------------------------------------------------------------
* 4. CAROUSEL — render, rotate, navigate.
* ------------------------------------------------------------------ */
function initCarousel(root, initialTips) {
var track = root.querySelector("[data-skio-gm-track]");
var dotsWrap = root.querySelector("[data-skio-gm-dots]");
var prevBtn = root.querySelector("[data-skio-gm-prev]");
var nextBtn = root.querySelector("[data-skio-gm-next]");
var reduceMotion =
window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
var tips = initialTips;
var index = 0;
var timer = null;
var AUTO_ADVANCE_MS = 6000;
function updateControlsVisibility() {
var multiple = tips.length > 1;
prevBtn.style.display = multiple ? "" : "none";
nextBtn.style.display = multiple ? "" : "none";
dotsWrap.style.display = multiple ? "" : "none";
}
function renderDots() {
dotsWrap.innerHTML = "";
tips.forEach(function (_, i) {
var dot = document.createElement("button");
dot.type = "button";
dot.className = "skio-get-more__dot";
dot.setAttribute("role", "tab");
dot.setAttribute("aria-label", "Show tip " + (i + 1) + " of " + tips.length);
dot.setAttribute("aria-current", i === index ? "true" : "false");
dot.addEventListener("click", function () {
goTo(i);
});
dotsWrap.appendChild(dot);
});
}
function renderCard(animate) {
var tip = tips[index];
var card = document.createElement("div");
card.className = "skio-get-more__card";
if (animate && !reduceMotion) {
card.setAttribute("data-state", "entering");
}
var top = document.createElement("div");
top.className = "skio-get-more__card-top";
var icon = document.createElement("div");
icon.className = "skio-get-more__icon";
icon.setAttribute("aria-hidden", "true");
icon.textContent = tip.emoji || "✨";
top.appendChild(icon);
var body = document.createElement("div");
body.className = "skio-get-more__card-body";
if (tip.tag) {
var tag = document.createElement("span");
tag.className = "skio-get-more__tag";
tag.textContent = tip.tag;
body.appendChild(tag);
}
var title = document.createElement("p");
title.className = "skio-get-more__tip-title";
title.textContent = tip.title;
body.appendChild(title);
var text = document.createElement("p");
text.className = "skio-get-more__tip-body";
text.textContent = tip.body;
body.appendChild(text);
if (tip.ctaText && tip.ctaHref) {
var cta = document.createElement("a");
cta.className = "skio-get-more__cta";
cta.href = tip.ctaHref;
cta.target = "_blank";
cta.rel = "noopener noreferrer";
cta.textContent = tip.ctaText + " →";
body.appendChild(cta);
}
top.appendChild(body);
card.appendChild(top);
track.innerHTML = "";
track.appendChild(card);
renderDots();
}
function goTo(newIndex, animate) {
index = ((newIndex % tips.length) + tips.length) % tips.length;
renderCard(animate !== false);
}
function next() {
goTo(index + 1);
}
function prev() {
goTo(index - 1);
}
function startAuto() {
if (reduceMotion || tips.length < 2) return;
stopAuto();
timer = window.setInterval(next, AUTO_ADVANCE_MS);
}
function stopAuto() {
if (timer) {
window.clearInterval(timer);
timer = null;
}
}
prevBtn.addEventListener("click", function () {
prev();
startAuto();
});
nextBtn.addEventListener("click", function () {
next();
startAuto();
});
root.addEventListener("mouseenter", stopAuto);
root.addEventListener("mouseleave", startAuto);
root.addEventListener("focusin", stopAuto);
root.addEventListener("focusout", startAuto);
document.addEventListener("visibilitychange", function () {
if (document.hidden) {
stopAuto();
} else {
startAuto();
}
});
updateControlsVisibility();
renderCard(false);
startAuto();
return {
// Swap in a new tip pool without tearing down listeners — used
// when a product is detected shortly after first render, so the
// block never has to sit blank waiting on detection.
setTips: function (newTips) {
tips = newTips && newTips.length ? newTips : [];
index = 0;
updateControlsVisibility();
renderCard(true);
startAuto();
}
};
}
/* ------------------------------------------------------------------
* 5. WIRE IT UP
* ------------------------------------------------------------------ */
function setHeading(root, matchedKeys) {
var heading = root.querySelector("[data-skio-gm-heading]");
if (matchedKeys.length === 1) {
heading.textContent = "Get more from your " + matchedKeys[0];
} else if (matchedKeys.length > 1) {
heading.textContent = "Tips for your subscription";
} else {
heading.textContent = "Tips & ideas for you";
}
}
function boot() {
var root = document.currentScript
? document.currentScript.closest("[data-skio-get-more-root]")
: document.querySelector("[data-skio-get-more-root]");
if (!root) return;
// Render immediately with whatever we can detect right now — this
// is usually the generic pool on first paint, since Skio's own
// portal elements can render a beat after this script runs.
var initialMatchedKeys = matchTipLibraryKeys(collectCandidateProductNames());
setHeading(root, initialMatchedKeys);
var carousel = initCarousel(root, buildTipPool(initialMatchedKeys));
// If we didn't find a product match yet, keep watching quietly for
// a short grace period and upgrade in place the moment one shows
// up — the customer never sees a blank block while this happens.
if (initialMatchedKeys.length === 0) {
watchForLateProductMatch(function (matchedKeys) {
setHeading(root, matchedKeys);
carousel.setTips(buildTipPool(matchedKeys));
});
}
}
boot();
})();
</script>
</div>How to install
- Copy the code above, or download the .html file.
- Go to
Storefront→Customer Portal→Branding→Layoutand clickAdd custom extension. - Pick
HTML with embedded CSS and JavaScriptand paste the whole file into the code editor. - Swap anything the file asks you to, like
[BRAND], product names or Quick Action IDs. The AI prompt below can walk you through it. - Save, click
+to add it to the layout, thenPublish. Check it on a test subscription’s magic link with?forcePreview=true, since some scripts don’t run in Preview.
Write your tips in TIP_LIBRARY, keyed by product name. Product detection is best-effort; with no match it shows the generic tips.
Customize it with AI
Paste this prompt into Claude Code or Codex. It asks for the code and your brand and product details, then writes a version for your store.
I want to add a custom extension from customizeyourportal.xyz to my subscription customer portal. Walk me through customizing it for my brand, then write the finished code. EXTENSION Name: Get the Most From Your Products (No. 13) Page: /blocks/get-the-most What it does: Rotating tips, recipes and tutorials based on what each subscriber actually gets. My portal app: Skio WHAT THE BUILDER SAYS TO CUSTOMIZE Write your tips in TIP_LIBRARY, keyed by product name. Product detection is best-effort; with no match it shows the generic tips. HOW TO WORK 1. Ask me to paste the full code (the "Copy code" button on the page) or to give you the path to the downloaded get-the-most.html. Wait until you have it. 2. Read the whole file. List every value that is specific to the original brand or needs my input: brand name and [PLACEHOLDERS], product and variant names or IDs, prices, discount codes and reward amounts, Quick Action IDs or tokens, store domains and API endpoints, colors, fonts, logos and images, and any copy that names the original brand, products or offers. Include everything in the builder's notes above. 3. Ask me for those values in one numbered list, with the current value next to each, so I can answer or say "keep". If you can open web pages, offer to pull my brand colors, fonts, logo and product names from my store's URL. 4. Don't invent stats, reviews, IDs, prices or claims. If I don't have a value, leave a clearly marked TODO in the code and tell me. 5. Write the customized version as get-the-most-<my-brand>.html. Change only what my brand needs. Keep the logic, behavior and structure the same, and keep it one self-contained file (HTML, <style> and <script>). 6. Finish with a short list of what you changed, any TODOs left, and these install steps for Skio: 1. Go to `Storefront` → `Customer Portal` → `Branding` → `Layout` and click `Add custom extension`. 2. Pick `HTML with embedded CSS and JavaScript` and paste the whole file into the code editor. 3. Save, click `+` to add it to the layout, then `Publish`. Check it on a test subscription’s magic link with `?forcePreview=true`, since some scripts don’t run in Preview.
Blocks are shared as-is by the people who built them. Read the code before you add it to your portal, and test it with a test subscriber first.