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
| Filter | Use it for |
|---|---|
asset_url | A file you ship inside assets/ (CSS, JS, SVG, font, image) |
asset_img_url | An image in assets/, resized to a size string (e.g. 'logo.png' | asset_img_url: '200x') |
file_url | A file uploaded under Settings → Files (referenced by filename) |
file_img_url | A Files image, resized to a size string |
global_asset_url | A 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.
{% style %} emits an inline <style> tag right where it sits, and it
does evaluate Liquid inside the body. Use it when the CSS depends on a
setting value — a colour, a number, a per-section value:
{% style %} .banner-{{ section.id }} { background: {{ section.settings.bg_color }}; padding-block: {{ section.settings.spacing }}px; }{% endstyle %}Because the CSS is scoped to section.id, each instance of the section styles
only itself. The output is a <style data-shopify> block placed inline.
For shared CSS or JS used across the whole theme, ship a file in assets/ and
link it from your layout:
{{ 'base.css' | asset_url | stylesheet_tag }}<script src="{{ 'global.js' | asset_url }}" defer></script>stylesheet_tag produces a <link rel="stylesheet"> for you; pass media: or
preload: true as needed.
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
-
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 globalbase.cssremoves that per-page gating. -
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. -
Prefer static
.css/.jsover.liquidassets unless a value truly comes from a setting. Static files skip the per-request Liquid pass and get the immutable far-future cache. -
Defer scripts. Add
defer(orasync) to linked<script>tags so they don’t block rendering. -
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
-
Open the theme editor’s code view from Online store → Themes → Edit code.
-
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.
-
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.
-
Save. The file is published with the theme version and gets its own versioned CDN URL.
Next steps
- Liquid filters — the full
asset_url,image_url,inline_asset_content, andpreload_tagreference. - Liquid tags —
{% style %},{% stylesheet %}, and{% javascript %}in detail. - Theme architecture — where
assets/fits among layouts, sections, and snippets. - Edit theme code — working in the code editor.
- Shopify theme compatibility — exact publish, cache, storage, and Section Rendering API policy.