Jillur Rahman

Jillur Rahman

Front-End Developer

Back to Blog
Development11 min read

Advanced Shopify Theme Customization: What a Developer Does That the Editor Can't

The Shopify theme editor is designed for marketers, not developers. When you need your store to look and behave in ways the editor doesn't support — custom animations, bespoke layouts, complex interactive components — a developer opens a different set of tools entirely.

ShopifyTheme DevelopmentDawnJavaScriptCSSCustom Development
Cover image for: Advanced Shopify Theme Customization: What a Developer Does That the Editor Can't

The Shopify theme editor is an excellent tool for the use case it was designed for: letting non-technical users adjust layouts, swap images, change colors, and add or remove sections. It does that job well.

But it's a tool built for marketers, not developers. When a store needs something beyond what the editor can express — a navigation that behaves differently than the theme's default, a product page layout that doesn't fit any of the standard patterns, a custom animation on scroll, an interactive element that requires precise DOM control — the editor is the starting point, not the ending point.

This is where a developer works in the code directly. Here's what that looks like and why it matters.


The Architecture of a Modern Shopify Theme (Dawn)

Shopify's reference theme, Dawn, is the foundation most modern themes build on. Understanding its architecture is prerequisite to modifying it correctly.

Dawn uses:

Web Components for interactive UI — The cart drawer, product media galleries, variant selectors, and predictive search are all implemented as custom HTML elements (<cart-drawer>, <product-form>, <variant-selects>). Each has its own JavaScript class extending HTMLElement.

CSS custom properties (variables) for design tokens — Instead of hardcoded color values, Dawn uses variables like --color-base-background-1, --color-base-accent-1. This means theme color changes propagate correctly and custom CSS can tap into the same token system.

Section-based architecture — Every page is built from sections. The JSON template files (templates/product.json, templates/collection.json) define which sections appear and in what order. A developer can modify these directly, add new sections, or create entirely custom layouts.

Deferred asset loading — Dawn loads JavaScript modules on demand. Critical CSS is inlined; non-critical CSS is loaded asynchronously. Custom code should follow the same pattern to avoid undoing the performance architecture.


The Right Way to Extend Dawn

A common mistake: editing Dawn's core files directly. When Shopify releases theme updates, your changes get overwritten. The correct patterns:

Custom sections — Add new functionality in new files in sections/. Never edit sections/main-product.liquid to add features that could be a separate section.

CSS overrides via a custom stylesheet — Create assets/custom.css and include it at the bottom of layout/theme.liquid. This keeps your changes separate from the theme's base styles and makes upgrades safer.

JavaScript extensions that don't patch core — Rather than modifying Dawn's Web Component classes directly, extend them:

// Don't: Modify VariantSelects class in assets/variant-selects.js
// Do: Extend it in your custom JS file
 
class EnhancedVariantSelects extends VariantSelects {
  constructor() {
    super();
    // Your additional initialization
  }
 
  // Override the method you need to change
  updateOptions() {
    super.updateOptions(); // Call original behavior
 
    // Add your custom behavior after
    this.updateCustomDisplay();
  }
 
  updateCustomDisplay() {
    const selectedVariant = this.currentVariant;
    if (!selectedVariant) return;
 
    // Custom behavior: update a custom "stock indicator" element
    const stockEl = this.closest("[data-product-form]")?.querySelector(
      "[data-stock-indicator]",
    );
    if (!stockEl) return;
 
    const inventory = selectedVariant.inventory_quantity;
    stockEl.dataset.level =
      inventory > 10 ? "high" : inventory > 0 ? "low" : "out";
    stockEl.textContent =
      inventory > 10
        ? "In Stock"
        : inventory > 0
          ? `Only ${inventory} left`
          : "Sold Out";
  }
}
 
// Replace the built-in element registration
customElements.define("variant-selects", EnhancedVariantSelects);

This approach: you get the original behavior for free, your code runs on top of it, and if Dawn updates VariantSelects with bug fixes, you benefit automatically.


Custom CSS Architecture That Scales

Most store customizations start with someone editing Dawn's base.css file. This becomes a maintenance problem at scale. A better architecture:

