Guides Guide

PrestaShop Child Themes: Classic & Hummingbird Customization Guide

Build PrestaShop child themes for Classic and Hummingbird: template overrides, SCSS customization, JavaScript hooks, and PS 9 changes.

Why we run a child theme on every shop we touch

mypresta.rocks itself runs on mypresta-rocks — a child theme of Hummingbird that we maintain ourselves. Every client shop we've migrated since 2014 ends up the same way: a child theme of whatever the merchant bought, with our overrides on top. The reason is simple. Theme authors push updates. Merchants buy "premium" themes and then someone edits the parent .tpl files directly. Six months later the theme updates and every tweak is gone.

A child theme is the only sane place to put your customisations. PrestaShop's template loader looks in the child first, falls through to the parent if it doesn't find the file. You only ship the files you've actually changed. Everything else inherits.

The half-day we spend setting up a child theme has paid for itself, with interest, on every shop we've upgraded since 2018. The half-day we don't spend ends with a client email titled "the site looks wrong again."

Use a child theme for targeted changes — colours, layout tweaks, template overrides, custom CSS/JS, module template overrides. Build a full custom theme only when the design is so different from anything off the shelf that inheritance would cost you more than it saves.

Parent themePS versionFrameworkPick it when
Classic1.7.x / 8.xBootstrap 4, jQueryYou're on PS 8 or below and need the broadest module compatibility
Hummingbird9.1+Bootstrap 5.3, TypeScriptYou're on PS 9, or you're migrating and want to land where the platform is going

For anything new on PS 9.1+, pick Hummingbird. It's the only theme PrestaShop is actively developing. Classic still works, but every new core feature lands on Hummingbird first.

Use theme.yml to declare the inheritance

A child theme starts with config/theme.yml, not with a full copy of the parent folder. The active mypresta-rocks theme declares parent: hummingbird, display metadata, and PrestaShop 9 compatibility in one place, so PrestaShop can load the parent theme and apply only the files you intentionally override.

Classic projects use the same principle with parent: classic. Keep this configuration small, versioned, and tested on a staging shop before enabling it on production; the PrestaShop staging guide is the right companion for that rollout.

PrestaShop child theme theme.yml declaring Hummingbird as parent

Keep the child theme folder focused

The child theme should contain only files you own: its configuration, preview image, selected template overrides, selected module template overrides, and assets you compile or register. A focused child theme is easier to audit than a full parent copy because every file has a clear reason to exist.

The structure shown here comes from the live site theme. Overrides sit under templates/ and modules/, while unchanged files remain in Hummingbird. Use the development tools guide to keep asset builds and deployment steps repeatable.

Focused PrestaShop child theme folder structure

Override Smarty blocks instead of copying whole templates

Product page customization can extend the parent template and replace one named block, such as product_header or product_tabs. That keeps upstream Hummingbird changes available while the child theme owns the specific layout decision.

Full template copies still work, but they increase maintenance cost. Prefer {extends file='parent:...'} and precise {block} overrides, then test customized product, category, checkout, and module surfaces after each theme or PrestaShop update. The hooks and overrides guide covers the same ownership rule at module level.

Smarty block override in a PrestaShop child theme product template

Open the real Theme & Logo settings page

After the files are ready, package the child theme as an importer-ready ZIP. The required file is config/theme.yml; the mypresta.rocks generator creates a ZIP with that file at the archive root, and the PrestaShop 9 importer registers it correctly.

In PrestaShop 9, go to Back Office > Design > Theme & Logo. This is the proper settings page for theme upload, activation, logos, page layouts, and advanced theme customization.

  1. Open Design > Theme & Logo.
  2. Click Add new theme in the page header.
  3. Keep the existing parent theme installed; the child theme inherits from it.
PrestaShop 9 Theme and Logo settings page with Add new theme button

Import the child theme ZIP from your computer

On the Theme import page, use Import from your computer and select the ZIP file containing the child theme files. PrestaShop reads the theme metadata from config/theme.yml and registers the theme after the upload succeeds.

  1. Select the child theme ZIP in the Zip file field.
  2. Click Save to upload it.
  3. Return to Theme & Logo, choose Use this theme, clear cache, and verify the storefront.

For production stores, upload and test the theme on staging first, then follow the same activation steps during a quiet maintenance window.

PrestaShop 9 Theme import page showing Import from your computer ZIP upload

Generate a Hummingbird child theme ZIP online

