Also there are other problems with the Nightmode in my opinion. The fact that the server doesn’t know the state of it, results in flickering of the default mode whenever the user refreshes the page. This wouldn’t happen if it the information was saved either as a cookie or for logged in users inside the user_meta.
I already implemented a working nightmode as a Bricks Element that uses Cookies and User Meta and doesn’t suffer from flashing the wrong theme in the first few frames. Feel free to take inspiration from it.
Bricks Element
<?php
if (!defined('ABSPATH')) exit;
use Userfreunde\Ajax\Nightmode;
use Userfreunde\Helpers;
class UF_Nightmode extends Userfreunde\Bricks\Element
{
public $category = self::CATEGORY_SPECIAL;
public $name = 'uf-nightmode';
public $icon = 'ion-md-moon';
public $tag = 'button';
// MARK: Get Label
public function get_label()
{
return esc_html__('Nightmode', 'uf-theme');
}
// MARK: Get Keywords
public function get_keywords()
{
return ['mode', 'nightmode', 'switcher', 'dark', 'theme', 'light'];
}
// MARK: Set Controls
public function set_controls()
{
$this->controls['cookieExpiry'] = [
'label' => esc_html__('Cookie Expiry', 'uf-theme'),
'type' => 'number',
'units' => false,
'min' => 1,
'max' => 365,
'step' => 1,
'default' => 365,
'placeholder' => esc_html__('365', 'uf-theme'),
];
$this->controls['respectSystem'] = [
'label' => esc_html__('Respect System Preference', 'uf-theme'),
'type' => 'checkbox',
'default' => true,
];
}
// MARK: Enqueue Scripts
public function enqueue_scripts()
{
if (bricks_is_builder_main()) return;
wp_enqueue_style($this->name, $this->cssURL, [$this->themeStyles], filemtime($this->cssPath));
if (bricks_is_frontend()) {
wp_enqueue_script($this->name, $this->jsURL, [$this->themeScripts], filemtime($this->jsPath), true);
// Get the current preference from server
$current_preference = Nightmode::get_preference();
wp_localize_script($this->name, 'ufNightmode', [
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('uf-nightmode-nonce'),
'initial_mode' => $current_preference,
'is_user_logged_in' => is_user_logged_in(),
'has_saved_preference' => $this->has_saved_preference(),
]);
}
}
// MARK: Render
public function render()
{
$selector = $this->name;
$root_class[] = $selector;
$settings = $this->settings;
$cookie_expiry = !empty($settings['cookieExpiry']) ? intval($settings['cookieExpiry']) : 365;
$respect_system = isset($settings['respectSystem']) ? 'true' : 'false';
$nightmode = Nightmode::get_preference();
$this->set_attribute('_root', 'class', $root_class);
$this->set_attribute('_root', 'data-mode', esc_attr($nightmode));
$this->set_attribute('_root', 'data-respect-system', $respect_system);
$this->set_attribute('_root', 'data-cookie-expiry', $cookie_expiry);
$this->set_attribute('_root', 'aria-label', esc_attr__('Toggle Nightmode', 'uf-theme'));
$svgSun = Helpers::file_get_contents(dirname(__FILE__) . '/svg/lucide-sun.svg');
$svgMoon = Helpers::file_get_contents(dirname(__FILE__) . '/svg/lucide-moon.svg');
$svgSun = $this->render_svg($svgSun, ['class' => 'sun']);
$svgMoon = $this->render_svg($svgMoon, ['class' => 'moon']);
$output = "<{$this->tag} {$this->render_attributes('_root')}>";
$output .= $svgSun;
$output .= $svgMoon;
$output .= "</{$this->tag}>";
echo $output;
}
// MARK: Check if user has saved preference
private function has_saved_preference()
{
if (is_user_logged_in()) {
$user_id = get_current_user_id();
$preference = get_user_meta($user_id, Nightmode::$user_meta_key, true);
return !empty($preference);
} else {
return isset($_COOKIE['uf_nightmode']);
}
}
}
Ajax Class:
<?php
namespace Userfreunde\Ajax;
if (! defined('ABSPATH')) exit; // Exit if accessed directly
class Nightmode
{
public static $default_mode = 'light';
public static $user_meta_key = 'uf_nightmode_preference';
public function __construct()
{
add_action('wp_ajax_uf_set_nightmode', [$this, 'toggle']);
add_action('wp_ajax_nopriv_uf_set_nightmode', [$this, 'toggle']);
add_filter('bricks/body/attributes', [$this, 'modify_body_attr']);
}
/**
* MARK: Toggle
*/
public function toggle(): void
{
if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'uf-nightmode-nonce')) {
wp_send_json_error('Invalid nonce');
}
$mode = isset($_POST['mode']) ? sanitize_text_field($_POST['mode']) : self::$default_mode;
// Validate mode value
if (!in_array($mode, ['light', 'dark'])) {
$mode = self::$default_mode;
}
if (is_user_logged_in()) {
$this->set_user_preference($mode);
wp_send_json_success([
'message' => esc_html__('Nightmode preference saved (user)', 'uf-theme'),
'mode' => $mode
]);
return;
}
// Set cookie with proper parameters to match JavaScript
setcookie('uf_nightmode', $mode, [
'expires' => time() + (365 * DAY_IN_SECONDS), // Match the default 365 days from JS
'path' => '/',
'samesite' => 'Lax'
]);
wp_send_json_success([
'message' => esc_html__('Nightmode preference saved (cookie)', 'uf-theme'),
'mode' => $mode
]);
}
/**
* MARK: Modify Body Attribute
*/
public function modify_body_attr(array $attributes): array
{
$preference = self::get_preference();
$attributes['data-nightmode'] = $preference;
return $attributes;
}
/**
* MARK: Get Preference
*/
public static function get_preference(): string
{
if (is_user_logged_in()) {
$preference = self::get_user_preference();
} else if (isset($_COOKIE['uf_nightmode']) && in_array($_COOKIE['uf_nightmode'], ['dark', 'light'])) {
$preference = $_COOKIE['uf_nightmode'];
} else {
$preference = self::$default_mode;
}
return $preference;
}
/**
* MARK: Get User Preference
*/
public static function get_user_preference(): string
{
$user_id = get_current_user_id();
$preference = get_user_meta($user_id, self::$user_meta_key, true);
if (empty($preference) || !in_array($preference, ['light', 'dark'])) {
$preference = self::$default_mode;
}
return $preference;
}
/**
* MARK: Set User Preference
*/
public function set_user_preference(string $mode): void
{
$user_id = get_current_user_id();
update_user_meta($user_id, self::$user_meta_key, $mode);
}
}
Javascript
class UfNightmode {
constructor(element) {
this.element = element;
this.selector = element.dataset.selector || 'uf-nightmode';
this.body = document.body;
this.currentMode = 'light';
this.init();
}
init() {
this.loadInitialMode();
this.setupEventListeners();
}
loadInitialMode() {
// Use server-provided initial mode
if (typeof ufNightmode !== 'undefined' && ufNightmode.initial_mode) {
this.setMode(ufNightmode.initial_mode);
} else {
// Fallback to client-side detection
this.checkSystemPreference();
this.checkCookie();
}
}
/**
* Cache DOM elements for performance
*/
cacheElements() {
// Elements are queried globally for this special component
this.buttons = document.querySelectorAll(`.${this.selector}`);
}
setupEventListeners() {
this.element.addEventListener('click', () => this.toggleMode());
}
checkSystemPreference() {
// Only check if no saved preference and element allows it
if (typeof ufNightmode !== 'undefined' && !ufNightmode.has_saved_preference) {
const respectSystem = this.element.getAttribute('data-respect-system') === 'true';
if (respectSystem && window.matchMedia?.('(prefers-color-scheme: dark)').matches) {
this.setMode('dark');
}
}
}
checkCookie() {
// Only check cookie if not logged in and no server preference
if (
typeof ufNightmode !== 'undefined' &&
!ufNightmode.is_user_logged_in &&
!ufNightmode.initial_mode
) {
const mode = this.getCookie('uf_nightmode');
if (mode && (mode === 'light' || mode === 'dark')) {
this.setMode(mode);
}
}
}
toggleMode() {
const newMode = this.currentMode === 'light' ? 'dark' : 'light';
const cookieExpiry = this.element.getAttribute('data-cookie-expiry') || 365;
// Set cookie if user is not logged in
if (typeof ufNightmode === 'undefined' || !ufNightmode.is_user_logged_in) {
this.setCookie('uf_nightmode', newMode, cookieExpiry);
}
this.setMode(newMode);
this.sendAjaxRequest(newMode);
}
setMode(mode) {
this.currentMode = mode;
// Update body (data attribute handles styling)
this.body.setAttribute('data-nightmode', mode);
// Update all buttons globally
document.querySelectorAll(`.${this.selector}`).forEach((button) => {
button.setAttribute('data-mode', mode);
});
// Dispatch custom event
document.dispatchEvent(
new CustomEvent('ufNightmodeChanged', {
detail: { mode },
bubbles: true,
})
);
}
setCookie(name, value, days) {
const date = new Date();
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
const expires = `; expires=${date.toUTCString()}`;
document.cookie = `${name}=${value}${expires}; path=/; SameSite=Lax`;
}
getCookie(name) {
const nameEQ = `${name}=`;
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
cookie = cookie.trim();
if (cookie.indexOf(nameEQ) === 0) {
return cookie.substring(nameEQ.length);
}
}
return null;
}
sendAjaxRequest(mode) {
if (typeof ufNightmode === 'undefined' || !ufNightmode.ajax_url) return;
const formData = new FormData();
formData.append('action', 'uf_set_nightmode');
formData.append('mode', mode);
formData.append('nonce', ufNightmode.nonce);
fetch(ufNightmode.ajax_url, {
method: 'POST',
body: formData,
})
.then((response) => response.json())
.then((data) => {
if (!data.success) {
console.error('Error saving nightmode preference:', data.data);
}
})
.catch((error) => {
console.error('Error saving nightmode preference:', error);
});
}
}
// MARK: Initialization
const ufNightmodeFn = new BricksFunction({
parentNode: document,
selector: '.uf-nightmode',
eachElement: (element) => {
if (element.dataset.initialized === 'true') return;
new UfNightmode(element);
element.dataset.initialized = 'true';
},
});
function ufNightmode() {
ufNightmodeFn.run();
}
ufNightmode();
Also some Nightmodes respect User Preferences (which some people set dynamically depending on the hour of the day). this would also be a need feature, but would require a Select-Input instead of a switch
I didn’t get to implement it yet aswell..
Best Regards
Suat