Theme Folder Structure
Every Shopify theme follows a strict directory structure. Shopify expects specific folders and filenames โ there's no flexibility here. This chapter maps every directory, explains every file type, and shows you how the Dawn theme organizes its codebase.
Time estimate: 30 โ 45 minutes
Tags:
Liquid
Architecture
CSS
JavaScript
The Complete Directory Tree
Open your Dawn theme project in VS Code and you'll see this structure. Every Shopify theme โ from Dawn to the most complex premium theme โ follows this exact layout:
Shopify only recognizes these specific directories. You cannot
create custom folders like components/ or partials/.
All reusable code goes in snippets/. All static files go in
assets/. This is a hard platform constraint.
The assets/ Directory
The assets/ folder contains all static files: CSS, JavaScript, images,
fonts, and SVGs. This is a flat directory โ no subdirectories are allowed.
Naming Conventions
Since you can't use subdirectories, use naming prefixes to organize your files. Here's the pattern Dawn uses:
| Prefix | Purpose | Examples |
|---|---|---|
base-* |
Foundation styles (reset, typography, variables) | base.css |
component-* |
Reusable UI component styles | component-card.css, component-badge.css |
section-* |
Section-specific styles | section-featured-collection.css |
template-* |
Template-specific styles | template-product.css |
global.* |
Global JavaScript functionality | global.js |
product-* |
Product-related JS modules | product-form.js, product-modal.js |
CSS Organization Strategy
Dawn splits CSS into many small files and loads them conditionally. This is a performance optimization โ pages only load the CSS they need:
{%- comment -%}
Only load this CSS file when this section is present on the page.
This avoids loading unnecessary CSS on pages that don't use this section.
{%- endcomment -%}
{{ 'section-featured-collection.css' | asset_url | stylesheet_tag }}
{{ 'component-card.css' | asset_url | stylesheet_tag }}
<section class="featured-collection">
{%- for product in section.settings.collection.products limit: 4 -%}
{% render 'product-card', product: product %}
{%- endfor -%}
</section>
Split your CSS into logical files rather than one massive stylesheet. Use prefix naming to keep them organized. Load section-specific CSS at the top of each section file. This keeps unused CSS off pages that don't need it and improves load times.
JavaScript Organization
Dawn uses vanilla JavaScript with Web Components and custom elements. JS files follow the same flat structure:
<!-- Deferred loading for non-critical JavaScript -->
<script src="{{ 'product-form.js' | asset_url }}" defer="defer"></script>
<!-- Or conditionally load based on section presence -->
{%- if section.settings.enable_slideshow -%}
<script src="{{ 'slideshow.js' | asset_url }}" defer="defer"></script>
{%- endif -%}
Image and Font Files
Static images (logos, icons, backgrounds) and custom font files also go in
assets/:
<!-- Logo image -->
<img src="{{ 'logo.png' | asset_url }}" alt="{{ shop.name }}">
<!-- SVG icon -->
<img src="{{ 'icon-cart.svg' | asset_url }}" alt="" width="20" height="20">
<!-- Custom font in CSS -->
@font-face {
font-family: 'MyCustomFont';
src: url('{{ "my-font.woff2" | asset_url }}') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
Instead of storing SVG icons as image files, many professional themes store them as
Liquid snippets. This lets you inline SVGs for better performance
and CSS styling control. Create files like snippets/icon-cart.liquid
containing the raw SVG code, then use {% render 'icon-cart' %}.
The config/ Directory
The config/ directory contains exactly two files that control theme-wide
settings:
settings_schema.json
This file defines the UI for global theme settings. It specifies what settings appear in the theme editor under "Theme settings" and how they're organized into groups.
[
{
"name": "theme_info",
"theme_name": "My Custom Theme",
"theme_version": "1.0.0",
"theme_author": "Gamal",
"theme_documentation_url": "https://example.com/docs",
"theme_support_url": "https://example.com/support"
},
{
"name": "Colors",
"settings": [
{
"type": "header",
"content": "Primary Colors"
},
{
"type": "color",
"id": "color_primary",
"label": "Primary brand color",
"default": "#6366f1",
"info": "Used for buttons, links, and accents"
},
{
"type": "color",
"id": "color_text",
"label": "Text color",
"default": "#1a1a2e"
},
{
"type": "color",
"id": "color_background",
"label": "Page background",
"default": "#ffffff"
}
]
},
{
"name": "Typography",
"settings": [
{
"type": "font_picker",
"id": "font_heading",
"label": "Heading font",
"default": "helvetica_n7"
},
{
"type": "font_picker",
"id": "font_body",
"label": "Body font",
"default": "helvetica_n4"
},
{
"type": "range",
"id": "font_body_scale",
"label": "Body font size scale",
"min": 80,
"max": 130,
"step": 5,
"default": 100,
"unit": "%"
}
]
},
{
"name": "Social media",
"settings": [
{
"type": "header",
"content": "Social accounts"
},
{
"type": "text",
"id": "social_twitter_link",
"label": "Twitter",
"info": "https://twitter.com/your-handle"
},
{
"type": "text",
"id": "social_instagram_link",
"label": "Instagram"
},
{
"type": "text",
"id": "social_facebook_link",
"label": "Facebook"
}
]
},
{
"name": "Favicon",
"settings": [
{
"type": "image_picker",
"id": "favicon",
"label": "Favicon image",
"info": "32 x 32px .png recommended"
}
]
}
]
settings_data.json
This file stores the actual values that merchants have set through the theme editor. It's auto-generated and should rarely be edited manually.
{
"current": {
"color_primary": "#6366f1",
"color_text": "#1a1a2e",
"color_background": "#ffffff",
"font_heading": "helvetica_n7",
"font_body": "helvetica_n4",
"font_body_scale": 100,
"social_twitter_link": "https://twitter.com/shopify",
"social_instagram_link": "https://instagram.com/shopify",
"sections": {
"header": {
"type": "header",
"settings": {
"logo_position": "middle-center"
}
}
}
}
}
This file contains the merchant's customizations. If you're working on a client's
store and you push your local settings_data.json, it could overwrite
their settings. Use the --ignore flag when pushing:
shopify theme push --ignore config/settings_data.json
The layout/ Directory
The layout directory typically contains two files:
| File | Purpose | Used By |
|---|---|---|
theme.liquid |
Main layout for all pages. Contains <html>, <head>, header, footer, and {{ content_for_layout }}. |
Every page except password page |
password.liquid |
Layout for the password protection page shown when the store isn't public yet. | Store password page only |
Anatomy of theme.liquid
<!doctype html>
<html class="no-js" lang="{{ request.locale.iso_code }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- Page title with store name -->
<title>
{{ page_title }}
{%- if current_tags %} – tagged "{{ current_tags | join: ', ' }}"{% endif -%}
{%- if current_page != 1 %} – Page {{ current_page }}{% endif -%}
{%- unless page_title contains shop.name %} – {{ shop.name }}{% endunless -%}
</title>
<!-- SEO meta tags -->
{%- if page_description -%}
<meta name="description" content="{{ page_description | escape }}">
{%- endif -%}
<!-- Canonical URL -->
<link rel="canonical" href="{{ canonical_url }}">
<!-- REQUIRED: Shopify analytics, apps, meta tags -->
{{ content_for_header }}
<!-- Theme CSS variables from settings -->
{%- style -%}
:root {
--color-primary: {{ settings.color_primary }};
--color-text: {{ settings.color_text }};
--color-background: {{ settings.color_background }};
--font-heading-family: {{ settings.font_heading.family }},
{{ settings.font_heading.fallback_families }};
--font-body-family: {{ settings.font_body.family }},
{{ settings.font_body.fallback_families }};
}
{%- endstyle -%}
<!-- Base stylesheet -->
{{ 'base.css' | asset_url | stylesheet_tag }}
<!-- Preload critical fonts -->
<link rel="preconnect" href="https://fonts.shopifycdn.com" crossorigin>
<!-- Favicon -->
{%- if settings.favicon -%}
<link rel="icon" type="image/png"
href="{{ settings.favicon | image_url: width: 32, height: 32 }}">
{%- endif -%}
<!-- No-JS detection -->
<script>
document.documentElement.className =
document.documentElement.className.replace('no-js', 'js');
</script>
</head>
<body class="template-{{ template.name }}">
<a class="skip-to-content-link" href="#MainContent">
{{ 'accessibility.skip_to_content' | t }}
</a>
{%- sections 'header-group' -%}
<main id="MainContent" class="content-for-layout" role="main"
tabindex="-1">
{{ content_for_layout }}
</main>
{%- sections 'footer-group' -%}
<!-- Global JavaScript -->
<script src="{{ 'global.js' | asset_url }}" defer="defer"></script>
</body>
</html>
The theme.liquid file is the most important file in any theme. Study
Dawn's version line by line. Pay attention to the SEO meta tags, the skip-to-content
link for accessibility, the CSS variable bridge from settings, and the font preloading.
Every line serves a purpose.
The templates/ Directory
Templates define which sections appear on each page type. In Online Store 2.0, templates are JSON files.
Required Template Types
| Template | Page Type | Main Object Available |
|---|---|---|
index.json |
Homepage | shop |
product.json |
Product detail page | product |
collection.json |
Collection page | collection |
list-collections.json |
All collections page | collections |
page.json |
Custom content page | page |
blog.json |
Blog listing page | blog |
article.json |
Blog article page | article |
cart.json |
Cart page | cart |
search.json |
Search results page | search |
404.json |
404 error page | โ |
password.json |
Store password page | โ |
gift_card.liquid |
Gift card page | gift_card |
Anatomy of a JSON Template
{
"sections": {
"image_banner": {
"type": "image-banner",
"settings": {
"image": "shopify://shop_images/hero.jpg",
"heading": "Welcome to Our Store",
"heading_size": "h1",
"button_label": "Shop Now",
"button_link": "shopify://collections/all"
}
},
"featured_collection": {
"type": "featured-collection",
"settings": {
"heading": "Featured Products",
"collection": "frontpage",
"products_to_show": 8
}
},
"rich_text": {
"type": "rich-text",
"settings": {
"heading": "About Our Brand",
"text": "<p>We create premium products...</p>"
}
}
},
"order": [
"image_banner",
"featured_collection",
"rich_text"
]
}
Understanding the structure:
"sections"โ Object containing all sections. Each key is a unique ID you define."type"โ References a file insections/(without.liquidextension)."settings"โ Pre-set values matching the section's schema settings."order"โ Array defining the display order of sections.
Alternate Templates
Create alternate templates by adding a suffix after a dot:
Merchants assign alternate templates to specific products or pages in the Shopify admin. This is how a store can have different layouts for different product types.
Customer Templates
Customer account pages still use Liquid templates (not JSON) because they have special form requirements:
The sections/ Directory
This is where most of your development work happens. Each file in sections/
is a self-contained module with markup, optional CSS/JS, and a schema.
Section Naming Conventions
| Prefix | Purpose | Examples |
|---|---|---|
main-* |
Primary content section for a page type. Usually renders the core page-specific object. | main-product.liquid, main-collection.liquid, main-cart.liquid |
| (no prefix) | Reusable sections that can be added to any page through the theme editor. | featured-collection.liquid, image-banner.liquid, rich-text.liquid |
*-group.json |
Section groups that appear on every page (header, footer). | header-group.json, footer-group.json |
Section File Anatomy
{%- comment -%}
PART 1: CSS Loading
Load section-specific CSS only when this section is used
{%- endcomment -%}
{{ 'section-example.css' | asset_url | stylesheet_tag }}
{%- comment -%}
PART 2: Liquid Markup (HTML)
The visual output of the section
{%- endcomment -%}
<section
id="section-{{ section.id }}"
class="example-section color-{{ section.settings.color_scheme }}"
>
<div class="page-width">
{%- if section.settings.heading != blank -%}
<h2 class="example-section__heading">
{{ section.settings.heading }}
</h2>
{%- endif -%}
{%- for block in section.blocks -%}
<div class="example-section__block" {{ block.shopify_attributes }}>
{% case block.type %}
{% when 'text' %}
<p>{{ block.settings.text }}</p>
{% when 'image' %}
<img src="{{ block.settings.image | image_url: width: 600 }}"
alt="{{ block.settings.image.alt | escape }}">
{% endcase %}
</div>
{%- endfor -%}
</div>
</section>
{%- comment -%}
PART 3: Schema (JSON)
Defines settings, blocks, and presets for the theme editor
{%- endcomment -%}
{% schema %}
{
"name": "Example Section",
"tag": "section",
"class": "section",
"settings": [
{
"type": "text",
"id": "heading",
"label": "Heading",
"default": "Section Heading"
},
{
"type": "select",
"id": "color_scheme",
"label": "Color scheme",
"options": [
{ "value": "light", "label": "Light" },
{ "value": "dark", "label": "Dark" }
],
"default": "light"
}
],
"blocks": [
{
"type": "text",
"name": "Text Block",
"settings": [
{
"type": "richtext",
"id": "text",
"label": "Text",
"default": "<p>Share your story...</p>"
}
]
},
{
"type": "image",
"name": "Image Block",
"settings": [
{
"type": "image_picker",
"id": "image",
"label": "Image"
}
]
}
],
"presets": [
{
"name": "Example Section",
"blocks": [
{ "type": "text" },
{ "type": "image" }
]
}
]
}
{% endschema %}
The "presets" array in the schema is what makes a section available in
the "Add section" menu of the theme editor. Without presets, a
section can only be used if it's already referenced in a JSON template. Sections
like main-product typically don't have presets because they're always
present on their respective page type.
The snippets/ Directory
Snippets are reusable Liquid fragments. They're included using
{% render 'snippet-name' %}. Snippets have no schema โ they receive
all their data through explicitly passed variables.
Common Snippets in Dawn
| Snippet | Purpose |
|---|---|
product-card.liquid |
Renders a product card (used in collection grids, featured collections, search results) |
price.liquid |
Renders price display with sale/compare-at logic |
icon-*.liquid |
Inline SVG icons (cart, search, menu, close, etc.) |
meta-tags.liquid |
SEO meta tags, Open Graph, Twitter cards |
cart-notification.liquid |
Ajax cart notification popup |
header-search.liquid |
Search form component used in header |
Well-Structured Snippet Pattern
{%- comment -%}
============================================================
Product Card Snippet
============================================================
Renders a product card for grids and carousels.
Accepts:
- product: {Object} Product Liquid object (required)
- show_vendor: {Boolean} Display vendor name (default: false)
- show_rating: {Boolean} Display star rating (default: false)
- image_ratio: {String} Image aspect ratio: 'square', 'portrait',
'landscape', 'adapt' (default: 'adapt')
- lazy_load: {Boolean} Lazy-load the image (default: true)
- card_class: {String} Additional CSS class (default: '')
Usage:
{% render 'product-card',
product: product,
show_vendor: true,
image_ratio: 'square',
lazy_load: forloop.index > 4
%}
Dependencies:
- component-card.css
- snippets/price.liquid
============================================================
{%- endcomment -%}
{%- liquid
assign lazy = lazy_load | default: true
assign ratio = image_ratio | default: 'adapt'
assign show_vendor_name = show_vendor | default: false
-%}
<div class="product-card {{ card_class }}">
<a href="{{ product.url }}" class="product-card__link"
aria-label="{{ product.title | escape }}">
<!-- Image -->
<div class="product-card__media product-card__media--{{ ratio }}">
{%- if product.featured_image -%}
<img
src="{{ product.featured_image | image_url: width: 600 }}"
srcset="
{{ product.featured_image | image_url: width: 300 }} 300w,
{{ product.featured_image | image_url: width: 450 }} 450w,
{{ product.featured_image | image_url: width: 600 }} 600w
"
sizes="(max-width: 749px) calc(50vw - 30px), 300px"
alt="{{ product.featured_image.alt | escape }}"
width="{{ product.featured_image.width }}"
height="{{ product.featured_image.height }}"
{% if lazy %}loading="lazy" decoding="async"{% endif %}
>
{%- else -%}
{{ 'product-1' | placeholder_svg_tag: 'product-card__placeholder' }}
{%- endif -%}
</div>
<!-- Info -->
<div class="product-card__info">
{%- if show_vendor_name and product.vendor != blank -%}
<span class="product-card__vendor">{{ product.vendor }}</span>
{%- endif -%}
<h3 class="product-card__title">{{ product.title | escape }}</h3>
{% render 'price', product: product %}
</div>
</a>
</div>
Always add a comment block at the top of every snippet listing: what it renders, what variables it accepts (with types and defaults), usage examples, and CSS dependencies. This is how professional theme developers work. When you revisit code months later โ or when another developer reads it โ this documentation is invaluable.
The locales/ Directory
The locales/ directory contains translation files. Even if your theme
only supports English, you should use the locales system for all user-facing strings.
{
"general": {
"search": "Search",
"close": "Close",
"loading": "Loading..."
},
"products": {
"product": {
"add_to_cart": "Add to cart",
"sold_out": "Sold out",
"unavailable": "Unavailable",
"price": {
"from": "From {{ price }}",
"sale_price": "Sale price",
"regular_price": "Regular price"
}
}
},
"cart": {
"general": {
"title": "Your cart",
"empty": "Your cart is empty",
"continue_shopping": "Continue shopping"
}
},
"accessibility": {
"skip_to_content": "Skip to content",
"close": "Close"
}
}
Access translations in Liquid using the t filter:
<!-- Simple translation -->
<button>{{ 'products.product.add_to_cart' | t }}</button>
<!-- Output: Add to cart -->
<!-- Translation with variable -->
<span>{{ 'products.product.price.from' | t: price: product.price | money }}</span>
<!-- Output: From $25.00 -->
<!-- Accessibility label -->
<a href="#MainContent">{{ 'accessibility.skip_to_content' | t }}</a>
1. Future-proofing: Adding languages later is trivial โ just add
new JSON files.
2. Merchant control: Merchants can edit text through the theme editor
language settings without touching code.
3. Consistency: All strings are in one place, making it easy to find
and update wording.
4. Theme Store requirement: Shopify requires translations for themes
submitted to the Theme Store.
Dawn File-by-File Walkthrough
Let's walk through the most important files in Dawn to understand how a real theme connects everything:
layout/theme.liquid
Open this file first. Notice the CSS custom properties generated from settings,
the content_for_header placement, and how section groups are
rendered for header and footer.
templates/index.json
See how the homepage is composed from sections. Each section reference in the
"sections" object corresponds to a file in sections/.
sections/image-banner.liquid
Study this section's schema. See how it defines settings for the image, heading, button text, and button link. Notice the blocks for flexible content arrangement.
sections/main-product.liquid
This is the most complex section in Dawn. Study how it uses blocks for the product form layout โ title, price, variant picker, quantity selector, buy button, and description are all individual blocks.
snippets/product-card.liquid
See how this snippet is used in multiple sections (featured collection, search results, product recommendations). Notice the variable parameters it accepts and how it handles missing images.
config/settings_schema.json
Explore how Dawn organizes global settings into logical groups: colors,
typography, social media, favicon, layout. Notice the info
fields that help merchants understand each setting.
Exercise: Explore Your Dawn Theme
Theme Structure Exploration
Objective: Build a mental map of how Dawn's files connect to each other.
Tasks:
- Open
templates/index.json. List all the section types referenced. - For each section type, find the corresponding
.liquidfile insections/. - Open
sections/featured-collection.liquid. Find every{% render %}call and note which snippets it uses. - Open those snippets in
snippets/and trace how data flows from section โ snippet. - Count how many CSS files are in
assets/. How many JS files? - Open
config/settings_schema.json. How many setting groups are there? How many individual settings? - Open
locales/en.default.json. Find the translation key for the "Add to cart" button text.
Learning goal: You should be able to trace any visual element on the storefront back to its source file in the theme.
Key Takeaways
- Shopify themes have a strict folder structure โ no custom directories allowed
assets/is flat โ use naming prefixes to organize filesconfig/has two files: schema (definition) and data (saved values)layout/theme.liquidis the outermost HTML shell for every pagetemplates/are JSON files defining which sections appear on each page type- Alternate templates use dot-suffix naming:
product.special.json sections/contain modular components with markup, CSS, JS, and schemasnippets/contain reusable Liquid fragments โ always document their parameterslocales/hold translations โ use them even for single-language themes- CSS should be split into small, conditionally-loaded files for performance