The Child Theme Generator is a public tool on mypresta.rocks, so there is no generator module to install on your own shop. Open the generator page, choose Hummingbird (PrestaShop 9), enter the child theme name, set the PrestaShop version to 9.x, and include the starter files you want in the ZIP.

For the PS9 proof here, the generator downloaded hummingbirdchild.zip with config/theme.yml, custom.css, custom.js, optional Smarty overrides, preview image, README, and optional SCSS build files.

Child Theme Generator form configured for a PrestaShop 9 Hummingbird child theme

Verify the generated child theme after import

After uploading the generated ZIP through Theme import, return to Design > Theme & Logo. The generated theme should appear next to the parent themes, with its display name, version, author, and preview image.

The PS9 demo import shown here created Hummingbird Child version 1.0.0 by mypresta.rocks. At this point you can activate it with Use this theme, clear cache, and review the storefront before using it on production.

PrestaShop 9 Theme and Logo page showing generated Hummingbird Child theme installed

What a child theme actually looks like on disk

A child theme lives next to its parent inside /themes/. The smallest viable one is two files:

themes/my-child-theme/
  config/
    theme.yml             <-- required: parent + name + version
  preview.png             <-- required: 200x200 preview shown in BO

As soon as you start customising, the directory mirrors the parent's structure — but only contains the files you've changed:

themes/my-child-theme/
  config/theme.yml
  assets/
    css/custom.css
    js/custom.js
  templates/
    catalog/product.tpl
    _partials/header.tpl
  modules/
    ps_featuredproducts/views/templates/hook/
      ps_featuredproducts.tpl
  translations/en-US/Shop/Theme.xlf
  preview.png
The discipline that matters: only add a file when you're actually changing it. Every file you copy across is a file you now own and have to maintain. An empty child theme that inherits everything is healthier than one stuffed with unchanged copies of parent templates.

theme.yml — the only file that's truly mandatory

The critical line is parent: classic or parent: hummingbird. That's what wires up inheritance. Get it wrong and PrestaShop treats your child as a standalone theme with no parent to fall back on, and half the site renders blank.

parent: classic
name: my-child-theme
display_name: My Child Theme
version: 1.0.0
author:
  name: "Your Name"
  email: "you@example.com"
  url: "https://yoursite.com"

meta:
  compatibility:
    from: 1.7.0.0
    to: ~

assets:
  use_parent_assets: true

theme_settings:
  default_layout: layout-full-width
  layouts:
    category: layout-left-column

Creating a child theme for Classic

Classic is what most shops on PS 1.7 and 8.x are running. Four steps and you're live:

# 1. Create the directory
mkdir -p themes/my-classic-child/config

# 2. Write config/theme.yml with parent: classic (see above)

# 3. Drop in preview.png (200x200) — any PNG will do at this point

# 4. Activate via Back Office > Design > Theme & Logo
#    Or CLI: php bin/console prestashop:theme:enable my-classic-child

With use_parent_assets: true and no overrides at all, your site renders identically to Classic. That's the safe starting point — confirm the empty child theme works, then add overrides one at a time.

Creating a child theme for Hummingbird

Hummingbird is a different beast. Bootstrap 5.3 with CSS custom properties, CSS Layers (@layer) for cascade control, TypeScript compiled through Webpack 5, and BEM-style naming (.product__name, .product__description-short). Setup is the same as Classic — write theme.yml with parent: hummingbird and a compatibility range starting at 9.0.0. The interesting part is how you customise assets.

The CSS Layer system, and why you stop fighting specificity

Hummingbird declares its layer order in SCSS:

@layer vendors, bs-base, bs-components, bs-custom-components,
       ps-base, ps-components, ps-pages, ps-modules, utilities;

Styles in later layers win over earlier ones, regardless of selector specificity. This is the part that catches people coming from Classic. You don't need !important any more. You don't need a longer selector. Drop your rules in the right layer and they win.

@layer ps-components {
  .product__name {
    font-family: 'Your Custom Font', sans-serif;
  }
}

@layer ps-pages {
  .page--category .product-miniature {
    border: 1px solid #eee;
    border-radius: 8px;
  }
}

SCSS compilation

If you want SCSS rather than plain CSS, add a build step. package.json with sass as a dev dependency, scripts for build:css (sass src/scss/custom.scss assets/css/custom.css --style=compressed) and watch:css. Then npm install && npm run build:css.

