Skip to content

Assets, CSS, and JavaScript

Every theme has an assets/ folder for the static files a storefront needs: stylesheets, scripts, web fonts, images, PDFs, and SVG icons. Sellerlane serves those files from a versioned CDN and gives you several ways to attach CSS and JS to a page. This page explains each option and when to reach for it.

The assets folder

Anything inside assets/ is referenced by filename only — never by a folder path. The renderer resolves the name through the asset_url filter:

<link rel="stylesheet" href="{{ 'base.css' | asset_url }}">
<script src="{{ 'product.js' | asset_url }}" defer></script>

The allowed file extensions are css, js, svg, json, png, jpg, jpeg, gif, webp, woff, woff2, ttf, otf, eot, and pdf, plus the Liquid variants .css.liquid and .js.liquid. The folder is flat — assets/ holds files, not subfolders.

URL filters

FilterUse it for
asset_urlA file you ship inside assets/ (CSS, JS, SVG, font, image)
asset_img_urlAn image in assets/, resized to a size string (e.g. 'logo.png' | asset_img_url: '200x')
file_urlA file uploaded under Settings → Files (referenced by filename)
file_img_urlA Files image, resized to a size string
global_asset_urlA platform-hosted shared asset
<img src="{{ 'hero.jpg' | asset_img_url: '1200x' }}" alt="">
<img src="{{ 'guide.pdf' | file_url }}" alt="download">

Versioned CDN

asset_url does not return an unversioned /assets/base.css path. It returns a stable asset URL with a standardized ?v= version parameter derived from the published theme version and file content. When you edit and republish the file, its version changes, so the cache key changes too.

That lets the CDN serve every live asset with a far-future, immutable cache header (Cache-Control: public, max-age=31536000, immutable). Shoppers get the new file when the published version changes. Do not append timestamps, random parameters, or a second cache-busting convention; use the URL returned by asset_url unchanged. In the theme editor’s draft mode the same logical files are served no-store so you always preview the active draft.

The S3/R2 bucket itself remains private. Only allowlisted published assets are public through the storefront/media edge route. Theme source, config, locales, draft files, and compiled render artifacts are never public bucket objects. See Shopify theme compatibility.

Adding CSS and JavaScript

There are three ways to attach styles or scripts, and they behave differently. Pick by where the code belongs.

{% stylesheet %} and {% javascript %} live inside a section, block, or snippet file and hold the CSS or JS that file needs. The renderer collects them and emits each one once per rendered source file — even if the section renders many times on a page. Files that are not in the page’s render tree do not contribute CSS, which keeps the page-specific bundle smaller.

{% comment %} sections/featured-product.liquid {% endcomment %}
<div class="featured-product"></div>
{% stylesheet %}
.featured-product { display: grid; gap: 1rem; }
{% endstylesheet %}
{% javascript %}
document.querySelectorAll('.featured-product').forEach(initSlider);
{% endjavascript %}

This is the recommended way to ship CSS and JS that belongs to one section. You do not write your own <style> or <script> tag — the renderer bundles and places them for you, and de-duplicates so a thrice-used section’s CSS ships once. A Section Rendering API fragment prepends its required subset in a <style data-section-stylesheet> block, so swapping in a section that was not on the original page also installs its CSS.

Liquid inside an asset file

Sometimes you want a whole CSS or JS file to be generated from theme settings — not just a snippet inline in a section. Name the file with a .liquid extension before its real one:

  • theme.css.liquid — a stylesheet whose values come from settings.
  • config.js.liquid — a script seeded with Liquid-rendered values.

The renderer runs these through Liquid on request and serves them with the right content type (text/css for .css.liquid, application/javascript for .js.liquid). Inside one, you have access to settings, shop, and the usual global objects:

{% comment %} assets/theme.css.liquid {% endcomment %}
:root {
--brand: {{ settings.brand_color }};
--radius: {{ settings.corner_radius }}px;
}

You still link it with asset_url, and you reference it by its full name — do not drop the .liquid suffix:

<link rel="stylesheet" href="{{ 'theme.css.liquid' | asset_url }}">

SVG icons

To drop an SVG’s markup straight into the page — so you can style it with CSS or animate it — use inline_asset_content with a single-file name from assets/:

<span class="icon">{{ 'icon-cart.svg' | inline_asset_content }}</span>

This reads the file and outputs its contents directly (no <img>, no extra request). It accepts .svg, .js, and .css files that live at the top level of assets/. If the file is missing, it returns an empty string.

Preloading the LCP image

For the one image that dominates a page’s largest contentful paint — usually the hero or first product photo — emit a preload hint so the browser fetches it early. The preload_tag filter builds a <link rel="preload"> and infers the as type from the file extension:

{{ image | image_url: width: 1200 | preload_tag: as: 'image' }}

The renderer collects these hints and flushes them into the document <head>, ahead of the rest of the assets. Preload only the LCP image — preloading everything defeats the purpose.

Performance notes

  1. Keep section CSS in {% stylesheet %}. Because each section’s CSS ships once and only when that section is on the page, pages stay lean — a product page never downloads cart-only styles. Dumping everything into one global base.css removes that per-page gating.

  2. Avoid global style bleed. Scope selectors to the section — for example prefix with the section’s class or section.id — so one section’s CSS can’t restyle another. Global, unscoped selectors in a section file leak to every page that section appears on.

  3. Prefer static .css / .js over .liquid assets unless a value truly comes from a setting. Static files skip the per-request Liquid pass and get the immutable far-future cache.

  4. Defer scripts. Add defer (or async) to linked <script> tags so they don’t block rendering.

  5. Replace sections from the response once. A cart/section response already represents the post-mutation state and includes the fragment’s stylesheet subset. Do not repeat the mutation to fetch updated markup.

Uploading assets in the editor

  1. Open the theme editor’s code view from Online store → Themes → Edit code.

  2. In the Assets folder, choose to add a file. To create an empty file, pick a type — CSS, CSS.liquid, JavaScript, JS.liquid, or SVG — and name it.

  3. To upload an existing file (image, font, or PDF), select it from your computer. Supported uploads include PNG, JPG, GIF, WebP, the web-font formats (WOFF, WOFF2, TTF, OTF, EOT), and PDF.

  4. Save. The file is published with the theme version and gets its own versioned CDN URL.

Next steps