Theme Settings and Extensibility Intermediate 12 min read

Metafields and Dynamic Sources

Extend Shopify's content model with metafields to store custom data on products, collections, and other resources, then connect them to your theme.

Metafields extend Shopify’s content model, allowing you to store custom data on products, collections, customers, and other resources. They’re essential for rich product information and custom content.

What Are Metafields?

Metafields are custom fields attached to Shopify resources. Each metafield has:

  • Namespace: Grouping identifier (e.g., custom, my_app)
  • Key: Field name (e.g., care_instructions)
  • Type: Data type (text, number, URL, reference, etc.)
  • Value: The actual content

Creating Metafield Definitions

In Shopify Admin:

  1. Go to Settings → Custom data
  2. Select a resource type (Products, Collections, etc.)
  3. Click Add definition

Common Metafield Types

TypeUse CaseExample
single_line_textShort textSubtitle, tagline
multi_line_textLong textCare instructions
rich_textFormatted contentDetailed descriptions
urlExternal linksSize guide, specs PDF
file_referenceMedia filesAdditional images, videos
product_referenceLink to productComplementary items
collection_referenceLink to collectionRelated category
number_integerWhole numbersStock count, rating
number_decimalDecimal numbersWeight, dimensions
booleanTrue/falseFeatured, on sale
dateCalendar dateRelease date
jsonStructured dataSpecifications object
colorColor valueProduct color
dimensionMeasurementsSize dimensions
weightWeight valueProduct weight

Accessing Metafields in Liquid

Direct Access