Dart Sass occasionally prepends a UTF-8 BOM to compiled CSS. Browsers then silently ignore the first rule, and you spend half an hour wondering why your reset isn't applying. Strip it: sed -i '1s/^\xEF\xBB\xBF//' assets/css/custom.css. We've added this to every Sass build pipeline we run.

Template overrides

This is where child themes earn their keep. PrestaShop's Smarty implementation supports a clean inheritance model, and it's how every one of our shops applies layout tweaks.

How PrestaShop picks a template

  1. Child themethemes/my-child/templates/
  2. Parent themethemes/hummingbird/templates/

The lazy way: copy the whole file

Copy the parent template into the child, then edit. Your version replaces the parent's outright:

cp themes/hummingbird/templates/catalog/product.tpl \
   themes/my-child/templates/catalog/product.tpl

The price you pay: you now own the entire file. When PrestaShop ships a fix to that template in the next minor (and they do) your copy doesn't get it. We use this approach for templates we've genuinely rewritten end to end, and almost never otherwise.

The right way: {extends} and override only the blocks you care about

{extends file='parent:catalog/listing/category.tpl'}

{block name='product_list_header'}
  <div class="custom-category-header">
    <h1>{$category.name}</h1>
    <p>{$listing.pagination.total_items} products</p>
  </div>
{/block}

The parent: prefix matters. Drop it and Smarty resolves the file back to your own child template, which extends itself, infinite loop, PHP timeout. We've seen this on every Smarty project that scales — write a snippet, forget the prefix, watch the request die.

Append and prepend instead of replace

You don't always want to replace a block — sometimes you want to add to it. Smarty has modifiers for that:

{extends file='parent:catalog/product.tpl'}

{block name='product_description' append}
  <div class="shipping-notice">Free shipping on orders over $50</div>
{/block}

{block name='product_prices' prepend}
  <span class="price-badge">Best Price</span>
{/block}

The blocks Hummingbird's product page exposes

The override points on Hummingbird's catalog/product.tpl:

{block name='product_cover_thumbnails'}   — product images
{block name='product_header'}              — product name (h1)
{block name='product_manufacturer'}        — brand link
{block name='product_prices'}              — price display
{block name='product_description_short'}   — short description
{block name='product_variants'}            — combination selectors
{block name='product_add_to_cart'}         — add-to-cart button
{block name='product_tabs'}                — description/details accordion
{block name='product_accessories'}         — related products

For any other page, the trick is to read the parent template. Every {block name='...'} in the parent is an override point you can target from the child.

CSS customisation

The custom.css convention — and why Classic and Hummingbird don't honour it

A few popular commercial themes (Warehouse, Flavor, and several marketplace themes) auto-load custom.css and custom.js if those files exist in the theme's assets/ directory. The theme's assets.yml registers them with high priority, they load last, and they never get touched by theme updates because they're not part of the theme's distribution files. It's the cleanest pattern PrestaShop has for "I just want to add a few CSS rules without owning a child theme."

themes/your-theme/
  assets/
    css/
      theme.css          <-- theme's main stylesheet (updated by theme)
      custom.css         <-- your overrides (never touched by updates)
    js/
      theme.js           <-- theme's main JS (updated by theme)
      custom.js          <-- your scripts (never touched by updates)

When to reach for which:

  • custom.css — CSS-only changes: colours, fonts, spacing, hiding elements, minor layout. No HTML changes. No JS. The fastest possible iteration loop.
  • Child theme — anything that changes HTML structure, adds JavaScript, overrides a module's template, or restructures a layout. A child theme can include its own custom.css on top.

Classic and Hummingbird do not auto-load custom.css. If you're on a default PrestaShop theme, you need either a child theme with the file registered in theme.yml, or a module like Custom CSS & JavaScript that lets you inject code from the back office. We use the latter on small client shops where the merchant won't touch SSH and we just need to push three colour tweaks.

