theme.liquid Deep Dive
Understand the master layout file that wraps every page in your Shopify theme, including required placeholders, section groups, and best practices.
Every page in your Shopify store passes through theme.liquid. It’s the master layout file that wraps all your content, providing the HTML structure, head elements, and global sections that appear on every page. Understanding this file is essential for building well-structured themes.
What is theme.liquid?
The theme.liquid file lives in your layout/ directory and serves as the outer shell for every page. Think of it as the HTML skeleton that holds everything together:
┌─────────────────────────────────────┐│ theme.liquid ││ ┌───────────────────────────────┐ ││ │ Header Section Group │ ││ └───────────────────────────────┘ ││ ┌───────────────────────────────┐ ││ │ │ ││ │ {{ content_for_layout }} │ ││ │ (Template content goes │ ││ │ here) │ ││ │ │ ││ └───────────────────────────────┘ ││ ┌───────────────────────────────┐ ││ │ Footer Section Group │ ││ └───────────────────────────────┘ │└─────────────────────────────────────┘Basic Structure
Here’s a minimal theme.liquid file:
<!DOCTYPE html><html lang="{{ request.locale.iso_code }}"><head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ page_title }}</title>
{{ content_for_header }}
{{ 'base.css' | asset_url | stylesheet_tag }}</head><body> {{ content_for_layout }}
{{ 'theme.js' | asset_url | script_tag }}</body></html>Required Placeholders
Two placeholders are absolutely required in every layout file:
{{ content_for_header }}
This placeholder outputs essential code that Shopify needs to function:
- Analytics and tracking scripts
- Shopify’s internal JavaScript
- App scripts and stylesheets
- Dynamic checkout buttons
- Payment provider scripts
<head> <!-- Your meta tags and title -->
{{ content_for_header }}
<!-- Your stylesheets --></head>Important: Always place content_for_header in the <head> section. Putting it elsewhere can break checkout, apps, and analytics.
{{ content_for_layout }}
This placeholder outputs the actual page content from your templates:
<body> <header>...</header>
<main id="main-content"> {{ content_for_layout }} </main>
<footer>...</footer></body>For a product page, content_for_layout outputs the rendered product.json template. For a collection page, it outputs collection.json, and so on.
Section Groups
Section groups let merchants add, remove, and reorder sections in specific areas of your layout. The most common are header and footer groups:
<body> {% sections 'header-group' %}
<main id="main-content"> {{ content_for_layout }} </main>
{% sections 'footer-group' %}</body>Section groups are defined in JSON files in the sections/ directory:
{ "type": "header-group", "name": "Header Group", "sections": { "announcement-bar": { "type": "announcement-bar" }, "header": { "type": "header" } }, "order": ["announcement-bar", "header"]}Merchants can then customize these sections and their order in the theme editor.
The Head Section
A well-structured <head> includes several important elements:
<head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
{%- comment -%} Title with fallback {%- endcomment -%} <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>
{%- if page_description -%} <meta name="description" content="{{ page_description | escape }}"> {%- endif -%}
{%- comment -%} Canonical URL {%- endcomment -%} <link rel="canonical" href="{{ canonical_url }}">
{%- comment -%} Favicon {%- endcomment -%} <link rel="icon" type="image/png" href="{{ 'favicon.png' | asset_url }}">
{%- comment -%} Preconnect to Shopify CDN {%- endcomment -%} <link rel="preconnect" href="https://cdn.shopify.com" crossorigin>
{%- comment -%} Required Shopify content {%- endcomment -%} {{ content_for_header }}
{%- comment -%} Theme stylesheets {%- endcomment -%} {{ 'base.css' | asset_url | stylesheet_tag }}
{%- comment -%} Theme settings as CSS variables {%- endcomment -%} {% render 'css-variables' %}</head>Loading Fonts
If you’re using custom fonts, preload them for better performance:
{%- comment -%} Preload critical fonts {%- endcomment -%}<link rel="preload" as="font" href="{{ 'custom-font.woff2' | asset_url }}" type="font/woff2" crossorigin>
{%- comment -%} Or use Shopify's font picker {%- endcomment -%}{% style %} {{ settings.type_body_font | font_face: font_display: 'swap' }} {{ settings.type_header_font | font_face: font_display: 'swap' }}{% endstyle %}CSS Variables from Theme Settings
A common pattern is rendering theme settings as CSS custom properties:
{# snippets/css-variables.liquid #}{% style %}:root { --color-primary: {{ settings.color_primary }}; --color-secondary: {{ settings.color_secondary }}; --color-background: {{ settings.color_background }}; --color-text: {{ settings.color_text }};
--font-body: {{ settings.type_body_font.family }}, {{ settings.type_body_font.fallback_families }}; --font-heading: {{ settings.type_header_font.family }}, {{ settings.type_header_font.fallback_families }};
--spacing-unit: {{ settings.spacing_unit }}px; --border-radius: {{ settings.border_radius }}px;}{% endstyle %}Then in your CSS:
body { font-family: var(--font-body); color: var(--color-text); background: var(--color-background);}
h1,h2,h3 { font-family: var(--font-heading); color: var(--color-primary);}JavaScript Loading
Load JavaScript at the end of the body for better performance:
<body> {% sections 'header-group' %}
<main> {{ content_for_layout }} </main>
{% sections 'footer-group' %}
{%- comment -%} Scripts at end of body {%- endcomment -%} <script src="{{ 'vendor.js' | asset_url }}" defer></script> <script src="{{ 'theme.js' | asset_url }}" defer></script>
{%- comment -%} Pass Liquid data to JavaScript {%- endcomment -%} <script> window.theme = { moneyFormat: {{ shop.money_format | json }}, cartCount: {{ cart.item_count }}, customerId: {{ customer.id | default: 'null' }} }; </script></body>Skip Link for Accessibility
Include a skip link for keyboard users:
<body> <a href="#main-content" class="skip-link"> Skip to content </a>
{% sections 'header-group' %}
<main id="main-content" tabindex="-1"> {{ content_for_layout }} </main>
{% sections 'footer-group' %}</body>With CSS:
.skip-link { position: absolute; top: -100%; left: 0; padding: 1rem; background: var(--color-primary); color: white; z-index: 9999;}
.skip-link:focus { top: 0;}Alternate Layouts
You can create additional layout files for special pages:
layout/├── theme.liquid # Default layout├── password.liquid # Password page layout└── gift_card.liquid # Gift card layoutPassword Layout
For stores with password protection enabled:
{# layout/password.liquid #}<!DOCTYPE html><html lang="{{ request.locale.iso_code }}"><head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>{{ shop.name }}</title> {{ content_for_header }} {{ 'base.css' | asset_url | stylesheet_tag }}</head><body class="password-page"> {{ content_for_layout }}</body></html>Gift Card Layout
Gift cards have special requirements:
{# layout/gift_card.liquid #}<!DOCTYPE html><html lang="{{ request.locale.iso_code }}"><head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>{{ 'gift_cards.issued.title' | t }} | {{ shop.name }}</title> {{ content_for_header }} {{ 'gift-card.css' | asset_url | stylesheet_tag }}</head><body class="gift-card-page"> {{ content_for_layout }}
{%- comment -%} QR code script for gift cards {%- endcomment -%} <script src="{{ 'qrcode.js' | shopify_asset_url }}" defer></script></body></html>Detecting the Current Page
Use the request object to apply page-specific logic in your layout:
<body class="template-{{ request.page_type }} {%- if customer %} customer-logged-in{% endif -%} {%- if request.design_mode %} design-mode{% endif -%}" data-template="{{ template.name }}">Or load page-specific styles:
{%- case request.page_type -%} {%- when 'product' -%} {{ 'product.css' | asset_url | stylesheet_tag }} {%- when 'collection' -%} {{ 'collection.css' | asset_url | stylesheet_tag }}{%- endcase -%}Complete Example
Here’s a production-ready theme.liquid:
<!DOCTYPE html><html lang="{{ request.locale.iso_code }}" class="no-js"><head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<title> {{ page_title }} {%- unless page_title contains shop.name %} | {{ shop.name }}{% endunless -%} </title>
{%- if page_description -%} <meta name="description" content="{{ page_description | escape }}"> {%- endif -%}
<link rel="canonical" href="{{ canonical_url }}">
{%- if settings.favicon -%} <link rel="icon" type="image/png" href="{{ settings.favicon | image_url: width: 32 }}"> {%- endif -%}
<link rel="preconnect" href="https://cdn.shopify.com" crossorigin>
{%- comment -%} Detect JS support {%- endcomment -%} <script>document.documentElement.classList.replace('no-js', 'js');</script>
{{ content_for_header }}
{{ 'base.css' | asset_url | stylesheet_tag }} {% render 'css-variables' %}</head>
<body class="template-{{ request.page_type }}"> <a href="#main-content" class="skip-link visually-hidden-focusable"> {{ 'general.accessibility.skip_to_content' | t }} </a>
{% sections 'header-group' %}
<main id="main-content" role="main" tabindex="-1"> {{ content_for_layout }} </main>
{% sections 'footer-group' %}
<script src="{{ 'theme.js' | asset_url }}" defer></script>
<script> window.shopUrl = {{ request.origin | json }}; window.routes = { cart: '{{ routes.cart_url }}', cartAdd: '{{ routes.cart_add_url }}', cartChange: '{{ routes.cart_change_url }}' }; </script></body></html>Key Takeaways
theme.liquidwraps every page in your storecontent_for_headeris required in<head>for Shopify functionalitycontent_for_layoutoutputs the template content- Section groups enable header/footer customization
- CSS variables from theme settings enable dynamic theming
- Alternate layouts handle special pages like password and gift cards
- Performance matters: preconnect, defer scripts, optimize font loading
What’s Next?
Now that you understand the layout layer, the next lesson covers JSON Templates and how they define which sections appear on each page type.
Finished this lesson?
Mark it complete to track your progress.
Discussion
Loading comments...