Setting up a dark/light theme switch in 1C-Bitrix

Our company is engaged in the development, support and maintenance of Bitrix and Bitrix24 solutions of any complexity. From simple one-page sites to complex online stores, CRM systems with 1C and telephony integration. The experience of developers is confirmed by certificates from the vendor.
Our competencies:
Development stages

Setting Up a Dark/Light Theme Toggle for 1C-Bitrix

A theme toggle is a small UI component — a button or switch in the site header that changes the theme when clicked. The task is solved entirely on the frontend and requires no changes to Bitrix PHP code, only correct integration into the site template.

Toggle HTML Markup

A simple toggle with sun and moon icons:

<button
    id="theme-toggle"
    class="theme-toggle"
    aria-label="Switch theme"
    title="Switch theme"
>
    <svg class="icon-sun" viewBox="0 0 24 24" width="20" height="20">
        <!-- sun icon -->
    </svg>
    <svg class="icon-moon" viewBox="0 0 24 24" width="20" height="20">
        <!-- moon icon -->
    </svg>
</button>

CSS for states:

.theme-toggle .icon-moon { display: none; }
.theme-toggle .icon-sun  { display: block; }

:root[data-theme="dark"] .theme-toggle .icon-moon { display: block; }
:root[data-theme="dark"] .theme-toggle .icon-sun  { display: none; }

Switching JavaScript

const toggle = document.getElementById('theme-toggle');

toggle.addEventListener('click', () => {
    const current = document.documentElement.getAttribute('data-theme');
    const next = current === 'dark' ? 'light' : 'dark';

    document.documentElement.setAttribute('data-theme', next);
    localStorage.setItem('theme', next);

    // If theme needs to be saved server-side for authenticated users:
    if (window.BX && BX.ajax) {
        BX.ajax.post('/local/ajax/save-theme.php', {theme: next});
    }
});

Embedding in the Bitrix Template

In the site template's header.php, place the button at the desired location in the header. The inline theme detection script (to prevent FOUC) must come first in <head>:

<!-- In <head>, before loading styles -->
<script>
(function() {
    var t = localStorage.getItem('theme')
        || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
    document.documentElement.setAttribute('data-theme', t);
    document.documentElement.classList.add('theme-ready');
})();
</script>

The theme-ready class helps disable transition animations on first load:

/* Disable animation before theme is ready */
html:not(.theme-ready) * { transition: none !important; }

Smooth Theme Transition

To avoid an abrupt theme switch:

body,
body *:not(.no-transition) {
    transition: background-color 0.2s ease, color 0.2s ease,
                border-color 0.2s ease, box-shadow 0.2s ease;
}

Only add transitions for specific colour CSS properties — not transition: all, otherwise content animations will be sluggish.

Saving the Theme for Authenticated Users

For authenticated users, the theme can be stored in the profile so it is applied immediately during server rendering without a flash:

// /local/ajax/save-theme.php
if ($USER->IsAuthorized()) {
    $theme = in_array($_POST['theme'], ['dark', 'light']) ? $_POST['theme'] : 'light';
    CUser::Update($USER->GetID(), ['UF_THEME' => $theme]);
}

In header.php:

<?php
$savedTheme = 'light';
if ($USER->IsAuthorized()) {
    $userFields = CUser::GetByID($USER->GetID())->Fetch();
    $savedTheme = $userFields['UF_THEME'] ?? 'light';
}
?>
<html data-theme="<?= htmlspecialchars($savedTheme) ?>">

In this case, the inline script in <head> is still needed — it overrides the server value with the localStorage preference for unauthenticated users.

Stage Time
Toggle markup and styles 1–2 h
JS switching logic + localStorage 1–2 h
FOUC prevention (inline script) 1 h
Smooth transitions (CSS transitions) 1 h
Saving in the user profile (optional) 2–3 h