CCC will bite you. When Combine, Compress, Cache is on, your custom.css gets merged into a hash-named combined file. Edit custom.css and nothing changes on the front until you bump the CSS version in Performance settings or delete themes/*/assets/cache/. Every "but I changed the CSS!" support ticket we've answered starts here.

Registering CSS through theme.yml

assets:
  use_parent_assets: true
  css:
    all:
      - id: my-child-custom-style
        path: assets/css/custom.css
        media: all
        priority: 200

Higher priority values load later, giving you natural override authority over parent styles. On Hummingbird, prefer @layer over priority gymnastics — it's the whole reason the layer system exists. On Classic, match the parent's selector depth and avoid !important. Once you start sprinkling !important, you're in a war you can only escalate.

Recolouring a Hummingbird shop in three lines

:root {
  --bs-primary: #2563eb;
  --bs-primary-rgb: 37, 99, 235;
  --bs-body-font-family: 'Inter', system-ui, sans-serif;
}

That's it. Bootstrap 5's custom-property approach means rebrand-level changes don't need a single SCSS recompile.

JavaScript customisation

Registering JS through theme.yml

assets:
  use_parent_assets: true
  js:
    all:
      - id: my-child-custom-js
        path: assets/js/custom.js
        priority: 200

Swap all for a page name to scope the script: product, category, cart, checkout, cms, index (homepage), or any controller name. We load most analytics and tracking scripts globally, and most per-page enhancements scoped — there's no reason a cart-page sticky bar needs to parse on every product impression.

PrestaShop's JS event bus

The front office exposes a small event bus on window.prestashop. Listen for the events you care about instead of polling the DOM:

prestashop.on('updatedProduct', function(event) { /* combination changed */ });
prestashop.on('updateCart', function(event) { /* cart updated */ });

jQuery: still loaded, still don't remove it

Classic loads jQuery globally. Hummingbird also loads jQuery 3.x for module compatibility, even though the theme itself is TypeScript. For your own code on Hummingbird, vanilla JS gives better performance — Bootstrap 5 dropped its jQuery dependency and the theme follows. But if you remove jQuery to "clean up," every third-party module that still uses $(...) breaks. We have a rule: never disable jQuery on a production shop without auditing every active module first.

Module template overrides

Any module's templates can be overridden from the theme by mirroring the module's directory structure under themes/your-child/modules/. This is one of PrestaShop's best features and the cleanest way to restyle a third-party module without forking it.

Resolution order

  1. Child theme: themes/my-child/modules/module_name/views/templates/...
  2. Parent theme: themes/parent/modules/module_name/views/templates/...
  3. Module itself: modules/module_name/views/templates/...

The mechanics

Find the module's template (e.g. modules/ps_featuredproducts/views/templates/hook/ps_featuredproducts.tpl), copy it to the matching path under your theme's modules/ directory, edit. The module updates freely without touching your version.

Theme overrides win absolutely. If a module patches its template to fix an XSS bug and your theme has a six-month-old copy, the old version keeps rendering — XSS and all. Diff your module overrides against the current module versions at least once per quarter. We do this as part of every shop's quarterly maintenance.

Translations inside child themes

Translation lookup order:

  1. Database translations (Back Office > International > Translations)
  2. Child theme translations/ directory
  3. Parent theme translations/ directory
  4. Module translations
  5. Core translations

For one-off string changes, just use the Back Office translation editor. For anything you want in Git, create XLIFF files under themes/my-child/translations/en-US/Shop/Theme/Global.xlf:

<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
  <file original="shop-theme-global" source-language="en-US"
        target-language="en-US" datatype="plaintext">
    <body>
      <trans-unit id="add_to_cart" approved="yes">
        <source>Add to cart</source>
        <target>Add to bag</target>
      </trans-unit>
    </body>
  </file>
</xliff>

Multilingual shops get one directory per locale: translations/fr-FR/, translations/de-DE/. Clear cache after changes — translations are aggressively cached: php bin/console cache:clear.

Customisations we end up doing on most shops

Restructured header

{extends file='parent:_partials/header.tpl'}

{block name='header_nav'}
  <nav class="custom-header-nav container">
    <div class="row align-items-center">
      <div class="col-md-3">{hook h='displayNav1'}</div>
      <div class="col-md-6 text-center">
        <a href="{$urls.base_url}"><img src="{$shop.logo}" alt="{$shop.name}"></a>
      </div>
      <div class="col-md-3 text-end">{hook h='displayNav2'}</div>
    </div>
  </nav>
{/block}

Custom fonts done right

WOFF2 files in assets/fonts/, referenced from CSS:

@font-face {
  font-family: 'CustomFont';
  src: url('../fonts/CustomFont-Regular.woff2') format('woff2');
  font-weight: 400;
  font-display: swap;
}
body { font-family: 'CustomFont', system-ui, sans-serif; }
Always set font-display: swap. Without it, the browser hides text until the font loads and your Largest Contentful Paint gets penalised by Core Web Vitals. Preload the critical font: <link rel="preload" as="font" type="font/woff2" crossorigin>. We've watched LCP scores jump 30 points on shops that did just this one fix.

