Quiz / Product Finder
Multi-step quiz that recommends products by tag-matching. Replaces Octane AI.
Live preview
See it in action.
Fully interactive, drag, click, scroll inside the frame, toggle to mobile.
About this section
A complete multi-step quiz with progress bar, branching answers, and tag-based product matching. Configure questions and answer options as theme blocks. Each option carries comma-separated tags. The result slide shows products from a chosen collection ranked by how many tags each one matches. Pure Liquid plus a few hundred lines of vanilla JS, no third-party SDK, no monthly fee.
Install in 90 seconds
- 01
Create /sections/modblo-quiz-product-finder.liquid.
- 02
Paste the section code and save.
- 03
Add the section to a /pages/quiz page (or your homepage) via the theme editor.
- 04
Pick a recommendation collection and tag your products with the same labels you use on quiz options (e.g. 'minimal', 'editorial', 'classic').
- 05
Add Question blocks. Each block has 4 option slots, each option holds comma-separated tags. Products that share more tags with the buyer's selections rank higher in the result.
The Liquid
{%- comment -%}
modblo. Quiz / Product Finder
Replaces Octane AI / Shop Quiz / Tabarnapp Quiz Kit.
Multi-step product recommendation quiz, configurable via theme blocks.
Each Question block has text + 4 answer options. Each option carries
comma-separated tags. The final step shows products from the source
collection ranked by how many accumulated tags each product matches.
{%- endcomment -%}
{%- assign rec_collection = collections[section.settings.rec_collection] -%}
<section class="modblo-qz" data-modblo-qz
data-section-id="{{ section.id }}"
style="--modblo-qz-bg: {{ section.settings.bg }};
--modblo-qz-fg: {{ section.settings.fg }};
--modblo-qz-accent: {{ section.settings.accent }};">
<div class="modblo-qz__inner page-width">
{%- comment -%} Intro slide {%- endcomment -%}
<div class="modblo-qz__slide modblo-qz__intro is-active" data-modblo-qz-slide="intro">
{%- if section.settings.eyebrow != blank -%}
<p class="modblo-qz__eyebrow">{{ section.settings.eyebrow }}</p>
{%- endif -%}
<h2 class="modblo-qz__h">{{ section.settings.heading | default: 'Find your fit in 60 seconds.' }}</h2>
<p class="modblo-qz__sub">{{ section.settings.subheading | default: 'Answer a few quick questions and we will recommend products tailored to you.' }}</p>
<button type="button" class="modblo-qz__start" data-modblo-qz-start>
{{ section.settings.start_label | default: 'Start the quiz' }}
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</button>
</div>
{%- comment -%} Question slides (one per Question block) {%- endcomment -%}
{%- assign q_index = 0 -%}
{%- for block in section.blocks -%}
{%- if block.type == 'question' -%}
{%- assign q_index = q_index | plus: 1 -%}
<div class="modblo-qz__slide modblo-qz__question"
data-modblo-qz-slide="q-{{ forloop.index }}"
data-q-index="{{ q_index }}"
{{ block.shopify_attributes }}>
<div class="modblo-qz__progress">
<div class="modblo-qz__progress-bar"><div data-modblo-qz-progress-fill></div></div>
<p class="modblo-qz__progress-text">
Question <strong>{{ q_index }}</strong> of <span data-modblo-qz-total></span>
</p>
</div>
<h3 class="modblo-qz__qh">{{ block.settings.text }}</h3>
{%- if block.settings.helper != blank -%}
<p class="modblo-qz__qhelp">{{ block.settings.helper }}</p>
{%- endif -%}
<div class="modblo-qz__opts">
{%- assign opts = '1|2|3|4' | split: '|' -%}
{%- for n in opts -%}
{%- assign label_key = 'option' | append: n | append: '_label' -%}
{%- assign tags_key = 'option' | append: n | append: '_tags' -%}
{%- assign label = block.settings[label_key] -%}
{%- assign tags = block.settings[tags_key] -%}
{%- if label != blank -%}
<button type="button" class="modblo-qz__opt"
data-modblo-qz-opt
data-tags="{{ tags }}">
<span class="modblo-qz__opt-letter" aria-hidden="true">{{ n }}</span>
<span class="modblo-qz__opt-label">{{ label }}</span>
<span class="modblo-qz__opt-check" aria-hidden="true">
<svg viewBox="0 0 12 12" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M2 6l3 3 5-6"/></svg>
</span>
</button>
{%- endif -%}
{%- endfor -%}
</div>
<div class="modblo-qz__nav">
<button type="button" class="modblo-qz__back" data-modblo-qz-back>← Back</button>
<button type="button" class="modblo-qz__next" data-modblo-qz-next disabled>Continue →</button>
</div>
</div>
{%- endif -%}
{%- endfor -%}
{%- comment -%} Result slide — pre-renders the source collection,
JS reorders by tag-match count and shows top N. {%- endcomment -%}
<div class="modblo-qz__slide modblo-qz__result" data-modblo-qz-slide="result">
<p class="modblo-qz__eyebrow">{{ section.settings.result_eyebrow | default: 'Your match' }}</p>
<h3 class="modblo-qz__h">{{ section.settings.result_heading | default: 'Picked just for you.' }}</h3>
<p class="modblo-qz__sub" data-modblo-qz-result-sub>
Based on your answers, we think these are right for you.
</p>
{%- if rec_collection.products.size > 0 -%}
<div class="modblo-qz__results-grid" data-modblo-qz-results>
{%- for product in rec_collection.products limit: 12 -%}
{%- assign first_var = product.selected_or_first_available_variant -%}
<article class="modblo-qz__rec" data-modblo-qz-rec data-tags="{{ product.tags | join: ',' | downcase }}">
<a class="modblo-qz__rec-link" href="{{ product.url }}">
{%- if product.featured_image -%}
{{ product.featured_image | image_url: width: 480 | image_tag:
loading: 'lazy', widths: '240,480',
sizes: '(max-width:768px) 50vw, 240px',
class: 'modblo-qz__rec-img' }}
{%- endif -%}
</a>
<p class="modblo-qz__rec-vendor">{{ product.vendor }}</p>
<p class="modblo-qz__rec-title">{{ product.title }}</p>
<p class="modblo-qz__rec-price">
{%- if product.compare_at_price > product.price -%}
<s>{{ product.compare_at_price | money }}</s>
{%- endif -%}
<strong>{{ first_var.price | money }}</strong>
</p>
<span class="modblo-qz__rec-score" data-modblo-qz-score hidden>0 match</span>
<form action="/cart/add" method="post" class="modblo-qz__rec-form">
<input type="hidden" name="id" value="{{ first_var.id }}">
<button type="submit" class="modblo-qz__rec-add">+ Add to cart</button>
</form>
</article>
{%- endfor -%}
</div>
{%- else -%}
<p class="modblo-qz__empty">Configure a recommendation collection in the theme editor.</p>
{%- endif -%}
<button type="button" class="modblo-qz__restart" data-modblo-qz-restart>↻ Retake the quiz</button>
</div>
</div>
</section>
<style>
.modblo-qz { background: var(--modblo-qz-bg, #fff); color: var(--modblo-qz-fg, #0b0b0c); padding: clamp(48px, 6vw, 96px) 0; }
.modblo-qz__inner { max-width: 720px; margin: 0 auto; padding: 0 24px; min-height: 480px; }
.modblo-qz__slide { display: none; }
.modblo-qz__slide.is-active { display: block; }
.modblo-qz__intro, .modblo-qz__result { text-align: center; }
.modblo-qz__eyebrow {
text-transform: uppercase; letter-spacing: .18em;
font-size: 12px; font-weight: 700;
color: var(--modblo-qz-accent, #6366f1); margin: 0 0 14px;
}
.modblo-qz__h { font-size: clamp(28px, 4vw, 40px); letter-spacing: -.02em; line-height: 1.15; margin: 0 0 14px; }
.modblo-qz__sub { font-size: 16px; opacity: .7; margin: 0 auto 28px; max-width: 520px; line-height: 1.5; }
.modblo-qz__start, .modblo-qz__rec-add, .modblo-qz__next {
display: inline-flex; align-items: center; gap: 8px;
background: var(--modblo-qz-accent, #6366f1); color: #fff;
border: 0; padding: 14px 24px; border-radius: 12px;
font-size: 15px; font-weight: 600; cursor: pointer;
transition: opacity .2s, transform .15s;
}
.modblo-qz__start:hover, .modblo-qz__next:hover:not([disabled]) { opacity: .92; }
.modblo-qz__next[disabled] { opacity: .4; cursor: not-allowed; }
.modblo-qz__progress { margin-bottom: 28px; }
.modblo-qz__progress-bar {
height: 6px; border-radius: 999px; overflow: hidden;
background: color-mix(in oklab, var(--modblo-qz-fg) 8%, transparent);
margin-bottom: 10px;
}
.modblo-qz__progress-bar > div {
height: 100%; width: 0%;
background: var(--modblo-qz-accent, #6366f1);
transition: width .35s cubic-bezier(.16,1,.3,1);
}
.modblo-qz__progress-text {
font-size: 12px; opacity: .6; margin: 0;
text-transform: uppercase; letter-spacing: .12em; font-weight: 600;
}
.modblo-qz__qh { font-size: clamp(22px, 3vw, 30px); letter-spacing: -.02em; margin: 0 0 6px; line-height: 1.2; }
.modblo-qz__qhelp { font-size: 14px; opacity: .65; margin: 0 0 22px; }
.modblo-qz__opts { display: grid; gap: 10px; margin-bottom: 28px; }
.modblo-qz__opt {
display: flex; align-items: center; gap: 12px;
background: color-mix(in oklab, var(--modblo-qz-fg) 3%, transparent);
color: inherit;
border: 1.5px solid color-mix(in oklab, var(--modblo-qz-fg) 8%, transparent);
border-radius: 14px;
padding: 16px 18px; cursor: pointer;
font-size: 15px; text-align: left;
transition: border-color .2s, background .2s;
}
.modblo-qz__opt:hover {
border-color: color-mix(in oklab, var(--modblo-qz-accent, #6366f1) 50%, transparent);
}
.modblo-qz__opt.is-selected {
border-color: var(--modblo-qz-accent, #6366f1);
background: color-mix(in oklab, var(--modblo-qz-accent, #6366f1) 6%, transparent);
}
.modblo-qz__opt-letter {
flex-shrink: 0; width: 28px; height: 28px; border-radius: 8px;
background: color-mix(in oklab, var(--modblo-qz-fg) 8%, transparent);
display: grid; place-items: center;
font-size: 12px; font-weight: 700;
transition: background .2s, color .2s;
}
.modblo-qz__opt.is-selected .modblo-qz__opt-letter {
background: var(--modblo-qz-accent, #6366f1); color: #fff;
}
.modblo-qz__opt-label { flex: 1; }
.modblo-qz__opt-check {
flex-shrink: 0; opacity: 0; color: var(--modblo-qz-accent, #6366f1);
transition: opacity .2s;
}
.modblo-qz__opt.is-selected .modblo-qz__opt-check { opacity: 1; }
.modblo-qz__nav { display: flex; align-items: center; justify-content: space-between; margin-top: 12px; }
.modblo-qz__back {
background: transparent; border: 0; color: inherit;
cursor: pointer; opacity: .55; font-size: 13px;
padding: 10px 12px;
}
.modblo-qz__back:hover { opacity: 1; }
.modblo-qz__results-grid {
display: grid; gap: 16px; margin: 24px 0;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}
.modblo-qz__rec { text-align: left; }
.modblo-qz__rec[hidden] { display: none; }
.modblo-qz__rec-link { display: block; }
.modblo-qz__rec-img {
width: 100%; aspect-ratio: 1; object-fit: cover;
border-radius: 12px; margin-bottom: 8px;
background: color-mix(in oklab, var(--modblo-qz-fg) 4%, transparent);
}
.modblo-qz__rec-vendor { font-size: 11px; opacity: .6; text-transform: uppercase; letter-spacing: .08em; margin: 0 0 4px; }
.modblo-qz__rec-title { font-size: 14px; font-weight: 600; margin: 0 0 4px; line-height: 1.3; }
.modblo-qz__rec-price { font-size: 13px; margin: 0 0 8px; display: flex; gap: 6px; align-items: baseline; }
.modblo-qz__rec-price s { opacity: .5; }
.modblo-qz__rec-score {
display: inline-block;
background: color-mix(in oklab, var(--modblo-qz-accent, #6366f1) 15%, transparent);
color: var(--modblo-qz-accent, #6366f1);
font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .08em;
padding: 2px 7px; border-radius: 4px; margin-bottom: 8px;
}
.modblo-qz__rec-form { margin: 0; }
.modblo-qz__rec-add {
width: 100%; padding: 9px 12px; font-size: 12px;
background: color-mix(in oklab, var(--modblo-qz-fg) 6%, transparent);
color: inherit;
}
.modblo-qz__rec-add:hover { background: var(--modblo-qz-accent, #6366f1); color: #fff; }
.modblo-qz__restart {
background: transparent; border: 0; color: inherit;
cursor: pointer; opacity: .6; font-size: 13px; font-weight: 500;
padding: 10px; margin-top: 12px;
text-decoration: underline; text-underline-offset: 3px;
}
.modblo-qz__restart:hover { opacity: 1; }
.modblo-qz__empty { padding: 48px 0; opacity: .5; }
</style>
<script>
(function () {
var root = document.querySelector('[data-modblo-qz][data-section-id="{{ section.id }}"]');
if (!root) return;
var slides = Array.from(root.querySelectorAll('[data-modblo-qz-slide]'));
var questions = slides.filter(function (s) { return s.dataset.cbQzSlide.indexOf('q-') === 0; });
var totalEls = root.querySelectorAll('[data-modblo-qz-total]');
var progressFill = root.querySelectorAll('[data-modblo-qz-progress-fill]');
totalEls.forEach(function (el) { el.textContent = questions.length; });
var current = 'intro';
var answers = {}; // { qIndex: 'tag1,tag2' }
function show(target) {
slides.forEach(function (s) {
s.classList.toggle('is-active', s.dataset.cbQzSlide === target);
});
current = target;
window.scrollTo({ top: root.offsetTop - 24, behavior: 'smooth' });
if (target.indexOf('q-') === 0) {
var idx = parseInt(target.split('-')[1], 10);
var pct = (idx / questions.length) * 100;
questions[idx - 1].querySelectorAll('[data-modblo-qz-progress-fill]').forEach(function (f) {
f.style.width = pct + '%';
});
}
}
root.querySelector('[data-modblo-qz-start]').addEventListener('click', function () {
if (questions.length === 0) { show('result'); rankResults(); return; }
show('q-1');
});
questions.forEach(function (q, i) {
var opts = Array.from(q.querySelectorAll('[data-modblo-qz-opt]'));
var nextBtn = q.querySelector('[data-modblo-qz-next]');
var backBtn = q.querySelector('[data-modblo-qz-back]');
opts.forEach(function (opt) {
opt.addEventListener('click', function () {
opts.forEach(function (o) { o.classList.remove('is-selected'); });
opt.classList.add('is-selected');
answers[i + 1] = opt.dataset.tags || '';
nextBtn.disabled = false;
});
});
nextBtn.addEventListener('click', function () {
if (i + 1 < questions.length) {
show('q-' + (i + 2));
} else {
show('result');
rankResults();
}
});
backBtn.addEventListener('click', function () {
if (i === 0) { show('intro'); }
else { show('q-' + i); }
});
});
function rankResults() {
// Aggregate selected tags
var picked = new Set();
Object.keys(answers).forEach(function (k) {
answers[k].split(',').forEach(function (t) {
var clean = t.trim().toLowerCase();
if (clean) picked.add(clean);
});
});
var recs = Array.from(root.querySelectorAll('[data-modblo-qz-rec]'));
// Score each by overlap count
recs.forEach(function (rec) {
var tags = (rec.dataset.tags || '').split(',').map(function (t) { return t.trim().toLowerCase(); });
var score = tags.reduce(function (s, t) { return picked.has(t) ? s + 1 : s; }, 0);
rec.dataset.score = score;
});
// Sort by score descending, take top 4-6
recs.sort(function (a, b) { return parseInt(b.dataset.score) - parseInt(a.dataset.score); });
var maxShow = recs.length > 6 ? 6 : recs.length;
recs.forEach(function (rec, i) {
rec.parentNode.appendChild(rec); // reorder DOM
rec.hidden = i >= maxShow;
var scoreEl = rec.querySelector('[data-modblo-qz-score]');
var s = parseInt(rec.dataset.score, 10) || 0;
if (scoreEl && s > 0) {
scoreEl.hidden = false;
scoreEl.textContent = s + ' match' + (s > 1 ? 'es' : '');
}
});
// Cart-AJAX submit on each form
root.querySelectorAll('.modblo-qz__rec-form').forEach(function (form) {
form.addEventListener('submit', function (e) {
e.preventDefault();
var btn = form.querySelector('button');
var orig = btn.textContent;
btn.disabled = true; btn.textContent = 'Adding...';
fetch('/cart/add.js', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: form.querySelector('[name="id"]').value, quantity: 1 }),
}).then(function () {
btn.textContent = '✓ Added';
document.dispatchEvent(new CustomEvent('cart:updated'));
setTimeout(function () { btn.disabled = false; btn.textContent = orig; }, 1500);
});
});
});
}
root.querySelector('[data-modblo-qz-restart]').addEventListener('click', function () {
answers = {};
questions.forEach(function (q) {
q.querySelectorAll('[data-modblo-qz-opt]').forEach(function (o) { o.classList.remove('is-selected'); });
var n = q.querySelector('[data-modblo-qz-next]');
if (n) n.disabled = true;
});
show('intro');
});
})();
</script>
{% schema %}
{
"name": "Quiz / Product Finder",
"tag": "section",
"settings": [
{ "type": "header", "content": "Source" },
{ "type": "collection", "id": "rec_collection", "label": "Recommendation collection",
"info": "Products in this collection are scored against the buyer's answers. Use product tags that match your option-tag values." },
{ "type": "header", "content": "Intro slide" },
{ "type": "text", "id": "eyebrow", "label": "Eyebrow", "default": "Take the quiz" },
{ "type": "text", "id": "heading", "label": "Heading", "default": "Find your fit in 60 seconds." },
{ "type": "textarea", "id": "subheading", "label": "Subheading", "default": "Answer a few quick questions and we will recommend products tailored to you." },
{ "type": "text", "id": "start_label", "label": "Start button label", "default": "Start the quiz" },
{ "type": "header", "content": "Result slide" },
{ "type": "text", "id": "result_eyebrow", "label": "Result eyebrow", "default": "Your match" },
{ "type": "text", "id": "result_heading", "label": "Result heading", "default": "Picked just for you." },
{ "type": "header", "content": "Colors" },
{ "type": "color", "id": "bg", "label": "Background", "default": "#ffffff" },
{ "type": "color", "id": "fg", "label": "Foreground", "default": "#0b0b0c" },
{ "type": "color", "id": "accent", "label": "Accent", "default": "#6366f1" }
],
"blocks": [
{
"type": "question",
"name": "Question",
"settings": [
{ "type": "text", "id": "text", "label": "Question text", "default": "What's your style?" },
{ "type": "text", "id": "helper", "label": "Helper text (optional)" },
{ "type": "header", "content": "Option 1" },
{ "type": "text", "id": "option1_label", "label": "Label", "default": "Minimalist" },
{ "type": "text", "id": "option1_tags", "label": "Tags (comma-separated)", "default": "minimal, neutral" },
{ "type": "header", "content": "Option 2" },
{ "type": "text", "id": "option2_label", "label": "Label", "default": "Editorial" },
{ "type": "text", "id": "option2_tags", "label": "Tags (comma-separated)", "default": "editorial, bold" },
{ "type": "header", "content": "Option 3" },
{ "type": "text", "id": "option3_label", "label": "Label", "default": "Classic" },
{ "type": "text", "id": "option3_tags", "label": "Tags (comma-separated)", "default": "classic, heritage" },
{ "type": "header", "content": "Option 4 (optional)" },
{ "type": "text", "id": "option4_label", "label": "Label" },
{ "type": "text", "id": "option4_tags", "label": "Tags (comma-separated)" }
]
}
],
"max_blocks": 6,
"presets": [{
"name": "Quiz / Product Finder",
"blocks": [
{ "type": "question", "settings": { "text": "What's your style?", "option1_label": "Minimalist", "option1_tags": "minimal, neutral", "option2_label": "Editorial", "option2_tags": "editorial, bold", "option3_label": "Classic", "option3_tags": "classic, heritage" } },
{ "type": "question", "settings": { "text": "Which fit do you reach for?", "option1_label": "Slim", "option1_tags": "slim, fitted", "option2_label": "Regular", "option2_tags": "regular", "option3_label": "Relaxed", "option3_tags": "relaxed, oversized" } },
{ "type": "question", "settings": { "text": "What's the occasion?", "option1_label": "Everyday", "option1_tags": "everyday, casual", "option2_label": "Workwear", "option2_tags": "workwear, polished", "option3_label": "Going out", "option3_tags": "evening, statement" } }
]
}]
}
{% endschema %}Unlock the section code
Quiz / Product Finder is a premium section. Get the full Liquid + scoped CSS paste-ready.
One-time purchase · Lifetime updates · You own the code
Theme editor settings
| Setting | Type | Default |
|---|---|---|
Recommendation collection rec_collection Products are ranked by how many tags they match against the buyer's answers. | collection | , |
Intro eyebrow eyebrow | text | Take the quiz |
Intro heading heading | text | Find your fit in 60 seconds. |
Intro subheading subheading | textarea | , |
Start button label start_label | text | Start the quiz |
Result eyebrow result_eyebrow | text | Your match |
Result heading result_heading | text | Picked just for you. |
Background bg | color | #ffffff |
Foreground fg | color | #0b0b0c |
Accent accent | color | #6366f1 |
Questions (theme blocks) blocks Up to 6 questions. Each question has up to 4 answer options. Each option carries comma-separated tags that map to product tags in the recommendation collection. | blocks | , |
SEO & accessibility notes
- Result products are pre-rendered server-side, fully crawlable for SEO even though the visible set is JS-controlled.
- Real <button> for every option, full keyboard navigation, screen-reader friendly.
- Progress bar uses a single CSS width transition, no animation library.
- Cart-add fires the standard cart:updated event so other sections (cart drawer, mini-cart) refresh automatically.
- Quiz state lives in memory only, no localStorage / GDPR overhead.
Related
