Shahid Malla

WHMCS Theme Structure: The Practical Map

Every WHMCS theme tutorial shows the file tree and stops there. This one shows the decisions that determine whether your theme survives the next WHMCS update — inheritance, Smarty essentials, when to use hooks instead of templates.

S Shahid Malla
· Feb 7, 2026 · 9 min read · 143 views
shahidmalla.com/blog/whmcs-theme-structure-the-practical-map
WHMCS Theme Structure: The Practical Map
On this page (17 sections)

Most WHMCS theme tutorials show you the file tree and stop there. That's like handing someone a parts list for a car and calling it a driving lesson.

I've built and customized dozens of WHMCS themes — from minor tweaks on the default Six theme to full client-portal rebuilds for hosting brands that wanted nothing to look like WHMCS. This is the practical map I wish existed: what lives where, what actually matters, and the decisions that determine whether your theme survives the next WHMCS update.

The two-folder rule that solves 80% of theme problems

WHMCS has two separate theme systems, and people constantly confuse them:

  • /templates/ — the client area templates (what customers see)
  • /admin/templates/ — the admin area templates (what you and staff see)

Most "theme" work is in /templates/. Each subdirectory is one theme. The default theme is six:

/templates/
  six/                 # default — never edit this directly
  six-custom/          # YOUR theme — created by cloning six
  orderforms/          # shared order-form templates

Rule one of WHMCS theming: never edit the six folder. Every WHMCS update overwrites it. I've seen entire branding systems vanish because someone "just changed the header" in the wrong folder.

Cloning the default theme correctly

Don't just cp -r six six-custom. WHMCS has a documented theme-inheritance model that lets your custom theme fall back to six for anything you haven't overridden. This means:

  • You only ship the files you actually changed.
  • WHMCS updates can ship fixes to the inherited files without your custom theme blocking them.
  • Your diff is auditable — anyone can see what you customized.

The correct clone:

cd /path/to/whmcs/templates
mkdir six-custom
cp six/theme.yaml six-custom/theme.yaml

Open six-custom/theme.yaml and edit:

name: "Six Custom"
author: "Your Brand"
parent: "six"        # this enables inheritance

Then activate it in Setup → General Settings → General → Template.

Now copy only the files you want to customize from six/ to six-custom/. Need to change the header? Copy header.tpl over. Need to restyle the order form? Copy orderforms/standard_cart/ over. Everything else inherits from six.

The files that actually matter

A six theme contains ~150 template files. You'll touch fewer than 20. Here's the map I use:

Global layout

  • header.tpl<head>, top nav, logo. The file I customize most often.
  • footer.tpl — footer markup, JS includes.
  • sidebar.tpl — secondary nav for logged-in clients.
  • theme.yaml — theme metadata + which CSS/JS files to load.

Client area

  • clientareahome.tpl — the dashboard after login.
  • clientareaproducts.tpl — "My Services" list.
  • clientareaproductdetails.tpl — individual service page.
  • clientareainvoices.tpl + viewinvoice.tpl — billing pages.
  • clientareadetails.tpl — profile / account settings.

Pre-login pages

  • login.tpl — login form.
  • register.tpl — signup form.
  • pwreset.tpl — password reset.

Support

  • supporttickets.tpl — ticket list.
  • viewticket.tpl — single ticket.
  • supportticketsubmit.tpl — create-ticket form.
  • knowledgebase.tpl + announcements.tpl

Knowing this map saves real time. When a client says "I want a different colour scheme on the invoice page," I know exactly which file to touch and what it cascades into.

Smarty templating — the parts you'll actually use

WHMCS uses Smarty 3. If you've used Blade, Twig, Liquid, or even ERB, the model translates instantly:

What you wantSmarty syntax
Print a variable{$companyname}
Conditional{if $loggedin}...{else}...{/if}
Loop{foreach $items as $item}...{/foreach}
Function call{$item.name|escape}
Include another template{include file="orderform.tpl"}
Translation{$LANG.welcomeback}
URL helper{$systemurl}

Smarty pitfalls I see all the time:

  • Always escape user content. {$client.firstname|escape}, not {$client.firstname}. WHMCS does some auto-escaping but not everywhere.
  • Arrays use dot notation. {$invoice.total} for arrays, {$invoice->total} for objects (depends on the variable).
  • Curly-brace conflict with CSS/JS. If you're writing inline JS that uses curly braces (object literals), wrap it with {literal}...{/literal} so Smarty doesn't try to parse it.

Loading CSS and JS the right way

Don't dump <link> tags in header.tpl with version strings you'll forget to bump. Use theme.yaml:

name: "Six Custom"
author: "Your Brand"
parent: "six"
stylesheets:
  - css/brand-overrides.css
  - css/dashboard.css
scripts:
  - js/onboarding.js

Files referenced here are loaded after the parent theme's assets, so your overrides win. WHMCS appends a cache-busting query string automatically.

If you need conditional loading (load this CSS only on the order form), use a hook instead:

add_hook('ClientAreaPageOrderForm', 1, function () {
    return [
        'extraJsFiles' => [
            'templates/six-custom/js/orderform-validation.js',
        ],
    ];
});

Customizing without touching templates

Half the time I'm asked to "customize a template," the change can be done with a hook instead. This is dramatically more upgrade-safe.

Example — adding a "loyalty badge" to the dashboard:

add_hook('ClientAreaPageDashboard', 1, function ($vars) {
    if (!isset($_SESSION['uid'])) return $vars;

    $monthsActive = Capsule::table('tblclients')
        ->where('id', $_SESSION['uid'])
        ->value(Capsule::raw('TIMESTAMPDIFF(MONTH, datecreated, NOW())'));

    $vars['loyaltyBadge'] = match (true) {
        $monthsActive >= 60 => ['label' => 'Diamond', 'color' => '#06b6d4'],
        $monthsActive >= 36 => ['label' => 'Platinum', 'color' => '#94a3b8'],
        $monthsActive >= 12 => ['label' => 'Gold',     'color' => '#eab308'],
        default             => null,
    };

    return $vars;
});

