Liquid Variables Don't Work Inside {% javascript %} Tags — Here's Why and the Fix
You write {% javascript %}const productId = {{ product.id }};{% endjavascript %} inside a Shopify section. The JavaScript runs but productId is undefined. Or worse, the entire script block errors. Liquid variables are not processed inside {% javascript %} tags — this is intentional. The {% javascript %} tag is for bundleable JavaScript that gets processed and cached independently of any Liquid context. This guide explains why, and gives you the correct patterns to bridge Liquid data into JavaScript.
Why Liquid does not render inside {% javascript %}
Shopify's {% javascript %} tag was introduced for section JavaScript bundling — Shopify concatenates all {% javascript %} blocks from all sections on a page into a single cached JavaScript bundle. This bundle is served from Shopify's CDN and cached aggressively. If Liquid variables were processed inside {% javascript %}, every unique combination of Liquid values would produce a different bundle — making caching impossible. The tag deliberately treats its content as static JavaScript text.
The correct pattern: data attributes on the section element
Pass Liquid values to JavaScript via data attributes on the section's root element or a specific element within it: <div id="product-{{ section.id }}" data-product-id="{{ product.id }}" data-product-handle="{{ product.handle }}" data-price="{{ product.selected_or_first_available_variant.price }}" data-section-id="{{ section.id }}">. In JavaScript, read these attributes: const container = document.querySelector('#product-{{ section.id }}'); const productId = container.dataset.productId;. This pattern works with JavaScript bundling and keeps Liquid data and JavaScript logic cleanly separated.
The schema settings approach for section configuration data
For section settings that need to be available in JavaScript, expose them via data attributes on the section wrapper: <div data-autoplay="{{ section.settings.autoplay }}" data-autoplay-speed="{{ section.settings.autoplay_speed }}" data-section-id="{{ section.id }}">. This allows merchant-configured settings to flow into JavaScript without breaking bundling. The JavaScript reads: const speed = parseInt(container.dataset.autoplaySpeed, 10);.
When to use inline script tags instead of {% javascript %}
If you need Liquid variables in JavaScript and the code cannot use data attributes (for example, a script block that initializes a third-party library with Liquid-provided configuration), use a regular <script> tag — not {% javascript %}. Regular inline script tags do process Liquid: <script>window.ShopConfig = { money_format: {{ shop.money_format | json }}, currency: {{ cart.currency.iso_code | json }} };</script>. Keep these minimal and use {% javascript %} for the bulk of component logic.
The fix: before and after
// CODE_COMPARISON
Frequently asked questions
- Why does my Liquid code inside {% javascript %} output blank instead of an error?
Shopify processes the {% javascript %} tag content as raw JavaScript text without running it through the Liquid engine. Liquid expressions like {{ product.id }} are passed through as literal text and become part of the JavaScript string. When JavaScript tries to evaluate {{ product.id }} as a variable name, it fails silently or throws a reference error depending on context — which is why you see undefined rather than a Liquid rendering error.
- Can I use the | json filter inside data attributes to safely pass complex objects?
Yes. The | json filter is the correct way to pass complex Liquid objects to JavaScript via data attributes or inline scripts. Example: data-product='{{ product | json | escape }}'. In JavaScript: const product = JSON.parse(this.dataset.product). The escape filter ensures HTML special characters in the JSON do not break the attribute. For simple scalar values (strings, numbers, booleans), direct attribute assignment without | json is fine.
- Does the {% javascript %} tag support ES module syntax and modern JavaScript?
Yes. The {% javascript %} tag supports full modern JavaScript including class syntax, async/await, template literals, destructuring, and import-like patterns via module bundling. Shopify's bundler processes the content as ES2015+ JavaScript. However, do not use actual import/export statements inside {% javascript %} — Shopify has its own module system. Use class-based Web Components and global registration instead.
// SCAN_YOUR_CODE
Does your theme have this bug?
Paste your code. Syphio automatically detects and fixes this error and hundreds of others — in seconds.
Validate my Liquid →