Ready to maximize your revenue?Book a Demo
TOPCODE
Theme App Extensions

Creating your first theme app extension

Last updated on June 2, 2026

Creating your first theme app extension

TOPCODE

Theme app extensions (TAEs) are the modern mechanism for apps to add UI into a Shopify storefront. Before TAEs arrived, apps injected JavaScript via Script Tags — a fragile, uncontrolled approach that polluted global scope and caused store slowdowns. TAEs instead let your app contribute a named Liquid block or section that merchants can enable and position using the theme editor, with no direct file access to the merchant's theme required. This tutorial walks through building a "social proof" app block that shows a purchase counter on product pages.

What you'll build

A theme app extension named social-proof that provides a single app block showing a purchase count badge (e.g. "47 people bought this this week"). Merchants can add this block to any OS 2.0 product page section that supports app blocks. The block will have a configurable label prefix and a toggle to show/hide the count. The count itself will be loaded asynchronously from a fictional app API endpoint.

Before you start

You need: Shopify CLI 3.x, a Partner account, an app registered in the Partner Dashboard, Node.js 18+, and a development store with an OS 2.0 theme installed. This tutorial assumes the app already has a backend (any stack) and focuses exclusively on the theme extension layer. The app must be installed on the development store before you can test the extension.

1

Generate the theme app extension

From inside your app's root directory, run shopify app generate extension. When prompted to choose an extension type, select "Theme app extension". Give it the name social-proof. Shopify CLI scaffolds a folder at extensions/social-proof/ with the following structure.

The scaffold creates extensions/social-proof/blocks/ for app blocks (sections that appear in the theme editor), extensions/social-proof/snippets/ for reusable Liquid partials, extensions/social-proof/assets/ for JS and CSS, and an extension.toml manifest at the root of the extension folder. Note there is no sections/ folder — the term "section" in theme extensions means something different. App blocks live in blocks/ and render inside a theme section's app block zone.

shopify app generate extension
# Choose: Theme app extension
# Name: social-proof

# Resulting structure:
# extensions/
# └── social-proof/
#     ├── assets/
#     │   ├── social-proof.css
#     │   └── social-proof.js
#     ├── blocks/
#     │   └── purchase-count.liquid
#     ├── snippets/
#     └── extension.toml

Expected outcome: The extensions/social-proof/ directory exists with the scaffolded folder structure.

2

Define the block schema in purchase-count.liquid

Open extensions/social-proof/blocks/purchase-count.liquid. Every app block file is a Liquid file with a {% schema %} tag at the bottom, just like a theme section. The schema defines the block's display name, its configurable settings, and its target — the template type(s) where the block can be used.

For our purchase count block, we define two settings: a text setting for the label prefix (defaulting to "people bought this in the last") and a range setting for the time window in days (1–30, default 7). The target of section means the block can be placed into any section that declares "type": "@app" in its blocks array — exactly what Dawn's main-product section does.

The Liquid portion of the block renders the container with a data-product-id attribute and a loading placeholder. The JavaScript asset will replace the placeholder with the fetched count. This approach (SSR container + client-side data fetch) avoids blocking the storefront render while still providing the purchase count data.

extensions/social-proof/blocks/purchase-count.liquidliquid
<div
  class="social-proof-block"
  data-product-id="{{ product.id }}"
  data-days="{{ block.settings.days }}"
>
  <span class="social-proof-block__count" aria-live="polite">
    <span class="social-proof-block__loading" aria-hidden="true">…</span>
  </span>
  <span class="social-proof-block__label">{{ block.settings.label_prefix }} {{ block.settings.days }} days</span>
</div>

{{ 'social-proof.css' | asset_url | stylesheet_tag }}
{{ 'social-proof.js' | asset_url | script_tag }}

{% schema %}
{
  "name": "Purchase count",
  "target": "section",
  "settings": [
    {
      "type": "text",
      "id": "label_prefix",
      "label": "Label prefix",
      "default": "people bought this in the last"
    },
    {
      "type": "range",
      "id": "days",
      "label": "Time window (days)",
      "min": 1,
      "max": 30,
      "step": 1,
      "default": 7
    }
  ]
}
{% endschema %}