{# Access product metafield #}
{{ product.metafields.custom.care_instructions }}
{# With type: multi_line_text #}
{{ product.metafields.custom.care_instructions | newline_to_br }}
{# With type: rich_text (already HTML) #}
{{ product.metafields.custom.description }}

Safe Access with Default

{%- assign care = product.metafields.custom.care_instructions -%}
{%- if care != blank -%}
<div class="product__care">
<h3>Care Instructions</h3>
{{ care | newline_to_br }}
</div>
{%- endif -%}

File Reference Metafields

{%- assign size_guide = product.metafields.custom.size_guide.value -%}
{%- if size_guide -%}
{%- if size_guide.media_type == 'image' -%}
{{ size_guide | image_url: width: 800 | image_tag }}
{%- else -%}
<a href="{{ size_guide.url }}" target="_blank">View Size Guide</a>
{%- endif -%}
{%- endif -%}

Product Reference Metafields

{%- assign complementary = product.metafields.custom.complementary_product.value -%}
{%- if complementary -%}
<div class="complementary-product">
<h3>Goes well with</h3>
<a href="{{ complementary.url }}">
{{ complementary.featured_image | image_url: width: 200 | image_tag }}
<span>{{ complementary.title }}</span>
<span>{{ complementary.price | money }}</span>
</a>
</div>
{%- endif -%}

List Metafields

{# product_reference with list capability #}
{%- assign related = product.metafields.custom.related_products.value -%}
{%- if related.size > 0 -%}
<div class="related-products">
<h3>Related Products</h3>
{%- for item in related -%}
<a href="{{ item.url }}">{{ item.title }}</a>
{%- endfor -%}
</div>
{%- endif -%}

Dynamic Sources in Theme Editor

Metafields can be connected directly in the theme editor using dynamic sources.

Making a Setting Dynamic-Source-Enabled

{
"type": "text",
"id": "subtitle",
"label": "Subtitle"
}

When merchants click the dynamic source icon, they can select:

  • Product metafields
  • Variant metafields
  • Collection metafields
  • Page metafields

Image Settings with Dynamic Sources

{
"type": "image_picker",
"id": "additional_image",
"label": "Additional image"
}

Merchants can connect a file_reference metafield.

URL Settings

{
"type": "url",
"id": "external_link",
"label": "External link"
}

Can be connected to a url metafield.

Building Metafield-Powered Features

Product Specifications Table

{# Assuming JSON metafield with specifications #}
{%- assign specs = product.metafields.custom.specifications.value -%}
{%- if specs -%}
<table class="product-specs">
<caption>Specifications</caption>
<tbody>
{%- for spec in specs -%}
<tr>
<th>{{ spec.label }}</th>
<td>{{ spec.value }}</td>
</tr>
{%- endfor -%}
</tbody>
</table>
{%- endif -%}

Product Tabs with Metafields

{# sections/product-tabs.liquid #}
<div class="product-tabs">
<div class="product-tabs__nav" role="tablist">
<button role="tab" aria-selected="true" data-tab="description">Description</button>
{%- if product.metafields.custom.care_instructions != blank -%}
<button role="tab" data-tab="care">Care</button>
{%- endif -%}
{%- if product.metafields.custom.specifications != blank -%}
<button role="tab" data-tab="specs">Specifications</button>
{%- endif -%}
</div>
<div class="product-tabs__panels">
<div role="tabpanel" id="description" class="product-tabs__panel">
{{ product.description }}
</div>
{%- if product.metafields.custom.care_instructions != blank -%}
<div role="tabpanel" id="care" class="product-tabs__panel" hidden>
{{ product.metafields.custom.care_instructions | newline_to_br }}
</div>
{%- endif -%}
{%- if product.metafields.custom.specifications != blank -%}
<div role="tabpanel" id="specs" class="product-tabs__panel" hidden>
{# Render specs table #}
</div>
{%- endif -%}
</div>
</div>

Collection Metafields

{# Collection banner with metafield image #}
{%- assign banner = collection.metafields.custom.banner_image.value -%}
<div class="collection-banner">
{%- if banner -%}
{{ banner | image_url: width: 1600 | image_tag:
loading: 'eager',
class: 'collection-banner__image'
}}
{%- elsif collection.image -%}
{{ collection.image | image_url: width: 1600 | image_tag }}
{%- endif -%}
<div class="collection-banner__content">
<h1>{{ collection.title }}</h1>
{%- if collection.metafields.custom.subtitle != blank -%}
<p class="collection-banner__subtitle">
{{ collection.metafields.custom.subtitle }}
</p>
{%- endif -%}
</div>
</div>

Customer Metafields

{%- if customer -%}
{%- assign loyalty_points = customer.metafields.custom.loyalty_points -%}
{%- if loyalty_points != blank -%}
<div class="loyalty-badge">
<span>Your Points: {{ loyalty_points }}</span>
</div>
{%- endif -%}
{%- endif -%}

Variant Metafields

Each variant can have its own metafields:

{%- for variant in product.variants -%}
<div class="variant-info">
<h4>{{ variant.title }}</h4>
{%- if variant.metafields.custom.lead_time != blank -%}
<p>Lead time: {{ variant.metafields.custom.lead_time }}</p>
{%- endif -%}
{%- if variant.metafields.custom.country_of_origin != blank -%}
<p>Origin: {{ variant.metafields.custom.country_of_origin }}</p>
{%- endif -%}
</div>
{%- endfor -%}

Page Metafields

Custom page content:

{# On page template #}
{%- assign hero_image = page.metafields.custom.hero_image.value -%}
{%- assign cta_text = page.metafields.custom.cta_text -%}
{%- assign cta_link = page.metafields.custom.cta_link -%}
{%- if hero_image -%}
<section class="page-hero">
{{ hero_image | image_url: width: 1400 | image_tag }}
{%- if cta_text != blank and cta_link != blank -%}
<a href="{{ cta_link }}" class="button">{{ cta_text }}</a>
{%- endif -%}
</section>
{%- endif -%}
<div class="page-content">
{{ page.content }}
</div>

Shop Metafields

Store-wide custom data:

{# Access shop metafields #}
{%- assign store_message = shop.metafields.custom.announcement_message -%}
{%- if store_message != blank -%}
<div class="announcement-bar">
{{ store_message }}
</div>
{%- endif -%}

Metafield Filters

Format Values

{# Date metafield #}
{{ product.metafields.custom.release_date | date: '%B %d, %Y' }}
{# Money metafield (if storing cents) #}
{{ product.metafields.custom.wholesale_price | money }}
{# Rich text (already formatted) #}
{{ product.metafields.custom.rich_description }}
{# Multi-line text #}
{{ product.metafields.custom.notes | newline_to_br }}

Weight and Dimension

{# Weight metafield #}
{%- assign weight = product.metafields.custom.package_weight -%}
{{ weight.value }} {{ weight.unit }}
{# Dimension metafield #}
{%- assign dims = product.metafields.custom.dimensions -%}
{{ dims.value }} {{ dims.unit }}

Section Settings with Metafield Defaults

{
"name": "Product Details",
"settings": [
{
"type": "checkbox",
"id": "show_care",
"label": "Show care instructions",
"default": true,
"info": "Displays custom.care_instructions metafield if available"
},
{
"type": "checkbox",
"id": "show_specs",
"label": "Show specifications",
"default": true,
"info": "Displays custom.specifications metafield if available"
}
]
}

Best Practices

Namespace Conventions

custom. # Default namespace for Admin-created metafields
my_theme. # Theme-specific metafields
my_app. # App-specific metafields

Always Check for Blank

{# Good: check before rendering #}
{%- if product.metafields.custom.subtitle != blank -%}
<p>{{ product.metafields.custom.subtitle }}</p>
{%- endif -%}
{# Bad: may output empty elements #}
<p>{{ product.metafields.custom.subtitle }}</p>

Provide Fallbacks

{%- assign subtitle = product.metafields.custom.subtitle
| default: product.vendor -%}
<p>{{ subtitle }}</p>

Practice Exercise

Create a metafield-powered product page with:

  1. Subtitle metafield (single line text)
  2. Care instructions (multi-line text)
  3. Size guide PDF (file reference)
  4. Complementary product (product reference)
  5. Specifications (JSON)

Test by:

  • Adding metafield values to products
  • Verifying conditional display
  • Checking different metafield types

Key Takeaways

  1. Metafields extend standard content model
  2. resource.metafields.namespace.key access pattern
  3. Type-specific handling for files, references, rich text
  4. Dynamic sources connect metafields in theme editor
  5. Always check blank before rendering
  6. Namespace conventions keep things organized
  7. List metafields for multiple values

What’s Next?

The next lesson covers Metaobjects for complex content modeling beyond metafields.

Finished this lesson?

Mark it complete to track your progress.

Discussion

Loading comments...