Moving the full description out of the tabs

{extends file='parent:catalog/product.tpl'}

{block name='product_description_short'}
  <div class="product__description-short">{$product.description_short nofilter}</div>
  {if $product.description}
    <div class="product__description-full mt-3">{$product.description nofilter}</div>
  {/if}
{/block}

{block name='product_description'}{/block} {* removed from tabs *}

What changed in PS 9 themes

  • Hummingbird is the default on fresh PS 9.1 installs. Upgraded shops keep their previous theme. Classic still ships and works, but new features land on Hummingbird first.
  • Smarty stays for the front office. Twig is used in the admin (increasingly, though the migration is not 100% complete). Your front-office {extends} and {block} overrides keep working unchanged.
  • Bootstrap 5.3 instead of Bootstrap 4. If you're migrating a Classic child to Hummingbird, the Bootstrap migration guide is the page you want open.
  • CSS Layers change how specificity resolves. Stop reaching for !important.
  • WebP is the default image format. New framework key in theme.yml: bootstrap-v5.3.3.
  • Some legacy JS globals are gone. Anything that hooked into the old Classic JS will need testing.

Twig may eventually reach the front office in a future major. Keep your overrides minimal and your block names sane and you'll have an easier transition when it does.

Debugging theme issues

First move, always: php bin/console cache:clear. For development, set Template cache to "Force compilation" and turn CCC off in Advanced Parameters > Performance. Anything else and you're debugging cached files, not your code.

If you change a template and don't see the change, the cache is lying to you. Don't keep tweaking the template — clear the cache, force-compile, then look again. Half the "my override doesn't work" tickets we get start as a forgotten compile cache.

Append ?SMARTY_DEBUG to any front URL for a pop-up showing every template loaded and every variable in scope. Set _PS_SMARTY_FORCE_COMPILE_ and _PS_SMARTY_CONSOLE_ to 1 in config/defines.inc.php to keep it on across reloads.

The errors you'll actually hit

SymptomMost likely causeFix
Blank pageSmarty syntax errorCheck var/logs/ first, always
Template not foundWrong path or missing parent: prefixVerify path mirrors parent exactly
Request hangs / 504{extends} without parent: = infinite loopAdd the parent: prefix
Block content missingBlock name typoOpen parent template, copy the exact name
CSS not applyingCache or specificityClear cache, then check priority/layer in DevTools
Double-encoded HTMLAuto-escaping already-escaped content{$var nofilter}

If the cache is genuinely stuck, blow away var/cache/prod/smarty/compile/ and var/cache/prod/smarty/cache/ by hand. We do this more often than we'd like to admit.

Upgrading: where the child theme finally pays for itself

When the parent theme updates, here's what happens to each file:

  • Only in the parent — updates normally, child inherits the new version automatically.
  • Overridden in the child — your child's version still wins. The parent update is invisible to your shop.
  • Custom child-only files — completely untouched.

Before pulling the trigger on a parent update: back up everything, list your overrides (find themes/my-child/templates -name "*.tpl"), run the update in staging (we have a runbook for that in the local development guide), diff changed parent templates against your overrides, then test every customised page on real devices. Skip any of these and you'll find out at 8 AM on the wrong Monday.

Targeted {extends} overrides beat full-file copies, every time. When the parent adds a new block, your {extends} child inherits it automatically. A full-file copy misses it entirely — and you find out only when a customer complains the new feature isn't there.

Pre-launch checklist

Configuration: theme.yml has the correct parent: and a lowercase, no-spaces name:. use_parent_assets: true is set. preview.png exists. Compatibility range matches the PS version you're shipping on.

Templates: Every override uses {extends file='parent:...'} where it can. Block names match the parent character-for-character. var/logs/ is clean. Module overrides have been diff'd against the current module versions. nofilter is on every HTML variable that doesn't need re-escaping.

Assets: Custom CSS loads after the parent's (high priority or the right @layer). No !important sprinkled around. Fonts are WOFF2 with font-display: swap. No JS console errors. CCC has been turned on and the site still renders.

Testing: Desktop in Chrome, Firefox, Safari, Edge. Mobile in iOS Safari and Android Chrome. Every responsive breakpoint. Touch: homepage, categories, product, cart, checkout, customer account, CMS, search, 404.

Maintenance: Child theme is in Git. Overrides are documented somewhere a human can find them. The build process is written down (see our dev tools guide). The team knows not to edit parent files.

Related reading

Loading...
Back to top