Updated June 2026, the four update-safe methods below apply to PrestaShop 1.7 through 9 (Hummingbird's SCSS pipeline included). Code samples use the 1.7+ register* asset API.
You change a button colour. You open the theme's stylesheet, edit it, the button looks right. Three weeks later you update the theme to patch a security fix, and your change is gone, silently overwritten, with no warning and no record of what you'd touched. Every PrestaShop merchant who customises by editing core files hits this wall eventually, and the frustrating part is that it's entirely avoidable. The platform gives you several update-proof places to put custom CSS and JavaScript; the trick is knowing which one fits the change you're making, and why "just edit the theme" quietly fails. This guide is about exactly that: where your custom code can live so a theme update, a core upgrade, or a module update never erases it.
Why editing theme and module files is a trap
A PrestaShop theme is a set of files on disk: Smarty templates (.tpl), stylesheets, and JavaScript, all sitting under /themes/your-theme/. When you update that theme, whether you re-upload a newer ZIP or pull a vendor patch, the update process replaces those files wholesale with the new version. It does not merge your edits; it overwrites them. The same is true of every module: its assets live in /modules/module-name/views/, and a module update replaces that directory entirely. There is no diff, no prompt, no backup. Your work is simply gone, and because nothing errors out, you often don't notice until a customer does.
So what does that mean for you? Every hour spent editing core files is an hour you'll spend again at the next update, and "the next update" is not optional, because that's how security and bug fixes reach your store. The whole point of the methods below is to break that loop: write your customisation once, in a place the updater never touches, and stop re-applying lost work.
Match the method to the change
There is no single "right" way to add custom code, there's a right way for your change. A one-line colour tweak and a full visual overhaul belong in completely different places. Pick the lightest method that survives updates and does the job:
| You want to… | Use | Who it suits | Survives updates because… |
|---|---|---|---|
| Add a handful of CSS rules | custom.css file | Anyone with file access | Loads last so it wins the cascade (note: lives in the theme tree, so not fully update-proof) |
| Make heavy, ongoing visual changes | Child theme | Designers, agencies | Overrides live in a separate theme the parent update can't reach |
| Inject code from the back office (no file access) | HTML/code-block module | Store owners, non-developers | Code is stored in the database, independent of files |
| Load CSS/JS conditionally, like a pro | A small custom module + asset hooks | Developers | Theme-independent; nothing in the theme tree to overwrite |
The sections below cover each in PrestaShop-specific detail. Two of these methods touch topics that have their own dedicated guides, child themes and back-office HTML blocks, so rather than repeat that depth here, this article stays on the one thing all four share: keeping your code update-safe.
Method 1, the custom.css file (for small CSS tweaks)
Most modern PrestaShop themes ship with an empty custom.css file that the theme loads after every other stylesheet. Because it loads last, your rules naturally win the cascade, which makes it the convenient place for a few quick tweaks. Be aware, though, that custom.css usually lives inside the theme tree, so it isn't inherently update-proof. A theme ZIP or vendor update can replace it. For genuine update safety, put your rules in a child theme or a back-office code block; reserve the theme's own custom.css for throwaway colour, spacing, or font tweaks you can afford to re-apply.
Many themes, including Classic-style themes (Classic is the PrestaShop 8 default), load /themes/your-theme/assets/css/custom.css; the newer Hummingbird theme is another modern option. But custom.css support and its exact path are theme-dependent, so don't assume it as universal native behaviour, some older or commercial themes use /themes/your-theme/css/custom.css instead, and a minority don't wire it up at all. Verify the active theme's asset registration or documentation, and look for an existing (often empty) custom.css before creating your own. Drop your rules in, for example a primary-button override like .btn-primary { background-color: #2c3e50; border-color: #2c3e50; }, clear the cache, and you're done.
Know the limits before you lean on it. custom.css is CSS only, no JavaScript. It loads on every page whether the rule is needed there or not, and it gives you no way to target a specific page type, customer group, or shop. And a minority of themes don't wire it up at all. For a few tweaks those limits don't matter; the moment they start to, move up to one of the methods below.
Method 2, a child theme (for heavy, ongoing customisation)
When you're making more than a handful of changes, editing templates, restyling whole sections, layering in your own JavaScript, a child theme is the update-proof home for all of it. A child theme inherits everything from its parent and lets you override only the specific files you care about, in a separate directory the parent's updates can't reach. You point a config/theme.yml at the parent (its parent: key naming the parent theme folder), copy in only the files you're overriding, and add your own custom.css and .js alongside them.
This is the correct answer for serious customisation, and it's deep enough to deserve its own treatment rather than a rushed paragraph, including the all-important reason you build one instead of editing the parent directly. We covered that fully in child themes in PrestaShop: why you should never edit the parent theme. If you haven't yet picked the theme you're customising, start one step earlier with how to choose the right PrestaShop theme for your business. A child-theme-friendly parent saves you a lot of grief later.
Method 3, inject code from the back office (no file access)

Back-office blocks keep small HTML, CSS and JavaScript snippets out of theme files.
If you don't have SSH or FTP access, don't want it, or simply don't want to redeploy files for a one-line tracking snippet, a code-block module lets you add custom CSS, JavaScript, and HTML straight from the PrestaShop back office. The code lives in the database rather than in any theme or module file, which is precisely why a theme update or core upgrade can't touch it, and why you can disable a block to undo its effect instantly, without deleting anything.
This is the everyday tool for adding a Google Analytics or Meta Pixel script, dropping a seasonal CSS override into the page head, or placing a promo banner, and it's worth a guide of its own rather than a paragraph here. We walk through it in HTML blocks: adding custom content anywhere in your PrestaShop store, which covers assigning blocks to hooks and showing them only on the pages or customer groups you choose. Our own mprhtmlblocks module is built for exactly this, custom code, managed from the back office, hook-targeted and conditionally shown, with nothing for the next update to overwrite.
Method 4, a small custom module with asset hooks (for developers)
For developers, the cleanest and most controllable home for custom assets is a tiny module that registers them through PrestaShop's own asset pipeline. It's completely theme-independent, there's nothing in the theme tree to overwrite, so it survives theme switches and core upgrades alike, and it unlocks conditional loading you can't get from custom.css.
The work happens in a single hook, hookActionFrontControllerSetMedia(). Inside it you call registerStylesheet() and registerJavascript() on the controller, passing an asset id, the path under your module, and an options array. Those options are the payoff over the older 1.6-era addCSS() / addJS() calls: a priority (higher numbers load later, which is how you guarantee your script runs after the theme's), a media query, a JS position of bottom, and async/defer attributes. On PrestaShop 1.7 and up, prefer the register* methods; reach for addCSS/addJS only when you're maintaining a 1.6 store.
public function install()
{
return parent::install()
&& $this->registerHook('actionFrontControllerSetMedia');
}
public function hookActionFrontControllerSetMedia($params)
{
if (!$this->context->controller instanceof ProductController) {
return;
}
$this->context->controller->registerStylesheet(
'module-' . $this->name . '-product',
'modules/' . $this->name . '/views/css/product.css',
['media' => 'all', 'priority' => 200]
);
$this->context->controller->registerJavascript(
'module-' . $this->name . '-product',
'modules/' . $this->name . '/views/js/product.js',
['position' => 'bottom', 'priority' => 200]
);
}
The addCSS query-string bug worth knowing
There's a long-standing gotcha where addCSS() with a cache-busting query string (something like ?v=1.2.3) can produce a path PrestaShop mangles, so the stylesheet shows up in the page source but the browser never applies it. It's a maddening one to debug because nothing 404s. If you cache-bust your assets, use registerStylesheet(), which handles the versioning correctly.
Conditional loading, only where it's needed
The real reason developers reach for a module is conditional loading. Inside the same hook you can inspect $this->context->controller and register assets only when it matches, for example checking instanceof ProductController before adding product-page CSS, or instanceof OrderController before adding checkout JavaScript. The benefit is concrete: your custom code stays off the 90% of pages that don't need it, so you're not paying for it in every page's load time. That same principle. Load light, load late. Is what keeps a customised store fast, and it's the spirit behind small front-end touches like a back-to-top button being delivered as proper conditional assets rather than inline script dumped into a template.
JavaScript needs more care than CSS
CSS rarely fights back; JavaScript can. Two things cause most of the trouble. First, jQuery: PrestaShop 1.7+ ships jQuery 3.x on the front office, so if your script depends on jQuery, give it a high enough priority that it loads after jQuery does. Otherwise you'll see "$ is not defined" on first paint. The back office is its own world (older versions carried jQuery 1.x, newer ones 3.x), so check the page source before you write admin-side script against an assumed version.
Second, and this is the rule people most want to break: never put a raw <script> tag inside a Smarty template. It feels quick, but an inline script bypasses PrestaShop's asset management. It won't be managed or combined by CCC (Combine, Compress, Cache), and it can conflict with Content Security Policy on stores that set a CSP header. Always ship JavaScript as an external .js file registered through the proper hook. As for modern syntax, arrow functions, async/await, ES modules, it's fine on PrestaShop 8 and 9, where IE11 is no longer a concern; if you still support 1.7 stores with old-browser traffic, you may need to transpile with Babel.
When custom CSS "doesn't work": specificity
The single most common "my custom CSS is being ignored" situation isn't an update problem at all, it's specificity. Your rule loads fine, but the theme's selector is more specific, so the browser applies the theme's instead. The fix, in order of preference:
- Match the theme's selector. Open dev tools, find the exact selector the theme uses, and write yours to match it. Since custom.css loads last, an equally specific selector already wins.
- Add a parent for weight. If matching isn't enough, scope it, #wrapper .btn-primary beats a bare .btn-primary without resorting to brute force.
- Use !important only as a genuine last resort. It works, but every !important you add makes the next override harder, and you end up in a specificity arms race that future-you has to untangle.
Testing: why "I changed it and nothing happened"
Custom CSS and JS changes that seem to do nothing are almost always a caching layer, not your code. Work through these in order:
- Clear the PrestaShop cache. Go to Advanced Parameters → Performance and click Clear cache after every change. While developing templates, set Force compilation to Yes so Smarty re-renders.
- Watch out for CCC. With Combine, Compress, Cache enabled, your individual files are merged into bundles, which can change load order. If something behaves oddly, disable CCC temporarily to isolate it, then re-enable for production.
- Use the browser. The Network tab confirms your file actually loaded (200, not 404); the Elements tab shows whether your rule is applied or struck through by a more specific one.
- Purge the CDN. If you run Cloudflare or another CDN, purge it after asset changes. Stale edge copies are behind a huge share of "but I already changed that" reports.
The short version
- Never edit theme or module files directly, the next update overwrites them with no warning.
- 1–5 small CSS tweaks: drop them in custom.css.
- Heavy, ongoing changes: build a child theme.
- No file access, or quick snippets: use a back-office HTML/code block.
- Developer, conditional loading: a small module with registerStylesheet() / registerJavascript().
- Ship JS as external files, load at the bottom, clear every cache after changes, and keep your customisations backed up outside the theme directory.
Customising PrestaShop and surviving updates are not in tension. They only feel that way when your code lives in the wrong place. Pick the lightest update-safe method for the change in front of you, and a theme upgrade becomes what it should be: a routine click, not a day of re-applying lost work. If you'd rather manage custom code from the back office than touch files at all, our mprhtmlblocks module lets you inject and toggle HTML, CSS, and JavaScript block by block, stored in the database, untouched by the next update.
Frequently asked questions
Is the theme's custom.css file actually update-proof?
Not fully. Because custom.css usually lives inside the theme tree, a theme ZIP or vendor update can replace it along with everything else. It's convenient, it loads last so your rules win the cascade, but treat it as the place for throwaway tweaks you can afford to re-apply. For genuine update safety, put rules in a child theme or a back-office code block where the updater can't reach them.
My custom CSS loads but the theme's style still wins. What's wrong?
That's a specificity problem, not an update or caching one. The theme's selector is more specific than yours, so the browser applies the theme's. Open dev tools, find the exact selector the theme uses, and write yours to match it, since custom.css loads last, an equal selector already wins. Add a scoping parent like #wrapper only if matching isn't enough, and reach for !important only as a genuine last resort.
Can I just paste a <script> tag into a template?
Don't. An inline script bypasses PrestaShop's asset management, so it won't be combined by CCC, and it can break on stores that set a Content Security Policy header. Always ship JavaScript as an external .js file registered through hookActionFrontControllerSetMedia() with registerJavascript(), or through a back-office code block, never as raw markup in a .tpl.
How do I load my CSS or JS only on certain pages?
That's the main reason to use a small module instead of custom.css. Inside hookActionFrontControllerSetMedia(), inspect $this->context->controller and register the asset only when it matches, for example instanceof ProductController for product-page CSS, or instanceof OrderController for checkout JS. Your code then stays off the pages that don't need it, so you're not paying for it on every page load.
Why does my script throw "$ is not defined"?
Your JavaScript is running before jQuery has loaded. PrestaShop 1.7+ ships jQuery 3.x on the front office, so give your script a high enough priority in registerJavascript() that it loads after jQuery does. The back office is a separate world, older versions carried jQuery 1.x, so check the page source before writing admin-side script against an assumed version.
I changed my CSS and nothing happened. Where do I look?
Almost always a caching layer, not your code. Work through it in order: clear the PrestaShop cache under Advanced Parameters → Performance; temporarily disable CCC (Combine, Compress, Cache) if load order looks off; confirm in the browser Network tab that your file loaded as a 200, not a 404; and purge your CDN (Cloudflare and friends), stale edge copies cause a huge share of "but I already changed that" reports.
Should I still use addCSS() and addJS()?
Only on a 1.6 store. On PrestaShop 1.7 and up, prefer registerStylesheet() and registerJavascript(). They give you priority, media queries, a JS bottom position, and async/defer, and they handle cache-busting versioning correctly. The old addCSS() has a long-standing bug where a ?v= query string can mangle the path so the file appears in the source but never applies, with no 404 to tip you off.
Comments
Leave a comment
Share a question, an installation detail, or feedback that could help another reader.