Shopify Builds>LUS Brands>Curl Quiz 3.0: a nine-step diagnostic quiz rendered entirely from Shopify metaobjects

Curl Quiz 3.0: A Nine-Step Diagnostic Quiz Rendered Entirely From Shopify Metaobjects

A nine-step hair diagnostic on the LUS Brands storefront whose questions, options and selection rules live in Shopify metaobjects rather than in the section's code. The theme editor exposes exactly one setting.

Shopify has no primitive for a questionnaire and nowhere to hold a shopper's answers between steps. We built the quiz as one Liquid section that renders whatever the curl_quiz metaobject contains — nine questions, 43 options, per-question minimum and maximum selection rules — and publishes the finished answers as a DOM event and a shareable querystring, so the merchandising team edits the quiz in Shopify data rather than in code.

Fact Strip

  • Client: LUS Brands
  • Surface: A standalone quiz page — one template, page.quiz
  • Content model: 6 metaobject definitions, 103 entries, zero metafields
  • Theme-editor settings: 1
  • Complexity: High
  • Attribution: Deploi-authored. The section, its controller and the metaobject data model are ours; they run on a Dawn 15.4.1 base.
  • Status: Live, verified 2026-09-06
  • Evidence: Strong

The Problem

Shopify's storefront has no questionnaire primitive. There is no answer store, no session on a theme page, and Liquid renders once per request — so it cannot branch on an answer a shopper has not submitted yet. Anything that asks nine questions and reacts has to be built.

LUS needed one, because curly-hair shopping is genuinely diagnostic. Strand thickness, density, porosity and curl pattern each change which cleanser, conditioner and styler will work, and an unguided catalog sends a first-time buyer to the wrong three products. The quiz was to be the storefront's primary discovery path.

The other half of the problem was ownership. The question set was still being revised against a Curl Quiz 3.0 specification while the theme was being built, and the merchandising team — not a developer — had to reword a question, add an option, or change how many answers it accepts.

The Constraint

Three limits set the shape of this.

Liquid cannot branch across steps without a page load, and there is no session to hold answers, so every piece of step state had to live in the browser. Nothing about progress, selection or resume position could be a server concern.

The question set is not known at render time. It is whatever the curl_quiz entry's questions list contains that day, so the section had to render an arbitrary number of panes, an arbitrary grouping sidebar and arbitrary per-question selection rules from data.

And the theme editor had to stay almost empty. Section settings are the wrong editing surface here: they are per-placement, they cannot be referenced by anything else, and giving the marketing team write access to them means giving them the theme. Metaobject entries are a place we could hand over safely, so the section was designed down to a single setting to push all editing there.

What We Built

One section, curl-quiz.liquid, that reads shop.metaobjects.curl_quiz[handle] — default handle curl-quiz-3-0 — and renders one [data-cq-pane] per question, plus an email-capture pane and a synthetic "generating" pane at the end.

Every behavioral rule is projected out of the metaobject onto data attributes the inline controller reads back: data-cq-question-key, data-cq-selection-type (single or multiple), data-cq-min, data-cq-max, and per option data-cq-option-key and data-cq-exclusive. The controller enforces the rules; the metaobject states them. Neither one hard-codes a question.

The left sidebar is assembled by a nested Liquid pass that de-duplicates each question's group value into ordered groups — accumulated into a rendered_groups token string, since Liquid has no uniq filter over a list of metaobject references — and maps every sidebar row back to its pane index through question_key. Completed steps become clickable, steps beyond furthestStep stay locked, and both states are keyboard-operable.

Answers are mirrored into the querystring as cq_<question_key>=opt1,opt2 through history.replaceState, and hydrateFromUrl() replays them on load: it reselects the options, respects exclusivity and the maximum cap, and resumes at the first step whose answer count is still below its minimum. A half-finished quiz is a URL, not a record.

Completion is a publication, not a call. The section dispatches a window-level curl-quiz:results CustomEvent carrying { answers, email, emailSubmitted }, then hides itself. It does not know what listens. The results engine is a separate section that subscribes to that event. Publishing rather than calling is what keeps the two halves independently placeable and independently editable in the theme editor.

Underneath all of it sits a data model of six metaobject definitions and 103 entries: curl_quiz, curl_quiz_question and curl_quiz_option drive this section; curl_quiz_logic_rule, curl_quiz_product_recommendation and curl_quiz_result_section drive the results. The theme carries no question list, no option list and no metafields at all.

Why This Way

The event-plus-querystring contract is the load-bearing decision. Because the quiz publishes a DOM event and encodes its state in the URL instead of calling a renderer directly, the quiz and the results are two ordinary sections a merchandiser can reorder or move apart, and a completed result is a plain link that reconstructs itself with no backend.

Modeling the questions as metaobjects rather than as section blocks was the other call. Block schemas cap out, and — more usefully — a metaobject entry can be referenced by the logic rules that drive the recommendations. The same option entry is both a rendered button and a rule input, which removes an entire class of bug: a rule pointing at an option key that no longer exists.

What it costs is ownership of behavior a hosted quiz ships with. Minimum and maximum enforcement, mutual exclusivity, inline error states, resume position, sidebar keyboard access — all of it is now code we wrote and maintain. The grouping pass carries a smaller, sharper cost: recomputing each group's step total in an inner loop is O(n²), fine across nine questions and the reason to keep group count low.