assets/
  base.css              ← Dawn's base (don't edit)
  component-*.css       ← Dawn's components (don't edit)
  custom-base.css       ← Your design system overrides
  custom-components.css ← Your custom component styles
  custom-utilities.css  ← Utility classes for your team

Your custom-base.css only overrides the CSS variables and base styles you need to change:

/* custom-base.css */
 
:root {
  /* Override Dawn's color tokens with your brand colors */
  --color-base-background-1: #f9f6f2;
  --color-base-background-2: #ffffff;
  --color-base-accent-1: #c4773b;
  --color-base-accent-2: #8b4513;
  --color-base-text: #1a1a1a;
 
  /* Your custom tokens */
  --font-heading: "Playfair Display", Georgia, serif;
  --font-body: "Inter", system-ui, sans-serif;
  --border-radius-card: 4px;
  --transition-standard: 200ms ease-out;
}
 
/* Override body font */
body {
  font-family: var(--font-body);
}
 
h1,
h2,
h3,
h4 {
  font-family: var(--font-heading);
}

Because Dawn's components are built on top of these variables, changing the variables updates the entire theme consistently — header, buttons, cards, footer — without touching any component CSS directly.


Building a Custom Navigation Beyond What the Editor Allows

The most common request for advanced customization is navigation. Every theme has navigation settings, but they can't accommodate: mega menus, fly-out panels with product images, navigation that highlights the current collection, or behavior-on-scroll effects.

A developer builds this as a custom section with its own template:

{% comment %} sections/custom-nav.liquid {% endcomment %}
<nav class="custom-nav" data-scroll-behavior="shrink">
  <div class="nav-inner">
    <a href="/" class="nav-logo">
      {% if section.settings.logo %}
        <img
          src="{{ section.settings.logo | image_url: width: 200 }}"
          alt="{{ shop.name }}"
          width="120"
          loading="eager"
        >
      {% else %}
        <span class="nav-logo-text">{{ shop.name }}</span>
      {% endif %}
    </a>
 
    <ul class="nav-primary" role="list">
      {% for link in linklists[section.settings.menu].links %}
        <li class="nav-item {% if link.child_links.size > 0 %}has-dropdown{% endif %}">
          <a
            href="{{ link.url }}"
            {% if link.url == request.path %}aria-current="page"{% endif %}
          >
            {{ link.title }}
          </a>
 
          {% if link.child_links.size > 0 %}
            <div class="mega-menu" aria-hidden="true">
              <div class="mega-menu-inner">
                {% comment %} Featured collection from metafield — set per nav item via metafield {% endcomment %}
                {% assign featured_coll = link.object.metafields.custom.featured_collection.value %}
                {% if featured_coll %}
                  <div class="mega-menu-featured">
                    <p class="mega-menu-label">Featured</p>
                    {% for product in featured_coll.products limit: 3 %}
                      <a href="{{ product.url }}" class="mega-menu-product">
                        <img
                          src="{{ product.featured_image | image_url: width: 200 }}"
                          alt="{{ product.featured_image.alt | escape }}"
                          width="100"
                          loading="lazy"
                        >
                        <span>{{ product.title }}</span>
                      </a>
                    {% endfor %}
                  </div>
                {% endif %}
 
                <ul class="mega-menu-links" role="list">
                  {% for child in link.child_links %}
                    <li>
                      <a href="{{ child.url }}">{{ child.title }}</a>
                    </li>
                  {% endfor %}
                </ul>
              </div>
            </div>
          {% endif %}
        </li>
      {% endfor %}
    </ul>
  </div>
</nav>

The JavaScript handles the interaction model — keyboard navigation, ARIA state, hover delays to prevent accidental activation — in about 120 lines of vanilla JS. No library needed.

The scroll behavior (shrink header on scroll, hide on scroll down, show on scroll up) uses IntersectionObserver and CSS transitions:

// Header shrink on scroll — no scroll event listener (uses IntersectionObserver)
const sentinel = document.createElement("div");
sentinel.style.cssText =
  "position:absolute;top:0;height:80px;pointer-events:none";
document.body.prepend(sentinel);
 
new IntersectionObserver(([entry]) => {
  document
    .querySelector(".custom-nav")
    .classList.toggle("is-scrolled", !entry.isIntersecting);
}).observe(sentinel);

Using IntersectionObserver instead of a scroll event listener means zero performance overhead on scroll — no main thread work while the user is scrolling.


