If your app puts anything on a Shopify storefront, such as a review widget, a size chart, a chat bubble or a tracking script, a theme app extension is now the supported way to ship it. Shopify requires one for any App Store app that integrates with themes, and for the Built for Shopify badge. Since October 1, 2026, apps can't create or update script tags, and on March 1, 2027 Shopify stops injecting script tags into storefronts at all. If your app still relies on them, you have until March to move that code into a theme app extension or a web pixel.
This guide is for app developers and the agencies that commission them. It covers how app blocks and app embed blocks differ, when a plain theme section is the better answer, a minimal app block you can deploy today, and how to get merchants to turn it on. It also covers the limits that fail a deploy and what happens when a merchant uninstalls your app.
What a theme app extension is
A theme app extension is a bundle of Liquid blocks, snippets, assets and translations that Shopify serves inside a merchant's theme without your app ever editing theme code. You create it with Shopify CLI, and it ships as part of your app version. The generated folder looks like this:
| Folder or file | What goes in it |
|---|---|
blocks/ | One Liquid file per app block or app embed block. The file name becomes the block's handle. |
snippets/ | Reusable Liquid snippets that your blocks render. |
assets/ | CSS, JavaScript and images, served from Shopify's CDN. |
locales/ | Schema locale files (theme editor labels) and storefront locale files (customer-facing text). Merchants can't edit your storefront strings. |
shopify.extension.toml | The extension name and type = "theme". |
One catch for agencies: admin-created custom apps can't use app extensions at all, so they can't ship theme app extensions or web pixels. If a client's integration runs on a custom app that was created in the Shopify admin, it has to be rebuilt as a CLI or Dev Dashboard app first. Our custom app vs public app guide explains the app types.
App blocks vs app embed blocks
Both live in blocks/. The target value in the {% schema %} tag decides which one you're building.
| App block | App embed block | |
|---|---|---|
target value | section | head, body or compliance_head |
| Where it renders | Inside a section, or as its own section in an Apps wrapper | Injected before </head> or </body> |
| Theme support | Online Store 2.0 themes only (JSON templates, sections that accept @app blocks) | Vintage and Online Store 2.0 themes |
| How merchants turn it on | Add block or Add section, then pick it under Apps in the theme editor | Toggle it on under App embeds in the theme editor |
| State after install | Not added to the theme | Deactivated |
| Dynamic sources | Yes, with autofill resource settings | No, global Liquid scope only |
| Typical use | Reviews, ratings, size charts, product badges inline | Chat bubbles, pop-ups, SEO meta tags, analytics, cookie banners |
Use compliance_head only when you really need it, for example for a cookie consent banner. Shopify includes those embeds first.
For merchants, adding an app block means Online Store > Edit theme, then Add block (inside a section) or Add section (full width), and picking your block under Apps. Activating an embed means clicking the App embeds icon in the theme editor sidebar and switching on the toggle. Merchants with several markets can even set embeds per market.
App block, app embed or custom theme section?
Not every storefront feature needs an app. This is how we decide:
| Situation | Build | Why |
|---|---|---|
| Your app shows content the merchant should position on a page | App block | The merchant places, reorders and styles it in the theme editor. |
| Your app needs to load JavaScript on every page, or show a floating element | App embed block | It works on vintage themes too, and can be limited to the templates that need it. |
| Your app only collects analytics or conversion events | Web pixel | Shopify's replacement for tracking script tags. It needs no theme editor step. |
| One store, one theme, content with no app backend | Custom theme section | No app to install or maintain. Your team owns the code in the theme. |
| The feature must appear on checkout or the Order status page | Checkout or customer account UI extension | Theme app extensions can't render on checkout pages. |
| The merchant's published theme is vintage and your feature is inline | App embed block that places content with JavaScript | Shopify's own fallback for themes without app block support. |
The custom section route is honest advice for single-store work. A section in the merchant's theme is simpler than an app. The trade-off is that editing theme code takes the theme off its automatic upgrade path, and the code stays behind if anyone removes the feature later. For anything installed on more than one store, use an extension.
Theme app extensions vs script tags: the 2026–2027 deadlines
Shopify's changelog of August 24, 2026 set the storefront timeline:
- October 1, 2026:
scriptTagCreateandscriptTagUpdatereturn an error, andPOST/PUTto the RESTScriptTagresource fail, on every API version. Pinning an older version doesn't help. Existing script tags keep running, and you can still read and delete them. - March 1, 2027: Shopify stops injecting script tags into storefront pages.
- Order status page: script tags there already stopped on August 26, 2026. Our Thank you page tracking guide covers what replaced them.
We track these dates, and the other Shopify platform deadlines, on our Shopify deprecation tracker.
From October 1, merchants who install an app that uses script tags and ships neither an app embed block nor a web pixel see a dismissible warning banner that part of the app will stop working.
The safe migration order, from Shopify's storefront script tag guide:
- Build a theme app extension with an app embed block (or a web pixel for pure tracking).
- Send the merchant into the theme editor with a deep link that activates the embed.
- Confirm the embed is active on the published theme.
- Only then delete the script tag with
scriptTagDelete. Running both loads your script twice, which can double-count events or render your UI twice.
Build a minimal app block, step by step
This example adds a small "delivery note" to product pages. It uses the page's product automatically and gives the merchant two settings.
Step 1. From your app's root folder, generate the extension and choose Theme app extension:
shopify app generate extension
# choose "Theme app extension", then name it, for example "storefront-blocks"
Step 2. Replace the sample block with extensions/storefront-blocks/blocks/delivery-note.liquid:
{%- assign p = block.settings.product -%}
<div class="delivery-note" style="--delivery-note-accent: {{ block.settings.accent }}">
{%- if p.available -%}
<p>{{ block.settings.message | escape }}</p>
{%- else -%}
<p>This product is currently out of stock.</p>
{%- endif -%}
</div>
{% schema %}
{
"name": "Delivery note",
"target": "section",
"stylesheet": "delivery-note.css",
"enabled_on": { "templates": ["product"] },
"settings": [
{ "type": "product", "id": "product", "label": "Product", "autofill": true },
{ "type": "text", "id": "message", "label": "Message", "default": "Order by 2 pm for next-day dispatch" },
{ "type": "color", "id": "accent", "label": "Accent color", "default": "#1a7f37" }
]
}
{% endschema %}
Step 3. Add extensions/storefront-blocks/assets/delivery-note.css. Because the schema references it, Shopify adds it to the page once, however many copies of the block the merchant adds.
Step 4. Preview it on your dev store. The CLI walks you through adding the block in the theme editor. The hot-reloading preview works in Google Chrome only, and without --theme the CLI uploads Dawn to the dev store:
# --theme takes a theme ID or name
shopify app dev --theme "Horizon"
Step 5. Check the limits, then create and release a version:
shopify app build
shopify app deploy --version "1.4.0" --message "Add delivery note block"
# create the version without releasing it:
shopify app deploy --no-release
A few details in that schema matter. "autofill": true on the product setting makes the block follow the section's product, so it works on every product page. enabled_on keeps the block off templates where it makes no sense. Keep the name under 25 characters, and don't put your app's name in it, because the theme editor already shows it underneath.
Get merchants to turn it on
Nothing appears after install. App blocks aren't added, and app embeds are off. Onboarding is part of the build, and the App Store requires detailed setup instructions for both. Two tools make it painless.
Deep links. Send the merchant straight into the theme editor with the block ready to preview and save. api_key is your app's client ID, and handle is the block's file name without .liquid:
https://your-store.myshopify.com/admin/themes/current/editor?template=product&addAppBlockId={api_key}/delivery-note&target=mainSection
https://your-store.myshopify.com/admin/themes/current/editor?context=apps&template=product&activateAppId={api_key}/{embed-handle}
App block links can also target newAppsSection, a section group (sectionGroup:header, footer or aside) or a specific section ID. Each link adds one block. Before you link to a section or template, check it exists in the theme with the read_themes scope. Otherwise the merchant sees an error.
Detection. To show "Step 2 of 3: done" in your app, call app.extensions() from your embedded app. It needs no extra scopes and reports on the published theme only:
const extensions = await shopify.app.extensions();
const theme = extensions.filter((ext) => ext.type === "theme_app_extension");
const isActive = (handle) =>
theme.some((ext) =>
ext.activations.some((block) => block.handle === handle && block.status === "active"),
);
// "available" = shipped but not enabled; "unavailable" = hidden by available_if
console.log("Delivery note placed:", isActive("delivery-note"));
From your backend, read the published theme's files with read_themes. App blocks appear in template JSON files with a type like shopify://apps/<app>/blocks/<block>/<id>. App embeds appear in config/settings_data.json once first enabled, and stay there with "disabled": true if switched off, so check that flag.
Also remember that embeds are enabled per theme. When a merchant publishes a new theme, your app isn't active on it until they re-enable it, so re-check activation.
Limits that fail a deploy
Shopify validates the extension on deploy. If you go over an enforced limit, the extension fails validation and doesn't update. shopify app build runs Theme Check so you catch this locally.
| Content | Limit | Type |
|---|---|---|
| All files in the extension | 10 MB | Enforced |
| Number of blocks | 30 | Enforced |
| Locale files | 100 | Enforced |
| Size of each locale file | 15 KB | Enforced |
| Liquid across all files | 100 KB | Enforced |
| CSS referenced by the schema (compressed) | 100 KB | Suggested |
| JavaScript referenced by the schema (compressed) | 10 KB | Suggested |
JSON in your extension can't contain comments or trailing commas, even though theme files allow them. Your blocks also can't read content_for_header, content_for_layout or content_for_index, and they see only the parent section's id.
Performance
Everything in assets/ is served from Shopify's CDN. Files you reference with the javascript and stylesheet schema attributes are included once per page, even if a merchant adds several of your blocks. Shopify suggests keeping that JavaScript under 10 KB compressed. If you want the Built for Shopify badge, your app must not lower the storefront Lighthouse performance score by more than ten points.
In practice, use enabled_on to keep embeds to the templates that need them. Defer anything that isn't needed for first paint. Test in the theme editor too, because Shopify expects your app to work there.
What happens on uninstall
This is the strongest argument for extensions when you pitch them to merchants. When a merchant uninstalls your app, Shopify removes its app blocks and app embeds from their themes automatically and completely. Nobody has to hunt through theme code.
Compare that with the old pattern of writing snippets into the theme through the Asset API. Shopify's own docs describe that pattern leaving "ghost code" behind, which can cause quality and performance problems and takes the theme off its upgrade path. Built for Shopify also says apps shouldn't add, remove or edit a merchant's theme files, apart from a few exceptions such as page builders and backup tools. If you inherit a store full of leftover snippets from old apps, cleaning that out is a separate job.
Versioning and releases
Your theme app extension is versioned together with your app configuration and every other extension as one app version:
shopify app deploycreates a version and releases it. Releasing replaces the active version for every store with your app installed, and the rollout can take several minutes.- You can revert to a previous version at any time, from the CLI or the Dev Dashboard.
- A CLI-created version includes only the extensions that exist in your local checkout. Deploying from a branch that's missing an extension leaves that extension out of the version.
- In CI, pass
--allow-updates(or--no-release) so the deploy doesn't wait for a confirmation prompt.
Common mistakes
- Deleting script tags before the embed is live. The feature goes dark until the merchant activates the embed.
- Assuming app blocks work on every theme. Vintage themes need an app embed fallback. Public listings should say whether an Online Store 2.0 theme is required.
- Cluttering the App embeds panel. Every app's embeds share one panel. Keep settings to visual choices and move the rest into your app.
- Linking to a section that doesn't exist. Check the template, section group or section ID with
read_themesbefore you build the deep link. - Trying to render on checkout. Theme app extensions can't render on checkout pages or the Order status page. Our checkout customization guide covers the right extension types.
- Reading theme files over REST. New public apps must use the GraphQL Admin API, which can read theme files too. See our REST to GraphQL migration guide.
Launch checklist
- The block
targetis correct:sectionfor inline content,head/bodyfor embeds. enabled_onordisabled_onlimits templates (only one of the two).- Resource settings use
autofillwhere the block follows a product or collection. shopify app buildpasses, and the Liquid total is under 100 KB.- Schema JavaScript is under the suggested 10 KB compressed.
- Deep links for each block and embed have been tested on Dawn and Horizon.
- The onboarding screen reports activation from
app.extensions(). - The backend re-checks activation after a theme change.
- Script tags are deleted per store only after the embed is confirmed active.
- Analytics-only scripts have moved to a web pixel.
- The block works on a vintage theme, or the listing says Online Store 2.0 is required.
--no-releasestaging and a rollback plan are documented for the team.
Want help with your theme app extension?
We build and migrate Shopify apps, including theme app extensions, script tag replacements and web pixels, and we test them on real themes before release. If your app still loads code through script tags ahead of the March 1, 2027 cut-off, see our Shopify development services and tell us what your storefront code does today.
Sources
- Shopify: About theme app extensions
- Shopify: Configure theme app extensions
- Shopify: Build theme app extensions
- Shopify: UX for theme app extensions
- Shopify: Migrate to theme app extensions
- Shopify: Apps in the online store
- Shopify: Storefront script tags
- Shopify developer changelog: Script tags are deprecated and will stop running on March 1, 2027
- Shopify: App API (app.extensions)
- Shopify: About app versions
- Shopify: Built for Shopify requirements
- Shopify Help Center: Extend your theme with apps