Why Not an App

Hosted quiz platforms are the usual purchase for this feature, and the category is well populated. They install in an afternoon.

What they hand back is a widget in an iframe or an injected script on the highest-intent page on the site. It cannot inherit the theme's type scale, color system or spacing, so the quiz looks adjacent to the brand rather than part of it. Pricing runs per response, which taxes the exact behavior the brand is trying to grow. And the answer-to-product mapping lives in a vendor dashboard.

Here the quiz is theme-native markup, and the questions, options and rules are Shopify data on LUS's own store. It's exportable, it survives a theme change, and it can be read by anything else in the storefront that wants it.

Implementation Notes

  • The data model is six metaobject definitions and 103 entries, and no metafields: curl_quiz (1 entry), curl_quiz_question (9), curl_quiz_option (43), curl_quiz_logic_rule (34), curl_quiz_product_recommendation (13) and curl_quiz_result_section (3).
  • A configuration audit run against a point-in-time snapshot of the store's custom data confirmed the shape from the other side: nine questions matching the specification, and no quiz-related metafield definitions anywhere — the quiz is entirely metaobject-driven.
  • Selection-rule reads are written defensively as question.maximum_selections.value | default: question.max_selections.value | default: 1, and the same for the minimum, so the section tolerated either field naming while the definitions were being migrated.
  • The live selection caps, confirmed by that same validation run: six single-select questions at min 1 / max 1, curl_challenges and scalp_concerns at min 1 / max 2, and curl_goals at min 1 / max 3.
  • Multi-select exclusivity is real exclusivity. Choosing an option marked data-cq-exclusive — the "None" answers on challenges and scalp concerns — clears every other selection, and choosing any normal option clears the exclusive one.
  • At the cap, the oldest selection is dropped rather than the new click being ignored. A shopper who has picked two of a maximum of two and clicks a third gets the third, not a dead control.
  • getStickyHeaderOffset() measures whichever fixed or sticky header sits within 80px of the viewport top and scrolls each pane to just under it, rather than hard-coding a height — the header differs between this theme's adult and kids variants.
  • The "generating your routine" pane is theater, deliberately, with two durations: 3200ms after an email submit and 1800ms after a skip, stepping a four-item progress list. It is not waiting on a network call.

Edge Cases

  • A missing or empty quiz metaobject renders nothing at all on the storefront, and renders a "Select a valid Curl Quiz metaobject entry" notice only inside the theme editor.
  • A root.dataset.bound guard stops a second controller attaching when the theme editor reloads the section or a merchandiser duplicates it onto the same page.
  • URL hydration tolerates hand-edited links: unknown option keys drop out through a CSS.escape'd lookup returning null, over-cap selections are sliced back to the maximum, and an exclusive option in the query wins.
  • Resume walks the panes until it finds the first question whose answer count is below its minimum, so a truncated link resumes at the right step instead of jumping to the end.
  • Advancing past a question with too few selections injects an inline role="alert" message and blocks navigation. The message clears on the next selection.
  • The email step is optional by design: "Send & view results" validates the address and refocuses the field on failure, and "Skip and go right to the results" proceeds with an empty one.
  • Every sessionStorage read and write is wrapped in try/catch, so Safari private mode or a storage-blocked browser still finishes the quiz.
  • Sidebar steps are keyboard-operable with role="button" and an Enter/Space handler, and locked steps leave the tab order at tabIndex -1 rather than merely refusing clicks.

Platform Primitives Used

  • Metaobjects — six definitions and 103 entries carrying every question, option, label and rule. The theme holds none of it.
  • Liquid section schema and settings — deliberately reduced to one setting, so the editing surface is the metaobject entry and not the theme editor.
  • Custom elements and DOM events — a window-level curl-quiz:results CustomEvent is the entire contract between this section and the results section.

Where It Runs

One template, page.quiz, on a dedicated quiz page — the discovery route LUS wanted shoppers to take before the catalog. The section is placed by itself, with the results section mounted alongside it on the same template; because the two communicate over a window event, either can be moved without touching the other.

What This Demonstrates

  • Guided selling: quizzes, finders and routine builders — a diagnostic questionnaire authored as Shopify data, rendered by the theme, with the matching done in the browser.
  • FAQ and self-service content — nine questions that answer, in effect, the support question a curly-hair shopper cannot answer for themselves.

How We Know

Two theme files carry this build — the section and its stylesheet — and both were read from the live theme, alongside the quiz page template. The question and option data was verified separately, against a point-in-time snapshot of the store's custom data pulled from the Admin API. The six definitions and 103 entries are counted from that snapshot rather than inferred. Behind the code sit thirteen client working-session records from April and May 2026, in which the stepper structure, the removal of illustrative imagery and the mobile treatment were each decided.

Related Builds

The Buy-vs-Build Question

The buy case for a quiz is the strongest in this corpus: mature category, quick setup, no controller to maintain. It loses on where the data ends up. A quiz whose questions and rules live in a vendor dashboard cannot be read by the storefront, exported, or reused by the recommendation logic — and here the rules had to be readable by both. The trade is laid out in the product quiz and finder decision.

Ready to Turn Your Catalog Into a Diagnosis?

We build guided selling as theme-native code with the content model in your own Shopify data. Contact us today to talk through the questions your shoppers can't answer alone.

More builds