Shopify themes, liquid, logos, and UX
Tried to add an FAQ section to our collection using the following code:
Sections/faq.liquid
{%- comment -%}
Section published at https://sections.design/blogs/shopify/faq-rich-snippets-section
Get the latest version: https://github.com/mirceapiturca/Sections/tree/master/FAQ
{%- endcomment -%}
{%- comment -%} ---------------- THE CSS ---------------- {%- endcomment -%}
{%- assign id = '#shopify-section-' | append: section.id -%}
{% style %}
{{ id }} {
background: {{ section.settings.background_color }};
--panel-bg: {{ section.settings.panel_color }};
--border-color: {{ section.settings.border_color }};
--question-color: {{ section.settings.q_color }};
--answer-color: {{ section.settings.a_color }};
{%- assign min = section.settings.q_size_small -%}
{%- assign max = section.settings.q_size_large -%}
{%- assign min_rem = min | append: 'rem' -%}
{%- assign max_rem = max | append: 'rem' -%}
--title-font-size: clamp({{ min_rem }}, calc({{ min_rem }} + ({{ max }} - {{ min }}) * ((100vw - 25rem) / (64 - 25))), {{ max_rem }});
}
{% endstyle %}
<style>
.flex { display: flex }
.items-center { align-items: center }
.justify-between { justify-content: space-between }
.w-full { width: 100% }
.text-left { text-align: left }
.m-0 { margin: 0 }
.p-0 { padding: 0 }
.p-4 { padding: 1rem }
.overflow-hidden { overflow: hidden }
.cursor-pointer { cursor: pointer }
{{ id }} .faq-container {
max-width: {{ section.settings.max_width }};
margin: {{ section.settings.margin_top }}rem auto {{ section.settings.margin_bottom }}rem
}
{{ id }} .faq-title {
border-bottom: 1px solid var(--border-color);
font-size: var(--title-font-size);
color: var(--question-color);
}
{{ id }} .faq-panel {
will-change: height;
border-bottom: 1px solid var(--border-color);
background-color: var(--panel-bg);
color: var(--answer-color);
}
.faq-button {
font: inherit;
background: transparent;
border: 0;
}
.faq-icon {
width: clamp(12px, 0.65em, 20px);
height: clamp(12px, 0.65em, 20px);
min-width: clamp(12px, 0.65em, 20px);
margin-left: 1rem;
}
.faq-icon-minus {
transition: transform 240ms cubic-bezier(0.4, 0.0, 0.2, 1);
transform-origin: 50% 50%;
}
.faq-button[aria-expanded="true"] .faq-icon-minus {
transform: rotate(90deg);
}
.faq-panel * {
color: inherit;
}
.faq-panel[data-is-animating] {
display: block!important;
}
</style>
{%- comment -%} ---------------- THE MARKUP ---------------- {%- endcomment -%}
<div class="faq-container">
{%- for block in section.blocks -%}
{%- if block.settings.title != blank and block.settings.content != blank -%}
{%- if block.settings.checkbox_expanded == true -%}
{%- assign expanded = 'true' -%}
{%- assign hidden = '' -%}
{%- else -%}
{%- assign expanded = 'false' -%}
{%- assign hidden = 'hidden' -%}
{%- endif -%}
<h2 class="faq-title m-0 p-0" data-faq-trigger="{{ block.id }}" {{ block.shopify_attributes }}>
<button class="faq-button flex items-center justify-between w-full text-left m-0 p-4 cursor-pointer" data-faq-button="{{ block.id }}" aria-expanded="{{ expanded }}">
<span>{{ block.settings.title }}</span>
<svg class="faq-icon" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path class="faq-icon-minus" fill="currentColor" d="M8 0v14H6V0z"></path>
<path fill="currentColor" d="M0 6h14v2H0z"></path>
</svg>
</button>
</h2>
<div class="faq-panel rte overflow-hidden custom-bg" data-faq-panel="{{ block.id }}" {{ hidden }}>
<div class="faq-wrap p-4">{{ block.settings.content }}</div>
</div>
{%- endif -%}
{%- endfor -%}
</div>
{%- comment -%} -------------- THE RICH SCHEMA ------------- {%- endcomment -%}
{%- if section.settings.enable_rich_schema -%}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{%- for block in section.blocks -%}
{%- if block.settings.title != blank and block.settings.content != blank -%}
{
"@type": "Question",
"name": {{ block.settings.title | json }},
"acceptedAnswer": {
"@type": "Answer",
"text": {{ block.settings.content | strip_html | json }}
}
}{%- unless forloop.last -%},{%- endunless -%}
{%- endif -%}
{%- endfor -%}
]
}
</script>
{%- endif -%}
{%- comment -%} ---------------- THE CONFIG ---------------- {%- endcomment -%}
<script type="application/json" data-faq-config="{{ section.id }}" >
{
"sectionId": {{ section.id | json }},
"blockIds": {{ section.blocks | map: 'id' | json }}
}
</script>
{%- comment -%} ---------------- THE SETTINGS ---------------- {%- endcomment -%}
{% schema %}
{
"name": "FAQ",
"class": "sd-faq",
"tag": "article",
"settings": [
{
"type": "header",
"content": "Rich schema"
},
{
"type": "checkbox",
"id": "enable_rich_schema",
"default": true,
"label": "Enable FAQ rich schema?"
},
{
"type": "header",
"content": "Dimensions"
},
{
"type": "text",
"id": "max_width",
"label": "Max width",
"default": "64rem"
},
{
"type": "range",
"id": "margin_top",
"min": 0,
"max": 10,
"step": 0.1,
"unit": "rem",
"label": "Margin top",
"default": 1
},
{
"type": "range",
"id": "margin_bottom",
"min": 0,
"max": 10,
"step": 0.1,
"unit": "rem",
"label": "Margin bottom",
"default": 1
},
{
"type": "header",
"content": "Colors"
},
{
"type": "color",
"id": "background_color",
"label": "Background color",
"default": "#ffffff"
},
{
"type": "color",
"id": "border_color",
"label": "Border color",
"default": "#eeeeee"
},
{
"type": "color",
"id": "panel_color",
"label": "Panel color",
"default": "#fdfdfd"
},
{
"type": "header",
"content": "Question"
},
{
"type": "range",
"id": "q_size_small",
"min": 1,
"max": 2,
"step": 0.1,
"unit": "rem",
"label": "Small devices font size",
"default": 1
},
{
"type": "range",
"id": "q_size_large",
"min": 1,
"max": 3,
"step": 0.1,
"unit": "rem",
"label": "Large devices font size",
"default": 1.4
},
{
"type": "color",
"id": "q_color",
"label": "Question text color"
},
{
"type": "header",
"content": "Answer"
},
{
"type": "color",
"id": "a_color",
"label": "Answer text color"
}
],
"blocks": [
{
"type": "faq",
"name": "FAQ",
"settings": [
{
"type": "checkbox",
"id": "checkbox_expanded",
"default": false,
"label": "Expanded?"
},
{
"type": "text",
"id": "title",
"label": "FAQ title",
"default": "FAQ title"
},
{
"type": "richtext",
"id": "content",
"label": "FAQ content",
"default": "<p>FAQ content</p>"
}
]
}
],
"presets": [
{
"name": "FAQ"
}
]
}
{% endschema %}
{%- comment -%} ------------------ THE JS ----------------- {%- endcomment -%}
<script src="{{ 'faq.js' | asset_url }}" defer></script>
{%- comment -%} ---------------- THE NO-JS ---------------- {%- endcomment -%}
<noscript>
<style>
#shopify-section-{{ section.id }} [hidden] { display: block }
.faq-icon { display: none }
</style>
</noscript>
{%- comment -%} ---------------- THE EDITOR ------------------ {%- endcomment -%}
{%- if request.design_mode -%}
<script>
(function FAQThemeEditor(SectionsDesign) {
'use strict';
document.addEventListener('shopify:section:load', sectionLoad);
document.addEventListener('shopify:block:select', blockToggle);
document.addEventListener('shopify:block:deselect', blockToggle);
function sectionLoad(evt) {
var sectionId = evt.detail.sectionId;
var section = SectionsDesign.faq[sectionId];
if (!section) return;
SectionsDesign.faq[sectionId] = section.init(sectionId);
}
function blockToggle(evt) {
var section = SectionsDesign.faq[evt.detail.sectionId];
if (!section) return;
var block = section.blocks[evt.detail.blockId];
if (!block) return;
evt.type === 'shopify:block:select' ? block.select() : block.deselect();
}
})(window.SectionsDesign = window.SectionsDesign || {});
</script>
{%- endif -%}
assets/faq.js
(function FAQ(SectionsDesign) {
'use strict';
if (SectionsDesign.faq) return;
SectionsDesign.faq = {};
var support = getSupport();
var init = function() {
var configNodes = Array.from(document.querySelectorAll('[data-faq-config]'));
configNodes.map(configNode => {
var section = compose(publicAPI, setEvents, getBlocks, getConfig)(configNode);
SectionsDesign.faq[section.id] = section;
});
}
init();
function publicAPI(config) {
return {
id: config.sectionId,
config: config,
blocks: zipObj(config.blockIds, config.blocks),
init: init
}
}
//**************************
/**
* Click event.
* @param {Object} block Section block elements and methods.
* @Return {Object} Section block elements and methods.
*/
function blockEvents(block) {
if (block.trigger.hasAttribute('data-faq-init')) return block;
block.trigger.setAttribute('data-faq-init', true);
block.trigger.addEventListener('click', triggerClick);
function triggerClick() {
toggle(block);
}
return block;
}
/**
* Toggle block state.
* @param {Object} block Section block elements and methods.
* @Return {Object} Section block elements and methods.
*/
function toggle(block) {
block.collapsed ? expand(block) : collapse(block);
return block;
}
/**
* Expand block.
* @param {Object} block Section block elements and methods.
* @Return {Object} Section block elements and methods.
*/
function expand(block) {
block.button.setAttribute('aria-expanded', true);
block.panel.removeAttribute('hidden');
animate(block.panel, 'normal');
return block;
}
/**
* Collapse block.
* @param {Object} block Section block elements and methods.
* @Return {Object} Section block elements and methods.
*/
function collapse(block) {
block.button.setAttribute('aria-expanded', false);
block.panel.setAttribute('hidden', '');
animate(block.panel, 'reverse');
return block;
}
/**
* Collapse block.
* @param {Object} block Section block elements and methods.
* @Return {boolean} Collapsed block state.
*/
function isCollapsed(block) {
return Boolean(block.button.getAttribute('aria-expanded') === 'false');
}
/**
* Collapse block.
* @param {HTMLElement} element Block panel element to animate.
* @param {String} direction Animation direction, normal or reverse.
* @Return {undefined} Nothing to return.
*/
function animate(element, direction) {
if (!support.WebAnimations) return;
element.setAttribute('data-is-animating', true);
element.animate([
{ height: 0 },
{ height: element.offsetHeight + 'px' }],
{ duration: 240,
fill: 'both',
easing: 'cubic-bezier(0.4, 0.0, 0.2, 1)',
direction: direction
}
).onfinish = function() {
element.removeAttribute('data-is-animating');
this.cancel();
};
}
//**************************
/**
* Maps all block IDs to exposed block API.
* @param {Object} config Section and section blocks IDs.
* @Return {Object} config Section and section blocks IDs.
*/
function getBlocks(config) {
config.blocks = config.blockIds.map(block);
return config;
}
/**
* Create section block API.
* @param {String} blockId Section Liquid block ID.
* @Return {Object} block elements and methods.
*/
function block(blockId) {
return {
trigger: document.querySelector('[data-faq-trigger="' + blockId + '"]'),
button: document.querySelector('[data-faq-button="' + blockId + '"]'),
panel: document.querySelector('[data-faq-panel="' + blockId + '"]'),
get collapsed() { return isCollapsed(this) },
select: function select() { return expand(this) },
deselect: function deselect() { return collapse(this) }
}
}
/**
* Adds event listeners to block elements.
* @param {Object} config Section and section blocks IDs.
* @Return {Object} config Section and section blocks IDs.
*/
function setEvents(config) {
config.blocks.forEach(blockEvents);
return config;
}
/**
* Pass the Liquid assigned section variabiles.
* @param {HTMLElement} element Configuration script node.
* @Return {Object} Section and section blocks IDs.
*/
function getConfig(node) {
return JSON.parse(node.innerHTML);
}
/**
* Feature detection.
* @Return {Object} Browser support.
*/
function getSupport() {
return {
WebAnimations: (typeof Element.prototype.animate === 'function')
}
}
//**************************
/**
* Creates a new object out of a list of keys and a list of values.
* Key/value pairing is truncated to the length of the shorter of the two lists.
* @example
* zipObj(['a', 'b', 'c'], [1, 2, 3]); //=> {a: 1, b: 2, c: 3}
* @param {Array} keys The array that will be properties on the output object.
* @param {Array} values The list of values on the output object.
* @Return {Object} The object made by pairing up same-indexed elements of `keys` and `values`.
*/
function zipObj(keys, values) {
return keys.reduce(
function zipObj(acc, key, idx) {
acc[key] = values[idx];
return acc;
}, {}
)
}
/**
* Performs right-to-left function composition.
* The rightmost function may have any arity, the remaining functions must be unary.
* @example
* function plus1(n) {return n + 1};
* function plus2(n) {return n + 2};
* compose(plus2,plus1)(1) => 4
* @Return {Function} Composed function
*/
function compose() {
var funcs = Array.prototype.slice.call(arguments).reverse();
return function() {
return funcs.slice(1).reduce(function(res, fn) {
return fn(res);
}, funcs[0].apply(undefined, arguments));
};
}
})(window.SectionsDesign = window.SectionsDesign || {});
How do I get it to display on collection page? I added the line {% include 'faq' %} to collection template, but get the error Liquid error (sections/collection-template line 12): Could not find asset snippets/faq.liquid
Hey @ChrisW3
The error occurs because you are using the {% include 'faq' %} syntax, which is designed to include snippet files. Instead of using {% include 'faq' %}, you should use the section inclusion syntax for a file located in the Sections folder.
{% section 'faq' %}
If I managed to help you then, don't forget to Like it and Mark it as Solution!
Best Regards,
Moeed
Now getting this
Liquid error (sections/collection-template line 131): Cannot render sections inside sections
I see, here's an alternate solution:
Use a Snippet Instead of a Section
Convert your FAQ section into a snippet. Snippets can be included within sections.
Steps:
1) Copy the code from Sections/faq.liquid.
2) Create a new snippet by going to Snippets > Add a new snippet and name it faq.
3) Paste the copied code into this new snippet file.
Now add this again where you added the {% section 'faq' %} code.
{% include 'faq' %}
If I managed to help you then, don't forget to Like it and Mark it as Solution!
Best Regards,
Moeed
Cant get this to work.
Now getting this:
"Liquid syntax error (line 161): Unknown tag 'schema'"
{% include 'faq' %} will call a code in Snippets, not in Sections, and you cannot call a section into another section. Please try to check if your theme has a collapsible content and add to your Collections from Online Store > Themes > Customize > Collections instead of
- Helpful? Like & Accept solution!
- Ryviu - Product Reviews & QA app: Collect customer reviews, import reviews from AliExpress, Amazon, Etsy, Walmart, Dhgate and CSV.
- Lookfy Gallery: Lookbook Image - Gain customers with photo gallery, video & shoppable image.
- Reelfy‑Shoppable Videos+Reels: Create shoppable videos to engage customers and drive more sales.
- Enjoy 1 month of Shopify for $1. Sign up now.
I never got an answer to this
June brought summer energy to our community. Members jumped in with solutions, clicked ...
By JasonH Jun 5, 2025Learn how to build powerful custom workflows in Shopify Flow with expert guidance from ...
By Jacqui May 7, 2025Did You Know? May is named after Maia, the Roman goddess of growth and flourishing! ...
By JasonH May 2, 2025