A theme is no longer a copied stylesheet. Since Maho 26.9 the storefront is dressed by a compiled skin built on Tailwind CSS 4 and daisyUI 5, and your theme is a thin layer of tokens on top of it. The payoff: change a handful of variables and you restyle the whole store, without touching core templates and without losing your work on every upgrade. In this tutorial we create a theme, activate it and customize it.
What you need
- Maho 26.9 or later, with the new skin system.
- A working development environment. Our examples use Harbor, the open source Docker Compose
launcher by MageMEGA, inspired by Laravel Sail, introduced in Local Maho development with
Harbor: if it is new to you, start there and then
come back. The commands also work with plain
./maho. - Node.js only for the version with its own Tailwind build (Option B). Design tokens need no build tooling at all.
Quick start: a theme in three steps
1. Scaffold the theme
./vendor/bin/harbor maho dev:frontend:theme:create -p base -t mytheme --parent base/default
The command writes:
app/design/frontend/base/mytheme/etc/theme.xml(the theme and its parent),app/design/frontend/base/mytheme/layout/local.xml(layout changes),public/skin/frontend/base/mytheme/css/theme.css(the theme identity).
The theme is created inside the existing base package and in the project root. Pass --parent base/default explicitly: before the first local theme exists, the list of available themes is
empty. Add --tailwind if you want your own build.
2. Define the identity
Open public/skin/frontend/base/mytheme/css/theme.css and change the tokens:
:root {
--font-display: 'Figtree', sans-serif;
--color-primary: #0e7a5f;
--color-base-100: #ffffff;
--color-base-200: #f3f6f4;
--color-base-content: #17221d;
--radius-box: 1rem;
}
Every component derives from these variables, so the theme spreads across the whole store.
3. Activate the theme for one store view
./vendor/bin/harbor maho config:set design/package/name "base" --scope stores --scope-id 2
./vendor/bin/harbor maho config:set design/theme/skin "mytheme" --scope stores --scope-id 2
./vendor/bin/harbor maho cache:flush
Set design/theme/skin per website or store view, never globally: at default scope it
replaces the skin of every website and you lose the industry variants (base/fashion,
base/electronics, and so on). Find the store view id in the admin, under System > Manage
Stores.
To preview a theme without activating it, append ?___package=base&___skin=mytheme to any URL.
🧵 Tailor’s note. A Maho theme is a made-to-measure suit, not a whole garment sewn from scratch: the compiled skin is the fabric, the design tokens are your needles. A few stitches and the suit fits the whole shop.
How the new system works
The base package holds the templates and layouts in base/default plus the industry variants;
the compiled skin lives in public/skin/frontend/base/default. Every page loads two
stylesheets:
css/styles.css: the compiled engine, organised in cascade layers, shared by every theme.css/theme.css: the active theme’s identity, unlayered and loaded after, so it always wins, with no!important.
Hence the golden rule: override as little as possible. Every file you copy stops receiving
Maho’s fixes and must be merged by hand on each upgrade. The Composer plugin overwrites the core
files on every composer install/update, so never edit anything under
public/skin/frontend/base/...: create your own theme and keep your assets in its skin.
Customization tiers
Always use the lowest tier that achieves the result:
| Tier | Method | Upgrade impact |
|---|---|---|
| 0 | Theme Settings in the admin | None |
| 1 | Design tokens in theme.css |
None |
| 2 | Additional CSS rules in theme.css |
None |
| 3 | Layout XML in local.xml |
Minimal |
| 4 | Template overrides | High, avoid |
Customize with tokens (tier 1)
Tokens cover colors, typography, radii, prices, filters, the gallery and much more. When a token is not enough, add plain CSS below the tokens: being unlayered, it takes precedence.
.btn { border-radius: 9999px; }
#nav a { text-transform: uppercase; letter-spacing: 0.04em; }
Do not reach for !important: if it looks necessary, there is a layer or order mistake.
Dark mode
Dark mode follows the operating system through a @media block, keeping the zero-specificity
selector:
@media (prefers-color-scheme: dark) {
:root:where(:not([data-color-scheme="light"])) {
--color-base-100: #131917;
--color-base-content: #dbe7e1;
}
}
The merchant can switch it off per store view: the storefront then sets
data-color-scheme="light", and every dark rule stays attached to the :where() selector.
Option B: your own Tailwind build
Use it only when you write templates with Tailwind class names or use @apply. Create
public/skin/frontend/base/mytheme/src/tailwind.css and import the base entry:
@import "../../../base/default/src/tailwind.css";
:root { --color-primary: #0e7a5f; }
@layer components {
.mytheme-banner { @apply alert alert-info rounded-box; }
}
Then build:
./vendor/bin/harbor maho dev:frontend:theme:build --theme base/mytheme
With this option tailwind.css becomes css/styles.css, served instead of Maho’s bundle: you
own the engine and must rebuild it on every upgrade. Commit the compiled CSS (production never
needs Node.js) and never edit css/styles.css by hand.
Layout and sub-themes
To add, move or remove blocks you do not touch templates: use
app/design/frontend/base/mytheme/layout/local.xml.
<layout version="0.1.0">
<default>
<reference name="left"><remove name="left.newsletter"/></reference>
</default>
</layout>
Before changing anything, ask Maho which handles and layout files make up that page: the command below prints the handles loaded for the URL, the XML files involved, the removed blocks and the final block tree.
./vendor/bin/harbor maho dev:frontend:layout:debug https://localhost:8443/your-url.html
That way you know exactly where to hook into local.xml, and why a block is there or is not.
A sub-theme (seasonal variant, A/B test) lives in the same package and declares the parent. Its
theme.css replaces the main one, so import the parent first:
@import url('../../../base/mytheme/css/theme.css');
:root { --color-primary: #c62828; }
Fonts, icons and CMS content
-
Web fonts are declared in
theme.xmlwith<fonts>, never with@importin CSS: Maho renders a<link>the preload scanner sees right away. -
In CMS pages and blocks use the
{{icon ...}}directive instead of inline SVG (the sanitizer strips it):{{icon name="truck" variant="outline" size="32" label="Delivery"}} -
The daisyUI classes you can use in content are only those listed in the
tailwind.csssafelist.
Verify and ship
config:get design/package/nameandconfig:get design/theme/skinconfirm the activation, andcache:flushapplies the changes.dev:frontend:theme:export --theme base/mytheme --forcewrites the Theme Settings declared underglobal/design/tokensto files (without--force, it stops iftheme.cssalready exists). It does not exportdesign/tokens/custom_cssor the dark mode flag: port those into the theme by hand.- Do a visual check at roughly 390, 820 and 1280 px (the layout switches at 771 px) and make sure the console shows no errors or 404 assets.
🧵 Last tailor’s note. A good theme is not judged by its patches, but by how little it disturbs the original fabric. Few stitches, straight, and always in your own atelier. Nice sewing!
Troubleshooting
| Symptom | Check |
|---|---|
| The theme is not applied | config:get of design/package/name and design/theme/skin at the right scope, then cache:flush |
| The sub-theme lost the parent identity | the parent @import at the top of theme.css |
| CSS changes do not show | wrong path or scope, or you are editing the core copy (overwritten) |
| The theme is missing from the Design selects | an etc/theme.xml must exist under app/design/frontend/base/<theme>/ in the project root |
| Preview assets 404 | use /skin/frontend/... without the store code |
| Build fails | missing Node.js/Bun toolchain |
| Empty icon | wrong Tabler name, or flush the icons cache |
If you prefer to start from a ready theme
Lightclean is the MahoCommerce theme MageMEGA built for a clean, light store: it works as it is, or as the parent of a child theme made with the method of this guide. The overview is on the Lightclean page.
Editor's note.Text prepared with the help of AI and checked against the source before publication. It can still contain mistakes: if you find one, report it.Read the manifesto
Does your store still sell?
Tell us where you are starting from: we reply with a first assessment, no strings attached.
Ask for the store assessment