Shahid Malla

How to Add Dark Mode to WHMCS

Dark mode for WHMCS in 2026 — one CSS-variables architecture, one toggle, no flash on load. Plus how to handle the things you can't theme (Stripe Elements, PayPal buttons).

S Shahid Malla
· Feb 13, 2026 · 5 min read · 86 views
shahidmalla.com/blog/how-to-add-dark-mode-to-whmcs
How to Add Dark Mode to WHMCS
On this page (11 sections)

Dark mode in the WHMCS client area isn't optional for a 2026 product — customers expect it, especially developers and sysadmins who keep WHMCS open in a tab while they work at night. The good news: implementing it properly takes one CSS file and one toggle. Most of the WHMCS theme work is already done if you set up your CSS with custom properties.

The approach that works

Three patterns I've seen attempted:

  1. Two separate stylesheets (light.css and dark.css), swap on toggle. Brittle; duplicates rules.
  2. CSS classes (body.dark-mode .card { ... }). Workable but every rule duplicates.
  3. CSS custom properties (variables) swapped at the :root level. One source of truth, one toggle, everything updates. This is the right approach.

Step 1 — Define your color tokens

In your theme's main CSS:

:root {
  /* Light mode (default) */
  --bg-page:    #ffffff;
  --bg-surface: #f8fafc;
  --bg-card:    #ffffff;
  --text-strong:#0f172a;
  --text-body:  #475569;
  --text-muted: #94a3b8;
  --border:     #e2e8f0;
  --brand:      #6366f1;
  --brand-cta:  #dfff4f;
  --danger:     #ef4444;
  --shadow:     0 4px 12px rgba(0, 0, 0, 0.06);
}

:root[data-theme="dark"] {
  --bg-page:    #0f172a;
  --bg-surface: #1e293b;
  --bg-card:    #1e293b;
  --text-strong:#f8fafc;
  --text-body:  #cbd5e1;
  --text-muted: #64748b;
  --border:     #334155;
  --brand:      #818cf8;     /* lighter for dark backgrounds */
  --brand-cta:  #dfff4f;
  --danger:     #f87171;
  --shadow:     0 4px 12px rgba(0, 0, 0, 0.4);
}

Step 2 — Use the tokens everywhere

Find every hardcoded color in your theme and replace:

/* Before */
.card { background: white; color: #333; border: 1px solid #eee; }

/* After */
.card { background: var(--bg-card); color: var(--text-body); border: 1px solid var(--border); }

If your theme uses the WHMCS-shipped six as the base, this audit is non-trivial — Bootstrap's variables are scattered throughout. You can:

  • Override Bootstrap's variables to use your tokens (cleanest).
  • Or layer your custom CSS after Bootstrap and override component by component.

Step 3 — Add the toggle

In your theme's header.tpl, add a button (typically near the user menu):

<button id="theme-toggle" aria-label="Toggle dark mode" class="theme-toggle">
  <svg class="icon-sun" ...>...</svg>
  <svg class="icon-moon" ...>...</svg>
</button>

And the JS:

<script>
(function () {
  const root = document.documentElement;
  const stored = localStorage.getItem('theme');
  const preferDark = window.matchMedia('(prefers-color-scheme: dark)').matches;

  // Apply on load — before content paints to avoid flash
  if (stored === 'dark' || (!stored && preferDark)) {
    root.setAttribute('data-theme', 'dark');
  }

  document.addEventListener('DOMContentLoaded', function () {
    const btn = document.getElementById('theme-toggle');
    if (!btn) return;
    btn.addEventListener('click', function () {
      const isDark = root.getAttribute('data-theme') === 'dark';
      if (isDark) {
        root.removeAttribute('data-theme');
        localStorage.setItem('theme', 'light');
      } else {
        root.setAttribute('data-theme', 'dark');
        localStorage.setItem('theme', 'dark');
      }
    });
  });
})();
</script>

Critical: apply the theme before content paints. If you wait for DOMContentLoaded, the page flashes white then goes dark. Run the localStorage check inline in <head>.

Step 4 — Respect system preference

The script above defaults to OS preference (prefers-color-scheme: dark) when no choice is stored. A user who's set their entire OS to dark mode shouldn't have to toggle here.

Add a third state (auto):

// stored values: "dark", "light", or null (auto = follow OS)
// On click, cycle through: auto → light → dark → auto

For most sites, two states (light and dark with system-respecting default) is enough.

Step 5 — Images, icons, and screenshots

Dark mode breaks images that were designed for light backgrounds. Patterns:

  • Logos: ship two versions (light + dark) and swap in CSS:
    .logo { content: url('logo-light.svg'); }
    [data-theme="dark"] .logo { content: url('logo-dark.svg'); }
    
  • Inline SVG icons: use currentColor for stroke/fill. They inherit text color, work in both modes.
  • Screenshots: hard. Either show different screenshots per mode (file size penalty) or accept that screenshots stay as-is.

Step 6 — Things you can't theme

Some elements ignore your dark mode:

  • Stripe Elements / Payment Element iframes — Stripe styles them. Pass dark-mode params via Stripe's Appearance API.
  • PayPal smart buttons — limited theming options.
  • WHMCS-shipped iframes (rare) — may need DOM intervention.

For Stripe specifically:

const appearance = {
  theme: document.documentElement.getAttribute('data-theme') === 'dark' ? 'night' : 'stripe',
  variables: {
    colorPrimary: '#6366f1',
    colorBackground: getComputedStyle(document.documentElement).getPropertyValue('--bg-card'),
  }
};
elements = stripe.elements({ appearance });

How to verify it works

  1. Toggle between light and dark on every key page: dashboard, services, billing, ticket, login. No flash, no broken contrast.
  2. Reload the page — your choice persists.
  3. Test in different browsers (Safari handles prefers-color-scheme slightly differently).
  4. Check accessibility contrast in dark mode using browser DevTools or WebAIM contrast checker. Target WCAG AA at minimum.
  5. Print preview — dark mode should not apply to print stylesheet.

Common pitfalls

"Flash of light mode on every page load." Theme detection runs after content paints. Move the localStorage check to an inline script in <head>.

"Dark mode doesn't apply to admin area." The admin area uses a different theme (/admin/templates/). Apply the same approach there, or accept that admin stays in one mode.

"Stripe Payment Element looks broken in dark mode." You're using static appearance config that doesn't update on theme change. Re-mount Elements when toggling, or use Stripe's Appearance API with CSS variables.

"Colors look great on monitor, washed out on phone." Phones often have different color profiles. Test on real devices, not emulators.

My take — ship it, but don't over-engineer

  1. Two themes (light + dark) is enough. Don't ship a "sepia" or "high contrast" mode unless you have specific accessibility requirements.
  2. Match the customer's OS preference by default. Don't force light mode at 11 PM.
  3. Test once a quarter; CSS drift happens and dark mode catches it first (always a contrast issue somewhere).

Going further


I add dark mode to WHMCS themes for hosting brands — the CSS architecture, the toggle, the third-party integrations like Stripe Elements. Tell me about your theme 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.