Skip to content
Ecom

Metafields Over Apps: Custom Product Features Without Plugin Bloat

Build size charts, ingredient lists, and care instructions with Shopify metafields and a small Liquid snippet. Code included, no app required.

By WitsCode11 min read

The average Shopify store we audit at WitsCode carries between eleven and eighteen installed apps. Roughly a third of them exist to render a single block on a product page. A size chart modal. An ingredient accordion. A care instructions panel with laundry icons. Each one ships its own script, its own CSS, its own admin UI, and its own monthly bill. The merchant added the app because a developer said it would take a week. The developer said a week because nobody had handed them the metafield pattern that does the same job in an afternoon and ships zero JavaScript to the browser.

This piece walks through the three most common product-page features that merchants install apps for and shows how to build each of them natively with metafields, metaobjects, and a short Liquid snippet. Every code block here works against a 2026 Shopify admin and a theme that supports JSON templates. No Online Store 2.0 magic is required beyond what every supported theme already exposes.

Why the Apps Exist in the First Place

Walking through the Shopify App Store, you will find north of forty apps that do nothing but render a size chart on a product page. The good ones cost fifteen dollars a month. The bad ones cost the same but add a hundred and sixty kilobytes of JavaScript, a shadow DOM wrapper, and a cross-origin request that blocks LCP. Multiply that by three apps for size chart, ingredients, and care, and you are looking at a product page that ships an extra four hundred kilobytes of vendor code before the primary image paints.

The apps exist because the merchant needs a place to enter the data and a place to render it. That is a two-part problem, and Shopify solved both parts natively when it shipped the metafield definitions UI and metaobject entries. The merchant enters structured data in Settings and Content. The theme renders it in Liquid. The browser gets HTML. That is the whole loop.

The harder question is why so many tutorials stop at the first rung of that ladder. Search for "shopify metafields tutorial" and you will find a hundred posts showing how to create a single line text metafield and echo it into a product template. That covers a tagline or a warranty period. It does not cover a size chart with sixteen rows and three unit systems, an ingredient list where four of the nineteen items need to render as allergen flags, or a care panel that has to map twelve ISO laundry symbols to inline SVG icons. Those are the real features merchants install apps for, and they all require the second and third rungs of the ladder, which is where this guide starts.

Metafields and Metaobjects in 2026, The Short Version

A metafield is a typed key-value pair attached to a resource such as a product, variant, collection, customer, or order. In the current admin, definitions live under Settings, Custom data, and then the resource you are attaching to. You pick a namespace (default is custom), a key, a type, and optional validations. The definition controls what merchants see in the admin and what types Liquid receives at render time. In Liquid, the access pattern is always {{ product.metafields.custom.your_key }} or, for structured types, {{ product.metafields.custom.your_key.value }} to reach into the underlying object.

A metaobject is a reusable structured record. Think of it as a tiny content model. You define it once with its own fields, create entries under Content, Metaobjects, and then reference those entries from a metafield on a product. Where a metafield is owned by the resource it sits on, a metaobject is owned by the shop and can be referenced from anywhere. This distinction is the single most important one in this entire article, because it is the one the surface tutorials skip, and it is the reason most merchants end up with fifty duplicate size charts scattered across their catalog.

The three types you will actually reach for here are metaobject_reference, json, and list.single_line_text_field. Everything else is either simpler (you already know how to render a string) or more niche (file references, dimensions, measurements). Master those three and you have replaced the top three product-page apps on the App Store.

Pattern One: The Size Chart As A Metaobject Reference

Most apparel stores have between three and eight distinct size charts. Tees use one chart. Hoodies share another. Women's bottoms have their own. If you store the chart on each product with a plain JSON or rich text metafield, you are on the hook to update three hundred products when the tee fit changes next season. You will not do it. You will update ten and forget the rest, and a customer will buy the wrong size and return it.

The veteran pattern is to define a size_chart metaobject with a fixed shape, create one entry per unique chart, and then reference that entry from every product that uses it. Update the entry once, and every product pulls the new data at render time. The metaobject definition looks like this under Settings, Custom data, Metaobjects.

size_chart
  name               single_line_text_field     (e.g. "Mens Standard Tee")
  units              single_line_text_field     (e.g. "in,cm")
  columns            list.single_line_text_field  (e.g. ["Size","Chest","Length","Sleeve"])
  rows               json                         (array of arrays)
  fit_note           multi_line_text_field

Each entry under Content, Metaobjects, gets filled in once. The rows JSON looks like this for a four-column chart.

[
  ["S",  [36, 91], [27, 68], [8,  20]],
  ["M",  [38, 96], [28, 71], [8,  20]],
  ["L",  [40,101], [29, 73], [9,  22]],
  ["XL", [42,106], [30, 76], [9,  22]]
]