Then in your template:

{if $loyaltyBadge}
  <span class="badge" style="background:{$loyaltyBadge.color}">
    {$loyaltyBadge.label} member
  </span>
{/if}

Now you can ship the badge logic to any theme that ever exists, and the only template change is a tiny snippet.

Order form templates — a separate world

Order forms have their own template system in /templates/orderforms/. Each subdirectory is one order-form template:

/templates/orderforms/
  standard_cart/
  modern/
  premium_comparison/
  ...

WHMCS lets you assign different order-form templates to different product groups (Setup → Products/Services → Product Groups → edit a group → Order Form Template). So you can ship a sleek "modern" form for hosting plans and a verbose comparison form for VPS plans, with no code changes.

When customizing an order form, copy a base template (I usually start with standard_cart) into a new folder name like standard_cart_brand. Each template folder contains its own configureproduct.tpl, viewcart.tpl, checkout.tpl, and CSS.

Build from zero: a minimal customization in 10 minutes

Let's add a "trust strip" below the header showing uptime, support response time, and a guarantee badge.

Step 1 — Clone the theme.

cd /var/www/whmcs/templates
mkdir six-brand
cat > six-brand/theme.yaml <<'YAML'
name: "Six Brand"
author: "Your Hosting Co"
parent: "six"
stylesheets:
  - css/trust-strip.css
YAML

Step 2 — Override header.tpl.

cp six/header.tpl six-brand/header.tpl

Open six-brand/header.tpl and find the closing </header> tag. Just above it, add:

<div class="trust-strip">
  <span>✓ 99.99% uptime</span>
  <span>✓ &lt;30 min support</span>
  <span>✓ 30-day money back</span>
</div>

Step 3 — Add the CSS.

mkdir -p six-brand/css
cat > six-brand/css/trust-strip.css <<'CSS'
.trust-strip {
  background: #f8fafc;
  border-bottom: 1px solid #e2e8f0;
  padding: .5rem 1rem;
  display: flex;
  gap: 1.5rem;
  justify-content: center;
  font-size: .85rem;
  color: #475569;
}
.trust-strip span { display: inline-flex; align-items: center; gap: .35rem; }
CSS

Step 4 — Activate.

WHMCS Admin → Setup → General Settings → General → Template → Six Brand → Save.

Reload your client area. The trust strip appears on every page. The rest of the theme is inherited from six and will receive WHMCS updates normally.

Responsive design and performance

The six theme is responsive out of the box (Bootstrap 3 under the hood — yes, still). Two things I always do on every theme:

  1. Audit and remove unused Bootstrap. The default theme loads ~150KB of unused CSS. If you're rebuilding the visual layer with your own utility CSS, gradually replace.
  2. Defer non-critical JS. The default theme blocks render on jQuery and several heavy scripts. Add defer to anything that doesn't need to execute before paint.

For a hosting business where the client area is the second-most important page after the order form, this can drop perceived load time by 1-2 seconds. I've measured it.

How to verify your theme works

  • Loads without warnings. Enable Setup → General Settings → Other → Display Errors. Reload your client area. If you see Smarty errors, you have a syntax issue.
  • Inherits correctly. Delete a file from your custom theme — the parent's version should render. If you get a "template not found" error, the inheritance isn't wired (check theme.yaml).
  • Survives a WHMCS update test. Before going live, do a dry-run update on staging. The custom theme should keep working without modification.

Common pitfalls

"My customization disappeared after the WHMCS update." You edited the six theme directly. Restore from backup, copy your changes into a custom theme, and re-apply.

"Smarty syntax errors I can't find." Most often: an unclosed {if} or {foreach}. WHMCS error messages aren't great here — bisect by commenting out half the template until you isolate it.

"Inline JavaScript is breaking the page." You forgot {literal}. JS object literals look like Smarty variables. Wrap them.

"My CSS won't apply." Browser cache. WHMCS appends a version string only when the file is referenced in theme.yaml — if you added a <link> manually in header.tpl, you control your own cache busting.

My take — buy vs. build

I get this question constantly: "should I buy a premium WHMCS theme or build my own?"

Premium themes (the marketplace ones in the $50-200 range) are fine for one purpose: you want a different look without writing CSS. They generally do not give you flexibility for unusual workflows, and they often add their own update churn.

Build your own when:

  • Your brand needs visual identity beyond color swaps.
  • You have unusual client-area flows (e.g. a marketplace, an IPTV portal, a SaaS dashboard).
  • You care about page weight and performance.

For the businesses I work with — hosting companies that want a real product feel, not a CMS feel — I always recommend building. The maintenance is lower than people fear if you stick to the inheritance model.

Going further

  • WHMCS theme developer docs — official reference.
  • Smarty 3 docs — needed often.
  • Look at /templates/orderforms/premium_comparison/ in your install for an example of a richer, more recent template layout.

I rebuild WHMCS themes and client portals end-to-end for hosting businesses that want their product to look like a product, not like a billing system. Tell me what you have in mind and I'll send a quote in 24 hours.

Share this article

S

Written by

Shahid Malla

WHMCS expert, full-stack developer, technical lead at Fada.cloud. 10+ years building hosting platforms, custom modules, and automation that ships.

Trusted platforms

Prefer to hire through a platform?

Not sure about working directly? Hire me through Fiverr or Upwork instead - same me, same work, with the platform's buyer protection and escrow.

Got a project like this?

Tell me what you need - I'll send a real quote within 24 hours.