Dynamic Content: Metafields and Tabs
Display rich product information using metafields, organize content with tabs and accordions, and create dynamic product pages.
Products often need more than titles and descriptions. Metafields let you add custom data, while tabs and accordions organize that information for easy scanning.
Understanding Metafields
Metafields store custom data beyond standard Shopify fields:
{# Access product metafields #}{{ product.metafields.custom.care_instructions }}{{ product.metafields.custom.materials }}{{ product.metafields.specifications.weight }}Metafields follow this pattern:
resource.metafields.namespace.keyMetafield Types
Shopify supports various metafield types:
{# Single-line text #}{{ product.metafields.custom.subtitle.value }}
{# Multi-line text / Rich text #}{{ product.metafields.custom.features.value }}
{# Number (integer or decimal) #}{{ product.metafields.specs.weight.value }} kg
{# URL #}<a href="{{ product.metafields.custom.manual.value }}">Download Manual</a>
{# File/Image #}<img src="{{ product.metafields.custom.size_chart.value | image_url: width: 800 }}" alt="Size chart">
{# JSON #}{%- assign nutrition = product.metafields.custom.nutrition.value -%}Calories: {{ nutrition.calories }}
{# List of values #}{%- for feature in product.metafields.custom.features.value -%} <li>{{ feature }}</li>{%- endfor -%}Displaying Metafield Content
Simple Text Metafield
{%- if product.metafields.custom.subtitle -%} <p class="product__subtitle"> {{ product.metafields.custom.subtitle.value }} </p>{%- endif -%}Rich Text Metafield
{%- if product.metafields.custom.details -%} <div class="product__details rte"> {{ product.metafields.custom.details.value }} </div>{%- endif -%}Metafield with Fallback
{%- assign care_info = product.metafields.custom.care_instructions.value | default: 'Machine wash cold. Tumble dry low.' -%}<p>{{ care_info }}</p>Building a Tabbed Interface
Organize product information in tabs:
{# sections/product-tabs.liquid #}
<product-tabs class="product-tabs"> <div class="product-tabs__nav" role="tablist"> <button class="product-tabs__tab is-active" role="tab" id="tab-description" aria-controls="panel-description" aria-selected="true" > Description </button>
{%- if product.metafields.custom.specifications -%} <button class="product-tabs__tab" role="tab" id="tab-specs" aria-controls="panel-specs" aria-selected="false" > Specifications </button> {%- endif -%}
{%- if product.metafields.custom.shipping_info -%} <button class="product-tabs__tab" role="tab" id="tab-shipping" aria-controls="panel-shipping" aria-selected="false" > Shipping </button> {%- endif -%}
<button class="product-tabs__tab" role="tab" id="tab-reviews" aria-controls="panel-reviews" aria-selected="false" > Reviews </button> </div>
<div class="product-tabs__content"> <div class="product-tabs__panel is-active" role="tabpanel" id="panel-description" aria-labelledby="tab-description" > <div class="rte"> {{ product.description }} </div> </div>
{%- if product.metafields.custom.specifications -%} <div class="product-tabs__panel" role="tabpanel" id="panel-specs" aria-labelledby="tab-specs" hidden > {{ product.metafields.custom.specifications.value }} </div> {%- endif -%}
{%- if product.metafields.custom.shipping_info -%} <div class="product-tabs__panel" role="tabpanel" id="panel-shipping" aria-labelledby="tab-shipping" hidden > {{ product.metafields.custom.shipping_info.value }} </div> {%- endif -%}
<div class="product-tabs__panel" role="tabpanel" id="panel-reviews" aria-labelledby="tab-reviews" hidden > {%- render 'product-reviews', product: product -%} </div> </div></product-tabs>Tabs CSS
.product-tabs__nav { display: flex; gap: var(--spacing-xs); border-bottom: 1px solid var(--color-border); overflow-x: auto;}
.product-tabs__tab { padding: var(--spacing-md) var(--spacing-lg); font-size: 0.9375rem; font-weight: 500; color: var(--color-text-light); background: none; border: none; border-bottom: 2px solid transparent; cursor: pointer; white-space: nowrap; transition: all 0.2s;}
.product-tabs__tab:hover { color: var(--color-text);}
.product-tabs__tab.is-active { color: var(--color-text); border-bottom-color: var(--color-primary);}
.product-tabs__content { padding: var(--spacing-lg) 0;}
.product-tabs__panel { display: none;}
.product-tabs__panel.is-active { display: block;}
.product-tabs__panel[hidden] { display: none;}Tabs JavaScript
class ProductTabs extends HTMLElement { connectedCallback() { this.tabs = this.querySelectorAll('[role="tab"]'); this.panels = this.querySelectorAll('[role="tabpanel"]');
this.tabs.forEach((tab) => { tab.addEventListener('click', () => this.switchTab(tab)); tab.addEventListener('keydown', (e) => this.onKeydown(e, tab)); }); }
switchTab(selectedTab) { // Deactivate all tabs this.tabs.forEach((tab) => { tab.classList.remove('is-active'); tab.setAttribute('aria-selected', 'false'); tab.setAttribute('tabindex', '-1'); });
// Hide all panels this.panels.forEach((panel) => { panel.classList.remove('is-active'); panel.hidden = true; });
// Activate selected tab selectedTab.classList.add('is-active'); selectedTab.setAttribute('aria-selected', 'true'); selectedTab.setAttribute('tabindex', '0');
// Show corresponding panel const panelId = selectedTab.getAttribute('aria-controls'); const panel = this.querySelector(`#${panelId}`); panel.classList.add('is-active'); panel.hidden = false; }
onKeydown(event, tab) { const tabs = Array.from(this.tabs); const index = tabs.indexOf(tab);
let newIndex;
switch (event.key) { case 'ArrowRight': newIndex = (index + 1) % tabs.length; break; case 'ArrowLeft': newIndex = (index - 1 + tabs.length) % tabs.length; break; case 'Home': newIndex = 0; break; case 'End': newIndex = tabs.length - 1; break; default: return; }
event.preventDefault(); tabs[newIndex].focus(); this.switchTab(tabs[newIndex]); }}
customElements.define('product-tabs', ProductTabs);Accordion Alternative
On mobile, accordions often work better than tabs:
{# snippets/product-accordions.liquid #}
<div class="product-accordions"> <details class="product-accordion" open> <summary class="product-accordion__header"> <span class="product-accordion__title">Description</span> <span class="product-accordion__icon">+</span> </summary> <div class="product-accordion__content rte"> {{ product.description }} </div> </details>
{%- if product.metafields.custom.specifications -%} <details class="product-accordion"> <summary class="product-accordion__header"> <span class="product-accordion__title">Specifications</span> <span class="product-accordion__icon">+</span> </summary> <div class="product-accordion__content"> {{ product.metafields.custom.specifications.value }} </div> </details> {%- endif -%}
{%- if product.metafields.custom.care_instructions -%} <details class="product-accordion"> <summary class="product-accordion__header"> <span class="product-accordion__title">Care Instructions</span> <span class="product-accordion__icon">+</span> </summary> <div class="product-accordion__content"> {{ product.metafields.custom.care_instructions.value }} </div> </details> {%- endif -%}</div>Accordion CSS
.product-accordions { border-top: 1px solid var(--color-border);}
.product-accordion { border-bottom: 1px solid var(--color-border);}
.product-accordion__header { display: flex; align-items: center; justify-content: space-between; width: 100%; padding: var(--spacing-md) 0; font-weight: 500; cursor: pointer; list-style: none;}
.product-accordion__header::-webkit-details-marker { display: none;}
.product-accordion__icon { font-size: 1.25rem; transition: transform 0.2s;}
.product-accordion[open] .product-accordion__icon { transform: rotate(45deg);}
.product-accordion__content { padding-bottom: var(--spacing-md);}Responsive Tabs/Accordion
Switch between tabs on desktop and accordions on mobile:
{# Tabs for desktop #}<div class="product-tabs--desktop"> {%- render 'product-tabs', product: product -%}</div>
{# Accordions for mobile #}<div class="product-tabs--mobile"> {%- render 'product-accordions', product: product -%}</div>.product-tabs--desktop { display: none;}
.product-tabs--mobile { display: block;}
@media (min-width: 768px) { .product-tabs--desktop { display: block; }
.product-tabs--mobile { display: none; }}Specification Tables
Display structured specifications:
{%- assign specs = product.metafields.custom.specifications.value -%}
{%- if specs -%} <table class="specs-table"> <tbody> {%- for spec in specs -%} <tr> <th>{{ spec.label }}</th> <td>{{ spec.value }}</td> </tr> {%- endfor -%} </tbody> </table>{%- endif -%}For JSON metafield:
[ { "label": "Weight", "value": "250g" }, { "label": "Dimensions", "value": "10 x 5 x 3 cm" }, { "label": "Material", "value": "Cotton" }]Icon-Based Features
Display features with icons:
{%- assign features = product.metafields.custom.features.value -%}
{%- if features -%} <ul class="product-features"> {%- for feature in features -%} <li class="product-feature"> {%- case feature.icon -%} {%- when 'shipping' -%} {% render 'icon-truck' %} {%- when 'returns' -%} {% render 'icon-refresh' %} {%- when 'warranty' -%} {% render 'icon-shield' %} {%- endcase -%} <span>{{ feature.text }}</span> </li> {%- endfor -%} </ul>{%- endif -%}Dynamic Content Blocks
Use section blocks for flexible content:
{ "blocks": [ { "type": "description", "name": "Description", "limit": 1 }, { "type": "metafield", "name": "Metafield Content", "settings": [ { "type": "text", "id": "title", "label": "Title" }, { "type": "text", "id": "namespace", "label": "Metafield namespace", "default": "custom" }, { "type": "text", "id": "key", "label": "Metafield key" } ] }, { "type": "custom_html", "name": "Custom HTML", "settings": [ { "type": "text", "id": "title", "label": "Title" }, { "type": "html", "id": "content", "label": "Content" } ] } ]}{%- for block in section.blocks -%} {%- case block.type -%} {%- when 'description' -%} <div class="content-block" {{ block.shopify_attributes }}> <h3>Description</h3> {{ product.description }} </div>
{%- when 'metafield' -%} {%- assign metafield = product.metafields[block.settings.namespace][block.settings.key] -%} {%- if metafield -%} <div class="content-block" {{ block.shopify_attributes }}> <h3>{{ block.settings.title }}</h3> {{ metafield.value }} </div> {%- endif -%}
{%- when 'custom_html' -%} <div class="content-block" {{ block.shopify_attributes }}> <h3>{{ block.settings.title }}</h3> {{ block.settings.content }} </div> {%- endcase -%}{%- endfor -%}Practice Exercise
Create a product content section that:
- Shows description in a tab
- Displays specifications metafield as a table
- Shows care instructions from metafield
- Switches to accordions on mobile
- Supports keyboard navigation
Test with:
- Products with all metafields
- Products missing some metafields
- Keyboard-only navigation
Key Takeaways
- Metafields store custom product data
- Access pattern:
product.metafields.namespace.key.value - Check existence before displaying metafields
- Tabs organize content for scanning
- Accordions work better on mobile
- ARIA attributes make tabs accessible
- Keyboard navigation for accessibility
- Blocks let merchants customize content order
What’s Next?
The final product page lesson covers Recommendations and Complementary Products for cross-selling.
Finished this lesson?
Mark it complete to track your progress.
Discussion
Loading comments...