Then on the product, define a metafield custom.size_chart of type metaobject_reference pointing at the size_chart definition. In the admin, the merchant picks from a dropdown of chart entries. No freeform fields, no chance of typos, no duplication.

The Liquid snippet that renders it lives in snippets/size-chart.liquid and reads from the referenced entry.

{%- liquid
  assign chart = product.metafields.custom.size_chart.value
  if chart == blank
    break
  endif
  assign rows = chart.rows.value
-%}
<section class="size-chart" aria-label="{{ chart.name }}">
  <h3>{{ chart.name }}</h3>
  <table>
    <thead>
      <tr>
        {%- for col in chart.columns.value -%}
          <th>{{ col }}</th>
        {%- endfor -%}
      </tr>
    </thead>
    <tbody>
      {%- for row in rows -%}
        <tr>
          {%- for cell in row -%}
            <td>
              {%- if cell.first -%}
                {{ cell[0] }} in / {{ cell[1] }} cm
              {%- else -%}
                {{ cell }}
              {%- endif -%}
            </td>
          {%- endfor -%}
        </tr>
      {%- endfor -%}
    </tbody>
  </table>
  {%- if chart.fit_note != blank -%}
    <p class="fit-note">{{ chart.fit_note }}</p>
  {%- endif -%}
</section>

The snippet handles both pure strings (the size name) and value pairs (the measurement in inches and centimeters) in one pass. When the merchant needs to add an XXL row to every tee, they edit one metaobject entry. Every product that references it updates on the next cache cycle. No app, no support ticket, no bulk editor hack.

Pattern Two: Ingredient Lists With A JSON Schema

Ingredient apps cost between nine and twenty-nine dollars a month. Every one of them stores, at the core, an array of structured objects. You can store the same array yourself in a JSON metafield and render it with more control than any app will give you, because the app has to generalize for a thousand merchants and you only have to generalize for yours.

Define a custom.ingredients metafield of type json. Write a short schema in your team documentation so the merchants and developers agree on what goes in. Here is the schema WitsCode ships as a starting point.

{
  "type": "array",
  "items": {
    "type": "object",
    "properties": {
      "name":       { "type": "string" },
      "percentage": { "type": "number" },
      "role":       { "type": "string" },
      "allergen":   { "type": "boolean" },
      "source":     { "type": "string" }
    },
    "required": ["name"]
  }
}

A populated metafield on a skincare product looks like this.

[
  { "name": "Aqua",            "percentage": 62.0, "role": "solvent" },
  { "name": "Niacinamide",     "percentage": 10.0, "role": "active" },
  { "name": "Glycerin",        "percentage":  8.0, "role": "humectant" },
  { "name": "Almond Oil",      "percentage":  4.0, "role": "emollient", "allergen": true, "source": "tree nut" },
  { "name": "Phenoxyethanol",  "percentage":  0.8, "role": "preservative" }
]

The render snippet takes advantage of that structure to do three things the app will not: sort by percentage if you want an INCI-ordered list, flag allergens with an accessible badge, and show the role as secondary text for customers who want to understand the formulation.

{%- liquid
  assign ingredients = product.metafields.custom.ingredients.value
  if ingredients == blank
    break
  endif
-%}
<section class="ingredients" aria-label="Ingredients">
  <ul class="ingredients-list">
    {%- for row in ingredients -%}
      <li class="ingredient {% if row.allergen %}is-allergen{% endif %}">
        <span class="ingredient-name">{{ row.name }}</span>
        {%- if row.percentage -%}
          <span class="ingredient-pct">{{ row.percentage }}%</span>
        {%- endif -%}
        {%- if row.role -%}
          <span class="ingredient-role">{{ row.role }}</span>
        {%- endif -%}
        {%- if row.allergen -%}
          <span class="ingredient-allergen" role="note" aria-label="Allergen: {{ row.source }}">
            Contains {{ row.source }}
          </span>
        {%- endif -%}
      </li>
    {%- endfor -%}
  </ul>
</section>

There are two subtle wins in this snippet that the apps almost never deliver. The first is the accessible allergen note with a real aria-label naming the source, which matters for screen readers and for regulatory labeling in the EU. The second is that the HTML renders at parse time, so the ingredient list is fully indexable by Google and visible to customers before any JavaScript runs. An app that renders the same list inside a shadow DOM after hydration is invisible to both.

Pattern Three: Care Instructions With Icon Keys

The care instructions panel is the feature where most merchants give up and install an app, because the app comes with a library of laundry symbols. You do not need the library. You need a list of keys and an SVG sprite. Ship a sprite file with your theme under assets/care-icons.svg containing symbols with IDs like wash_30, no_bleach, tumble_dry_low, iron_medium, dry_clean_only, no_wring, and the twenty or so others the garment industry actually uses.