Custom Product Page Layouts Beyond Section Templates

Dawn's product page is a section group — you can reorder blocks, but you can't change the fundamental layout grid. For a store that needs a product page with a specific structure (full-bleed imagery, tabbed specifications panel, sticky media, custom social proof placement), a developer creates a custom product template:

templates/
  product.json          ← Default Dawn product template
  product.custom.json   ← Custom product template

product.custom.json references a custom section:

{
  "sections": {
    "main": {
      "type": "custom-product-page",
      "blocks": {}
    }
  },
  "order": ["main"]
}

Products are then assigned to this template in Shopify Admin. Different product types can have completely different page layouts — a technical product with a specs-heavy layout, a lifestyle product with an editorial layout — without any app or workaround.


JavaScript Architecture for Complex Themes

Large themes with many custom interactive components need JavaScript architecture that doesn't create conflicts or performance problems.

The pattern that scales: each interactive component is an ES module that registers itself as a Web Component.

// assets/custom-product-tabs.js
 
class ProductTabs extends HTMLElement {
  connectedCallback() {
    this.tabs = this.querySelectorAll('[role="tab"]');
    this.panels = this.querySelectorAll('[role="tabpanel"]');
 
    this.tabs.forEach((tab) => {
      tab.addEventListener("click", () => this.selectTab(tab));
      tab.addEventListener("keydown", (e) => this.handleKeydown(e, tab));
    });
  }
 
  selectTab(selectedTab) {
    this.tabs.forEach((tab) => {
      const isSelected = tab === selectedTab;
      tab.setAttribute("aria-selected", isSelected);
      tab.setAttribute("tabindex", isSelected ? "0" : "-1");
    });
 
    this.panels.forEach((panel) => {
      const isActive = panel.id === selectedTab.getAttribute("aria-controls");
      panel.hidden = !isActive;
    });
  }
 
  handleKeydown(e, currentTab) {
    const keys = { ArrowLeft: -1, ArrowRight: 1, Home: "first", End: "last" };
    const direction = keys[e.key];
    if (!direction) return;
 
    e.preventDefault();
    const idx = Array.from(this.tabs).indexOf(currentTab);
    const target =
      direction === "first"
        ? 0
        : direction === "last"
          ? this.tabs.length - 1
          : idx + direction;
 
    this.tabs[Math.max(0, Math.min(target, this.tabs.length - 1))]?.focus();
  }
}
 
customElements.define("product-tabs", ProductTabs);

Web Components are:

  • Automatically scoped to their DOM element — no ID conflicts, no global variable pollution
  • Self-initializing — no document.ready needed, no querySelectorAll calls from a global script
  • Compatible with Shopify's dynamic section rendering — when a section is updated in the editor, the Web Component reconnects automatically
  • Inherently accessible when built with ARIA roles

A theme with 8–10 custom interactive components, each as its own Web Component module, is significantly easier to maintain than the equivalent built with jQuery or as functions in a single large JS file.


What "Custom Theme Work" Actually Looks Like as a Project

When a store owner needs their theme to do something the editor can't, the project typically has three phases:

Discovery — Understand exactly what the desired behavior is. Look at the current theme code to understand what already exists. Identify what needs to be built versus what can be configured. Establish what performance and accessibility constraints apply.

Development — Build in a theme duplicate (never in the live theme), test across devices and browsers, verify no regressions in existing functionality.

Deployment — Publish the theme, monitor for issues, document what was changed so future work doesn't accidentally revert it.

The most expensive customizations are ones done without documentation, directly in live themes, by multiple developers without a clear architecture. Those themes accumulate debt and get harder to modify over time.

Good theme customization work leaves the code cleaner and more maintainable than it started — with a clear separation between the base theme and custom code, and a record of what was changed and why.

If your store is in a place where the theme editor has hit its limits, or where previous customizations have created a tangle that's hard to build on — that's a good moment to bring in a developer to audit what exists and build forward cleanly.

Tags:ShopifyTheme DevelopmentDawnJavaScriptCSSCustom Development
Jillur Rahman — author

Jillur Rahman

Open to work

Front-End Developer & Shopify Theme Specialist — building fast, conversion-focused web experiences for agencies and brands worldwide.

All articles