Expected outcome: The block file is valid Liquid with a schema that appears correctly in the theme editor when the block is added to a product page.

3

Write the JavaScript asset to fetch and display the count

Create extensions/social-proof/assets/social-proof.js. The script queries the DOM for all [data-product-id] containers, extracts the product ID and days window, fetches the count from a proxied app endpoint (Shopify's App Proxy is the correct mechanism here — it gives your endpoint a *.myshopify.com/apps/social-proof URL), and updates the DOM.

Never call an external API directly from a storefront script. Always route through the App Proxy so that Shopify handles authentication verification and the request originates from the shop domain. Customers and browser security policies will block or flag direct third-party API calls.

extensions/social-proof/assets/social-proof.jsts
document.querySelectorAll('.social-proof-block').forEach(async (el) => {
  const productId = el.dataset.productId;
  const days      = el.dataset.days || 7;

  if (!productId) return;

  try {
    // App Proxy endpoint — configure in Partner Dashboard
    const res = await fetch(
      `/apps/social-proof/count?product_id=${productId}&days=${days}`
    );
    if (!res.ok) throw new Error('fetch failed');
    const { count } = await res.json();

    const countEl = el.querySelector('.social-proof-block__count');
    if (countEl) {
      countEl.innerHTML = `<strong>${count.toLocaleString()}</strong>`;
    }
  } catch {
    // Fail silently — social proof is non-critical UI
    el.style.display = 'none';
  }
});

Expected outcome: On a product page, the block shows a loading indicator briefly, then replaces it with the fetched count number. If the fetch fails, the block hides itself without breaking the page.

4

Enable app blocks in the theme section

Your app block will only appear in the theme editor if the target section's schema includes { "type": "@app" } in its blocks array. Dawn's main-product section already includes @app blocks support. For a custom section, you need to add it manually.

You also need to render the app blocks in the section Liquid template. Within your section's block loop, add a condition for the @app block type using {% render block %} — a special tag that hands rendering control to the app extension block file. The block.type for app blocks is @app at runtime.

sections/main-product.liquid (block loop)liquid
{%- for block in section.blocks -%}
  {%- case block.type -%}
    {%- when 'title' -%}
      <h1>{{ product.title }}</h1>
    {%- when 'price' -%}
      {% render 'price', product: product %}
    {%- when '@app' -%}
      {%- comment -%} Renders the app block registered by the installed app {%- endcomment -%}
      {% render block %}
  {%- endcase -%}
{%- endfor -%}

{%- comment -%} Schema must include @app in blocks array: {%- endcomment -%}
{% schema %}
{
  "name": "Product information",
  "blocks": [
    { "type": "title",       "name": "Title" },
    { "type": "price",       "name": "Price" },
    { "type": "@app" }
  ]
}
{% endschema %}

Expected outcome: After installing the app on the dev store, the theme editor shows a "Purchase count" block available to add to the product section.

5

Preview and deploy the extension

Run shopify app dev from the app root. This starts a local development tunnel and pushes the extension to your development store in a draft state. Navigate to the theme editor on the dev store, open a product page template, and you'll see the Purchase Count block listed under "Apps" in the block picker.

When you're ready to deploy, run shopify app deploy. This creates a new app version in the Partner Dashboard. You can then choose to release it immediately or schedule it. Once released, stores that have your app installed will receive the updated extension — no theme file changes required on their end.

# Start local dev + push extension draft to dev store
shopify app dev

# Deploy to create a releasable app version
shopify app deploy

# Then in Partner Dashboard: App versions > Release

Expected outcome: The extension is live on all stores with your app installed. Merchants can enable the Purchase Count block from the theme editor without receiving a code update to their theme.

What's next

From here, consider adding Liquid conditional logic to the block — for example, only rendering if the product has been purchased more than a threshold number of times (to avoid embarrassing single-digit counts on new products). You can also add multiple blocks to one extension (a review snippet, a stock counter, a recently viewed indicator) by creating additional files in the blocks/ directory. Each file becomes a separately selectable block in the theme editor.

Senior Shopify Engineer

Frances Chen

Keep reading