Recipes
Practical patterns for common funnel-building tasks. Each recipe combines variables, Liquid templates, conditions, and component configuration to solve a real use case.
Progress Bar from Page Variables
Display a visual progress bar that fills as the visitor advances through the funnel.
Method 1: Custom CSS on a Stack
Create a Stack element for the progress fill. In its Custom CSS:
width: {{ page.progressPercentage }}%;
height: 4px;
background: linear-gradient(90deg, #6366f1, #8b5cf6);
border-radius: 2px;
transition: width 0.4s ease;Wrap it in a parent Stack with a gray background to create the track:
width: 100%;
height: 4px;
background-color: #e5e7eb;
border-radius: 2px;
overflow: hidden;page.progressPercentage updates automatically on every page change, calculating progress based on the expected path length.
Method 2: Text-based step counter
Use a Text element with Liquid:
Step {{ page.current }} of {{ page.total }}Result: Step 3 of 8
Method 3: Percentage display
{{ page.progressPercentage }}% completeCountdown Timer
Display a countdown that ticks down in real time using page.timeOnCurrent.
Basic countdown (seconds)
In a Text element, count down from 5 minutes (300 seconds):
{% assign remaining = 300 | minus: page.timeOnCurrent %}
{% assign minutes = remaining | divided_by: 60 | floor %}
{% assign seconds = remaining | modulo: 60 | round %}
{% if remaining > 0 %}
{{ minutes }}:{% if seconds < 10 %}0{% endif %}{{ seconds }}
{% else %}
0:00
{% endif %}page.timeOnCurrent updates every second, so the countdown ticks in real time.
Urgency message with time pressure
{% assign remaining = 600 | minus: page.timeOnCurrent %}
{% if remaining > 300 %}
Take your time to decide.
{% elsif remaining > 60 %}
This offer expires in {{ remaining | divided_by: 60 | floor }} minutes.
{% elsif remaining > 0 %}
Hurry! Only {{ remaining | round }} seconds left.
{% else %}
This offer has expired.
{% endif %}CSS-based animation timer
Animate a progress bar that shrinks over 60 seconds. In Custom CSS:
{% assign elapsed = page.timeOnCurrent %}
{% assign total = 60 %}
{% assign remaining = total | minus: elapsed %}
{% assign percent = remaining | divided_by: total | times: 100 %}
width: {% if percent > 0 %}{{ percent }}%{% else %}0%{% endif %};
height: 3px;
background-color: {% if percent > 50 %}#22c55e{% elsif percent > 20 %}#eab308{% else %}#ef4444{% endif %};
transition: width 1s linear, background-color 0.3s ease;The bar shrinks from 100% to 0% over 60 seconds, changing from green to yellow to red.
Personalized Content with User Answers
Use quiz answers to customize messaging throughout the funnel.
Personalized headline
{{ answers.name | default: "Friend" }}, your personalized {{ answers.goal | downcase }} plan is ready.Dynamic content blocks based on experience
Use dynamic properties on the hidden property to show different content based on answers:
Beginner block — visible when:
- Condition:
answers.experienceequals"beginner"
Advanced block — visible when:
- Condition:
answers.experienceequals"advanced"
Each block contains different copy, images, and calls to action tailored to the audience.
Answer summary page
Here's your profile:
Name: {{ answers.name }}
Goal: {{ answers.goal | capitalize }}
Experience: {{ answers.experience | capitalize }}
Interests: {{ answers.interests | size }} selected
Budget: ${{ answers.budget | default: 0 | to_fixed: 2 }}/monthConditional social proof
{% if answers.experience == 'beginner' %}
Join 50,000+ beginners who started their journey with us.
{% elsif answers.experience == 'intermediate' %}
Level up like 25,000+ intermediate users.
{% else %}
Trusted by 10,000+ expert-level professionals.
{% endif %}Conditional Visibility with Dynamic Properties
Control which components appear based on variable state.
Show discount badge after timer
- Add a Set Variable click action (or use a timer mechanism) to set
data.showDiscount = trueafter a delay. - On the discount badge component, set a dynamic property on
hidden:
| Order | Condition | Value |
|---|---|---|
| 1 | data.showDiscount equals true | false (visible) |
| 2 | (no condition) | true (hidden) |
Show different CTAs for mobile vs desktop
On the “Download App” button, set a dynamic property on hidden:
| Order | Condition | Value |
|---|---|---|
| 1 | device.isMobile equals true | false (visible) |
| 2 | (no condition) | true (hidden) |
On the “Continue on Desktop” text, use the inverse:
| Order | Condition | Value |
|---|---|---|
| 1 | device.isMobile equals true | true (hidden) |
| 2 | (no condition) | false (visible) |
Show payment error message
On an error text component, set visibility based on payment.error:
| Order | Condition | Value |
|---|---|---|
| 1 | payment.error isNotEmpty | false (visible) |
| 2 | (no condition) | true (hidden) |
In the text content:
{{ payment.error }}Post-purchase content
Show a “thank you” section only after a successful purchase:
| Order | Condition | Value |
|---|---|---|
| 1 | purchase.success equals true | false (visible) |
| 2 | (no condition) | true (hidden) |
Product Pricing Display
Build pricing cards and comparison tables using product variables.
Simple pricing card
{{ products.monthly.displayName }}
{{ products.monthly.price }}/{{ products.monthly.period }}Result: Monthly Plan — $9.99/month
Pricing with trial
{% if products.selected.hasTrial %}
Start your {{ products.selected.trialDays }}-day free trial
Then {{ products.selected.price }}/{{ products.selected.period }}
{% else %}
{{ products.selected.price }}/{{ products.selected.period }}
{% endif %}Price comparison: monthly vs yearly
For the yearly pricing card:
{{ products.yearly.price }}/{{ products.yearly.period }}
{% assign monthlyEquivalent = products.yearly.rawPrice | divided_by: 12 | to_fixed: 2 %}
Just {{ products.yearly.currencySymbol }}{{ monthlyEquivalent }}/monthFor the savings badge:
{% assign monthlyTotal = products.monthly.rawPrice | times: 12 %}
{% assign saved = monthlyTotal | minus: products.yearly.rawPrice %}
{% assign percent = saved | divided_by: monthlyTotal | times: 100 | round: 0 %}
SAVE {{ percent }}%Per-day pricing
{{ products.yearly.currencySymbol }}{{ products.yearly.rawPrice | divided_by: 365 | to_fixed: 2 }}/dayResult: $0.16/day (for a $59.99/year plan)
Dynamic “most popular” badge
Use Active Style on a pricing card Stack. Set the active condition:
products.selectedProductIdequals the product’s ID
The Active Style applies a highlight border and “Most Popular” badge styling when that product is selected.
Time-Based Formatting
Use time variables for dynamic, time-aware content.
Time spent on page
{% if page.timeOnCurrent > 60 %}
{% assign mins = page.timeOnCurrent | divided_by: 60 | floor %}
You've been here for {{ mins }} minute{% if mins > 1 %}s{% endif %}.
{% else %}
You've been here for {{ page.timeOnCurrent }} seconds.
{% endif %}Session duration
{% assign sessionSeconds = system.now | minus: session.startedAt | divided_by: 1000 %}
{% assign sessionMinutes = sessionSeconds | divided_by: 60 | floor %}
Session time: {{ sessionMinutes }} minUrgency based on time on page
In the Custom CSS of a CTA button, pulse the background when the visitor has been on the page for more than 30 seconds:
{% if page.timeOnCurrent > 30 %}
animation: pulse 2s infinite;
{% endif %}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.03); }
}Spinner Wheel Discount
Use the Spinner Wheel element to gamify discounts.
- Configure the Spinner Wheel with segments (e.g., “10% Off”, “20% Off”, “Free Shipping”).
- After the wheel spins, the result is stored in
element.{spinnerId}.result. - Display the result:
{% if element.spinner1.result != '' %}
Congratulations! You won: {{ element.spinner1.result }}
{% endif %}- Use a condition to conditionally show the claim button:
element.{spinnerId}.resultisNotEmpty
Dialog/Drawer Controlled by Variables
Open a dialog from a button click
-
Add a Dialog component to the page.
-
On a button, add a Set Variable click action:
- Variable:
element.{dialogId}.open - Value:
true
- Variable:
-
Inside the dialog, add a close button with:
- Variable:
element.{dialogId}.open - Value:
false
- Variable:
Conditional dialog on page load
Use a dynamic property on element.{dialogId}.open:
| Order | Condition | Value |
|---|---|---|
| 1 | page.timeOnCurrent greaterThan 10 | true |
| 2 | (no condition) | false |
This opens the dialog automatically after 10 seconds on the page.
Element variables use the component’s internal ID, which looks like element.abc123.open. You can find the exact variable name in the variable picker when configuring click actions.
Multi-Select Answer Count
Show how many items the visitor selected from a Multi Select question:
You selected {{ answers.interests | size }} interests.
{% if answers.interests | size > 3 %}
Great choices! You'll get recommendations for all {{ answers.interests | size }} categories.
{% elsif answers.interests | size > 0 %}
Select at least 3 interests for better recommendations.
{% else %}
Please select at least one interest to continue.
{% endif %}Use the array size in a condition to require minimum selections before allowing navigation:
- Condition on the “Next” button’s
hiddenproperty:answers.interestslessThan1(compares array length)- Value:
true(hidden when less than 1 selection)