Skip to content

JavaScript and styles

Where assets live

Stylesheets and scripts belong in the theme's assets directory and are referenced through the asset_url filter, which serves them from the CDN with a cache-busting version.

bash
└── theme
    ├── assets
   ├── main.css
   ├── _global.js
   ├── section.slideshow.css
   ├── block.product.css
   ...
    ...

The directory is flat, so mirror your sections/ and snippets/ names. A file called section.slideshow.css is obviously the stylesheet for sections/slideshow.liquid, and block.product.css for snippets/block.product.liquid.

Loading assets

Pair asset_url with the tag filter that matches how the asset should load:

FilterOutput
stylesheet_tagA <link rel="stylesheet">.
script_tagA blocking <script>.
script_tag_deferredA <script defer>.
script_moduleA <script type="module">.
script_module_deferredA deferred <script type="module">.
preload_tagA <link rel="preload">.
liquid
{{ 'main.css' | asset_url | stylesheet_tag }}
{{ '_global.js' | asset_url | script_tag_deferred }}

Default to script_tag_deferred. A blocking script_tag stops HTML parsing, and almost nothing in a theme needs to run before the document is built.

Keep asset loading close to what needs it

Load a section's stylesheet from the section file rather than the layout, so pages that don't use the section don't pay for it:

liquid
{{ 'section.slideshow.css' | asset_url | stylesheet_tag }}

<div class="slideshow">
  ...
</div>

Reserve the layout for what every page genuinely needs: the base stylesheet, fonts, and the shared scripts.

Inline styles and scripts

For the handful of values that depend on settings, the {% style %} tag keeps CSS in the section file. Use the section's wrapper as the scope, and pass settings through custom properties instead of writing whole rules in Liquid:

liquid
{% style %}
  #youcan-section--{{ section.id }} {
    --bg-color: {{ section.settings.bg_color.hex }};
    --top-spacing: {{ section.settings.top_spacing }}px;
  }
{% endstyle %}

Every section is rendered inside a wrapper element with an id of youcan-section-- plus the section's id, and the class youcan-section, so that selector always resolves, and two copies of the same section on one page stay independent.

The {% javascript %} tag does the same for scripts. Keep what goes in it small: wiring up an element, passing a setting to a component. Anything reusable belongs in assets where it can be cached.

Scope your CSS

Section stylesheets in assets are global once loaded. Scope them so a section can't restyle the rest of the page:

css
.youcan-section .slideshow__item { ... }

Prefer a single class per component over deep descendant chains, and drive variations with data attributes rather than generating separate rules per setting value:

liquid
<div class="slideshow" data-alignment="{{ section.settings.alignment }}">
css
.slideshow[data-alignment='center'] { text-align: center; }

Write less JavaScript

Native browser features cover most of what themes reach for: <details> for accordions, <dialog> for modals, CSS scroll snap for carousels, form validation attributes for inputs. Each one you use is code you don't ship, debug, or block rendering with.

When you do need JavaScript:

  • Enhance server-rendered markup instead of building it on the client.
  • Scope queries to the section, using its wrapper id, rather than document.querySelector across the page.
  • Load heavy behaviour on interaction with a dynamic import() rather than at page load.
  • Guard against the element not being there. Sections come and go while a seller is customizing.
js
const root = document.getElementById('youcan-section--{{ section.id }}');
if (root) {
  root.querySelector('[data-open]')?.addEventListener('click', open);
}

Third-party code

Every external stylesheet or script adds a DNS lookup, a connection, and a dependency you don't control. Self-host what you can by putting it in assets. For what you can't, load it deferred and warm the connection:

liquid
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>