Custom Liquid and JavaScript in a Shopify Section, Safely

How to use the Custom Liquid section, add a script tag, and run JavaScript in a Shopify section without breaking the theme, leaking scope, or slowing the page.

5 min readUpdated

Part of our guide to Shopify Custom Sections: The Complete Guide.

Sometimes a section needs a bit of logic the theme editor can't express: a countdown, a small interactive widget, a third-party embed. Shopify gives you two ways in — the built-in Custom Liquid section for quick one-offs, and a {% javascript %} block or <script> tag in a section file for anything reusable. Both are useful; both can break the page if you skip a few precautions. This guide covers doing it safely: where to put the code, how to scope it, and what not to do.

If you only need static markup with no scripting, the custom HTML section route is simpler and this guide is more than you need.

The Custom Liquid section (no code file)

Most Online Store 2.0 themes ship a Custom Liquid section. Add it from the theme editor — Add section → Custom Liquid — and you get a single text box that renders whatever Liquid and HTML you paste in. It's the fastest way to drop in a snippet without touching theme files.

It has real limits, and they're worth knowing before you commit to it:

  • No schema, no settings. Everything is hard-coded in the box; a merchant can't configure it from the editor. For a configurable block you want a real section file — see how to create a custom section.
  • {% schema %}, {% javascript %}, and {% stylesheet %} tags are not allowed inside the Custom Liquid box. You can still write a plain inline <script> and <style>, but the section-specific asset tags won't work there.
  • Settings live in the template. The pasted code is stored in that template's JSON, where it is quick to lose track of. Keep a copy of anything non-trivial outside the editor.

For a genuinely quick embed it's fine. For anything you'll reuse or hand to a merchant to configure, write a section file instead.

A <script> tag in a section file

When your logic belongs to a specific section, keep it in that section's .liquid file. Shopify gives you a dedicated {% javascript %} block: its contents are bundled into a single theme JavaScript file and run once per page even if the section appears multiple times.

<section class="countdown" data-deadline="{{ section.settings.deadline }}">
  <span class="countdown__value"></span>
</section>

{% javascript %}
  document.querySelectorAll('.countdown').forEach((el) => {
    const deadline = new Date(el.dataset.deadline).getTime();
    const output = el.querySelector('.countdown__value');
    setInterval(() => {
      const diff = deadline - Date.now();
      output.textContent = diff > 0
        ? Math.floor(diff / 86400000) + ' days left'
        : 'Ended';
    }, 1000);
  });
{% endjavascript %}

Note what makes this safe:

  • Liquid does not run inside {% javascript %}. The block is static, so pass merchant values through a data- attribute in the markup (as data-deadline above) and read them with el.dataset — never try to interpolate {{ section.settings.x }} into the script.
  • Select by the section's own class, and query within each matched element (el.querySelector), so the script only ever touches its own markup.
  • Guard for the theme editor. A section can be added, removed, and re-rendered live while a merchant edits. Listen for shopify:section:load so your code re-initializes when the section is re-inserted:
{% javascript %}
  function initCountdown(scope) {
    scope.querySelectorAll('.countdown').forEach((el) => { /* ... */ });
  }
  initCountdown(document);
  document.addEventListener('shopify:section:load', (e) => initCountdown(e.target));
{% endjavascript %}

If you must load an external library, add a real <script src> in the markup (outside the {% javascript %} block) with defer, and load it once — not on every copy of the section.

Loading a third-party script tag

For an external embed — a reviews widget, a chat script — put the <script> in the markup and gate it on a setting so an empty field never injects a broken tag:

{% if section.settings.widget_src != blank %}
  <script src="{{ section.settings.widget_src }}" defer></script>
{% endif %}

Two cautions with third-party scripts:

  • Performance. Every external script blocks or delays rendering to some degree. Use defer (or async where the vendor allows it), and don't stack several on one page.
  • Trust. A <script src> you don't control runs with full access to the page, including checkout-adjacent data on some pages. Only load sources you trust, and prefer the vendor's official Shopify app where one exists.

Reading store data instead of hard-coding it

A lot of "I need custom Liquid" cases are really "I need to show some data." Before scripting, check whether Liquid can read it directly. Product, collection, cart, and shop objects are all available, and metafields let you attach and display structured data without any JavaScript:

{% if product.metafields.custom.care_guide %}
  <div class="care-guide">{{ product.metafields.custom.care_guide }}</div>
{% endif %}

That keeps the content with the product and in sync everywhere the section renders — no script, no external call. Define the metafield under Settings → Custom data first. The section schema and blocks guide covers how to expose those as editable fields.

A safety checklist

  • Scope every selector and DOM query to the section's own class.
  • Pass merchant values through data- attributes, never into a static {% javascript %} block.
  • Handle shopify:section:load so the code survives theme-editor re-renders.
  • Gate third-party <script> tags on a non-blank setting; defer them; load once.
  • Prefer Liquid and metafields over JavaScript whenever the data is already on the page.

Keeping this code through a theme update

Custom Liquid pasted into the editor and {% javascript %} written into a section file both live in your theme. When you update or switch themes, Shopify publishes a fresh copy of the theme's files and that code is not carried over — pasted Custom Liquid is especially prone to being lost because it's buried in a template's JSON. The disciplined way to handle it is in update your theme without losing customizations; the structural way is to render the section and its script from an app instead.

For interactive sections you rely on, SectionGuard renders the section — markup, styles, and script — from the app rather than your theme files, so it survives every theme update and swap while keeping its in-editor settings. See how the two approaches compare on pricing.