Assets: CSS, JS, and Front-End Architecture Beginner 12 min read

Assets Pipeline: How CSS, JS, and Images Load

Understand how Shopify serves your theme's CSS, JavaScript, and images through its CDN, and learn best practices for loading assets efficiently.

Every theme needs CSS for styling, JavaScript for interactivity, and images for visual content. Shopify provides a robust asset pipeline that serves these files through a global CDN, handles cache busting automatically, and gives you filters to load assets efficiently.

The Assets Directory

All static files live in your theme’s assets/ directory:

theme/
├── assets/
│ ├── base.css
│ ├── component-card.css
│ ├── theme.js
│ ├── product.js
│ ├── logo.svg
│ └── placeholder.png

Unlike other directories, assets/ is flat with no subdirectories allowed. Use naming conventions to organize:

assets/
├── base.css
├── component-header.css
├── component-footer.css
├── component-product-card.css
├── section-hero.css
├── section-featured-collection.css
├── theme.js
├── component-cart-drawer.js
└── component-product-form.js

The asset_url Filter

The asset_url filter converts a filename into a full CDN URL:

{{ 'theme.css' | asset_url }}

Output:

https://cdn.shopify.com/s/files/1/0123/4567/8901/t/1/assets/theme.css?v=123456789

Key features:

  • CDN delivery: Assets are served from Shopify’s global CDN
  • Cache busting: The ?v= parameter changes when the file changes
  • HTTPS: All assets are served securely

Loading Stylesheets

Using stylesheet_tag

The simplest way to load CSS:

{{ 'base.css' | asset_url | stylesheet_tag }}

Output:

<link href="//cdn.shopify.com/.../base.css?v=123" rel="stylesheet" type="text/css" media="all" />

For more control, build the tag yourself:

<link rel="stylesheet" href="{{ 'base.css' | asset_url }}" media="all">

Preloading Critical CSS

Preload CSS that’s needed immediately:

<link rel="preload" href="{{ 'base.css' | asset_url }}" as="style">
<link rel="stylesheet" href="{{ 'base.css' | asset_url }}">

Loading CSS Conditionally

Load page-specific CSS only where needed:

{%- case request.page_type -%}
{%- when 'product' -%}
{{ 'section-product.css' | asset_url | stylesheet_tag }}
{%- when 'collection' -%}
{{ 'section-collection.css' | asset_url | stylesheet_tag }}
{%- endcase -%}

Loading JavaScript

Using script_tag

{{ 'theme.js' | asset_url | script_tag }}

Output:

<script src="//cdn.shopify.com/.../theme.js?v=123" type="text/javascript"></script>

Defer and Async

For better performance, load scripts without blocking:

<script src="{{ 'theme.js' | asset_url }}" defer></script>

Or async for independent scripts:

<script src="{{ 'analytics.js' | asset_url }}" async></script>

Defer vs Async:

  • defer: Executes after HTML is parsed, maintains order
  • async: Executes as soon as downloaded, no guaranteed order

Loading Scripts at End of Body

Best practice is to load scripts before </body>:

<body>
{% sections 'header-group' %}
<main>{{ content_for_layout }}</main>
{% sections 'footer-group' %}
{# Scripts at the end #}
<script src="{{ 'vendor.js' | asset_url }}" defer></script>
<script src="{{ 'theme.js' | asset_url }}" defer></script>
</body>

Module Scripts

For modern JavaScript modules:

<script type="module" src="{{ 'theme.js' | asset_url }}"></script>

With a fallback for older browsers:

<script type="module" src="{{ 'theme.js' | asset_url }}"></script>
<script nomodule src="{{ 'theme-legacy.js' | asset_url }}" defer></script>

Loading Images

In Liquid Templates

For theme asset images:

<img src="{{ 'logo.svg' | asset_url }}" alt="{{ shop.name }}">
<img src="{{ 'placeholder.png' | asset_url }}" alt="Placeholder">

Product and Collection Images

Use the image_url filter for dynamic images:

{{ product.featured_image | image_url: width: 600 | image_tag }}

Background Images in CSS

For CSS background images, use inline styles with Liquid:

<div style="background-image: url('{{ section.settings.image | image_url: width: 1920 }}');">

Or in a style block:

<style>
.hero-banner {
background-image: url('{{ section.settings.image | image_url: width: 1920 }}');
}
</style>

Preconnect and Prefetch

Speed up asset loading with resource hints:

<head>
{# Preconnect to Shopify's CDN #}
<link rel="preconnect" href="https://cdn.shopify.com" crossorigin>
{# Preconnect to fonts #}
<link rel="preconnect" href="https://fonts.shopifycdn.com" crossorigin>
{# Prefetch assets for likely next pages #}
<link rel="prefetch" href="{{ 'product.js' | asset_url }}">
</head>

Resource hints:

  • preconnect: Establish connection early (DNS, TCP, TLS)
  • prefetch: Load resources that might be needed soon
  • preload: Load resources needed for current page

Font Loading

Using Theme Settings Fonts

{%- liquid
assign body_font = settings.type_body_font
assign heading_font = settings.type_header_font
-%}
<style>
{{ body_font | font_face: font_display: 'swap' }}
{{ heading_font | font_face: font_display: 'swap' }}
body {
font-family: {{ body_font.family }}, {{ body_font.fallback_families }};
}
h1, h2, h3 {
font-family: {{ heading_font.family }}, {{ heading_font.fallback_families }};
}
</style>

Custom Font Files

For custom fonts in your assets:

<style>
@font-face {
font-family: 'CustomFont';
src: url('{{ "custom-font.woff2" | asset_url }}') format('woff2'),
url('{{ "custom-font.woff" | asset_url }}') format('woff');
font-weight: 400;
font-style: normal;
font-display: swap;
}
</style>

Preload for faster loading:

<link
rel="preload"
href="{{ 'custom-font.woff2' | asset_url }}"
as="font"
type="font/woff2"
crossorigin
>

Inline Styles and Scripts

For critical CSS or small scripts, inline them:

<style>
/* Critical above-the-fold CSS */
.header { ... }
.hero { ... }
</style>
<script>
// Small inline script
document.documentElement.classList.replace('no-js', 'js');
</script>

Using {% style %} Tag

The style tag is useful for Liquid-generated CSS:

{% style %}
.section-{{ section.id }} {
background-color: {{ section.settings.background_color }};
padding: {{ section.settings.padding }}px;
}
{% endstyle %}

Complete Asset Loading Example

Here’s a production-ready theme.liquid head section:

<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ page_title }}</title>
{# Preconnect to CDN and fonts #}
<link rel="preconnect" href="https://cdn.shopify.com" crossorigin>
<link rel="preconnect" href="https://fonts.shopifycdn.com" crossorigin>
{# Preload critical assets #}
<link rel="preload" href="{{ 'base.css' | asset_url }}" as="style">
<link rel="preload" href="{{ settings.type_header_font | font_url }}" as="font" type="font/woff2" crossorigin>
{# Required Shopify content #}
{{ content_for_header }}
{# Theme fonts #}
{% style %}
{{ settings.type_body_font | font_face: font_display: 'swap' }}
{{ settings.type_header_font | font_face: font_display: 'swap' }}
{% endstyle %}
{# CSS custom properties #}
{% render 'css-variables' %}
{# Main stylesheet #}
{{ 'base.css' | asset_url | stylesheet_tag }}
{# Page-specific CSS #}
{%- case request.page_type -%}
{%- when 'product' -%}
{{ 'template-product.css' | asset_url | stylesheet_tag }}
{%- when 'collection' -%}
{{ 'template-collection.css' | asset_url | stylesheet_tag }}
{%- endcase -%}
{# No-JS detection #}
<script>
document.documentElement.className = document.documentElement.className.replace('no-js', 'js');
</script>
</head>
<body>
{# ... page content ... #}
{# Scripts at end of body #}
<script src="{{ 'vendor.js' | asset_url }}" defer></script>
<script src="{{ 'theme.js' | asset_url }}" defer></script>
</body>

Shopify Asset URLs

Shopify provides special asset URLs for built-in resources:

{# Shopify's hosted assets #}
{{ 'option_selection.js' | shopify_asset_url | script_tag }}
{# Payment icons #}
{{ 'visa' | payment_type_svg_tag }}
{# Placeholder images #}
{{ 'product-1' | placeholder_svg_tag }}

File URL for Uploaded Files

For files uploaded via theme settings (like PDFs):

{%- if settings.size_guide_pdf -%}
<a href="{{ settings.size_guide_pdf | file_url }}">Download Size Guide</a>
{%- endif -%}

Practice Exercise

Create the asset loading section for a theme.liquid that:

  1. Preconnects to the CDN
  2. Preloads the main CSS file
  3. Loads fonts with font_face
  4. Loads base CSS
  5. Conditionally loads product CSS on product pages
  6. Defers JavaScript loading
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ page_title }} | {{ shop.name }}</title>
<link rel="preconnect" href="https://cdn.shopify.com" crossorigin>
<link rel="preload" href="{{ 'theme.css' | asset_url }}" as="style">
{{ content_for_header }}
{% style %}
{{ settings.type_body_font | font_face: font_display: 'swap' }}
{{ settings.type_header_font | font_face: font_display: 'swap' }}
{% endstyle %}
{{ 'theme.css' | asset_url | stylesheet_tag }}
{%- if request.page_type == 'product' -%}
{{ 'product.css' | asset_url | stylesheet_tag }}
{%- endif -%}
</head>
<body>
{{ content_for_layout }}
<script src="{{ 'theme.js' | asset_url }}" defer></script>
{%- if request.page_type == 'product' -%}
<script src="{{ 'product.js' | asset_url }}" defer></script>
{%- endif -%}
</body>

Key Takeaways

  1. All assets live in assets/ with no subdirectories
  2. asset_url converts filenames to CDN URLs with cache busting
  3. stylesheet_tag and script_tag generate proper HTML tags
  4. Preconnect and preload speed up critical resource loading
  5. Defer JavaScript to avoid blocking page rendering
  6. Load conditionally to avoid unnecessary downloads
  7. Inline critical CSS for fastest first paint
  8. Use font_display: swap for better font loading UX

What’s Next?

Now that you understand how assets load, the next lesson covers CSS Structure for Maintainability and how to organize your stylesheets effectively.

Finished this lesson?

Mark it complete to track your progress.

Discussion

Loading comments...