Vai al contenuto
Home / Blog /Tutorial / Creating a Maho theme with the new skin system

Creating a Maho theme with the new skin system

Build and customize a child theme on Maho 26.9+ without touching core files: from scaffolding to design tokens, all the way to activating it per store view.

Leggi in italiano

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.xml with <fonts>, never with @import in 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.css safelist.

Verify and ship

  • config:get design/package/name and config:get design/theme/skin confirm the activation, and cache:flush applies the changes.
  • dev:frontend:theme:export --theme base/mytheme --force writes the Theme Settings declared under global/design/tokens to files (without --force, it stops if theme.css already exists). It does not export design/tokens/custom_css or 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