Define a custom.care metafield of type list.single_line_text_field with a validation that restricts values to the allowed icon keys. The validation means merchants pick from a dropdown in the admin, not a freeform field, which kills typos at the source. Then define a shop-level metaobject called care_label that maps each key to a human-readable label.

care_label
  key    single_line_text_field   (e.g. "wash_30")
  label  single_line_text_field   (e.g. "Machine wash at 30°C")

One entry per supported icon, created once at the start of the project. Every product then references by key, and the snippet pulls both the icon and the label in a single pass.

{%- liquid
  assign codes = product.metafields.custom.care.value
  if codes == blank
    break
  endif
  assign labels = shop.metaobjects.care_label.values
-%}
<section class="care" aria-label="Care instructions">
  <ul class="care-list">
    {%- for code in codes -%}
      {%- assign match = labels | where: "key", code | first -%}
      <li class="care-item">
        <svg class="care-icon" aria-hidden="true" width="24" height="24">
          <use href="{{ 'care-icons.svg' | asset_url }}#{{ code }}"></use>
        </svg>
        <span class="care-label">
          {%- if match -%}{{ match.label }}{%- else -%}{{ code }}{%- endif -%}
        </span>
      </li>
    {%- endfor -%}
  </ul>
</section>

The shop.metaobjects.care_label.values call pulls every care label entry once per request, and the where filter matches by key. This is a cleaner pattern than storing the label string on every product because it centralizes translation and copy changes. When marketing decides "Machine wash cold" reads better than "Machine wash at 30°C", you edit one metaobject entry and every product updates. The SVG sprite is cached at the CDN, the keys are validated at entry, and the entire feature ships without a single line of JavaScript.

Wiring The Snippets Into The Product Template

All three patterns plug into a JSON product template the same way. In templates/product.json, add a section that renders the snippet, or include it inline in your existing product section.

{% render 'size-chart',   product: product %}
{% render 'ingredients',  product: product %}
{% render 'care',         product: product %}

If you prefer merchant-facing blocks that can be reordered in the theme editor, wrap each snippet in a {% schema %} block within a section file. That gives the merchant drag-and-drop control over the order of the panels without touching code. The snippets themselves do not change.

Two deployment notes worth calling out. First, metafield definitions and metaobject definitions should be created before the theme is deployed, or the product.metafields.custom.* lookups will return blank and your conditional guards will silently hide the sections. Add a short QA checklist that confirms the definition exists, the product has a value, and the snippet renders. Second, if you run Shopify Markets with translated storefronts, metaobject fields support translation through the Translate and Adapt app or the API. The Liquid snippet does not change. The merchant translates the metaobject entries, not the theme.

When An App Is Still The Right Call

This is not a blanket "never install an app" piece. There are features where the app is doing work the metafield layer cannot, and it is worth paying for. Live inventory feeds from a 3PL, tax calculation for a dozen jurisdictions, fraud scoring, loyalty accrual with its own ledger, and anything that needs background jobs or webhook-driven state all justify an app. Those involve data that changes outside the product admin, business logic that does not belong in Liquid, or compliance surfaces that need a vendor on the hook.

What does not justify an app is rendering structured content the merchant types into the admin. Size charts, ingredients, care instructions, spec tables, assembly steps, fit guides, dietary information, warranty tiers, and compatibility matrices are all content. They belong in metafields and metaobjects. If the app you are evaluating is fundamentally a form plus a render, and the data never leaves the Shopify admin, you do not need the app. You need an afternoon with the Liquid snippets above.

The WitsCode Close

Across the two hundred and fifty Shopify stores WitsCode maintains, the single highest-impact change we make in the first thirty days is usually the same one: replace three to five content-rendering apps with native metafield and metaobject patterns. The measurable result is a product page that drops between two hundred and four hundred kilobytes of third-party JavaScript, an LCP improvement between eight hundred milliseconds and one and a half seconds on 4G, and a monthly app bill that falls by sixty to a hundred and twenty dollars per store.

The less measurable but louder result is merchants who stop being afraid of their product page. When the size chart, ingredient list, and care panel are three Liquid snippets reading from typed metafields, editing one of them does not involve a vendor support ticket, a theme reinstall, or a prayer. It involves opening the admin, clicking into a metaobject entry, and typing. That shift, from vendor-dependent to merchant-owned, is what we think custom-feature engagement on Shopify should feel like in 2026. If your store is carrying three apps that exist to render structured content, the fastest win in your roadmap is sitting in the snippets above. WitsCode builds these natively by default, and so should you.

Get weekly field notes.

Practical writing on shipping products, straight to your inbox. No spam.

Need help with this?

Shopify Development

We design and build web apps, MVPs, and SaaS products. Talk to us about what you are working on.

Talk to us

Want to discuss ecom for your business?

Start a project and we'll talk through where you are, what's working, and the highest-leverage moves for the next 90 days.