Using CSS variables to change a page style without reloading

Logo

Introduction

When designing a web site, avoiding pages reloads to update data or styles is often a prerequisite.

CSS variables are very helpful to change a page style without reloading the page.

In this paper, the toggle button in the page’s header which allow to switch from day style to night style is explained : mainly CSS variables, a cookie variable and very few Javascript code.

The toggle button

The toggle button is a checkbox, that’s all. How to design the toggle button is not the goal here, this excellent paper describes well this topic : Dev.to, Danielpdev - Build a toggle button with only HTML and CSS

                       <div class="toggle-container">
              <input type="checkbox">
              <div class="slider round"></div>
            </div>
          

CSS variables

As much as possible, in the main stylesheet, background and foreground colors for elements are not hard coded, CSS variables are used (only the relevant properties are displayed below for brevity) :

style.css
body {
  color: var(--wrap-fg-color);
  background-color: var(--page-bg-color);
}

div[id="wrap"] {
  background-color: var(--wrap-bg-color);
}

h1, h2, h3, h4, h5 {
  font-family: var(--heading-font-family);
  color: var(--heading-fg-color);
}

a { color: var(--anchor-color); }

section { border-color: rgb(var(--box-bg-color)); }

section h2 {
    background-color: rgb(var(--box-bg-color));
    color: var(--box-fg-color);
    opacity: var(--box-hdr-opacity);
}

Variables are defined in the stylesheet’s header (section :root) :

style.css
:root {

  --page-bg-color: var(--c-page-bg-color,#aaabad);
  --wrap-bg-color: var(--c-wrap-bg-color,#FFFFFF);
  --wrap-fg-color: var(--c-wrap-fg-color,#575757);
  
  --heading-fg-color:    var(--c-heading-fg-color,#808080);
  --heading-font-family: Sansation, "Century Gothic", Verdana, Arial, sans-serif;
  
  --anchor-color:  var(--c-anchor-color,#2a799e);
  
  --sys-box-bg-color: 168, 132, 72;
  
  --box-bg-color:    var(--c-box-bg-color,var(--sys-box-bg-color));
  --box-fg-color:    #FFFFFF;
  --box-hdr-opacity: var(--c-box-hdr-opacity,0.6);

}

body { … }
…

In the sample syntax below : the variable value --anchor-color is the variable value --c-anchor-color if exists, otherwise the fallback value #2a799e. The nomenclature --c- stands for custom.

--anchor-color:  var(--c-anchor-color,#2a799e);

So 2 stylesheets are prepared to define the --c-% variables :

style-default.css (day mode)
:root {
 --c-page-bg-color: #aaabad;
 --c-wrap-bg-color: #FFFFFF;
 --c-wrap-fg-color: #575757;
 --c-heading-fg-color: #808080;
 --c-anchor-color: #2a799e;
 --c-box-hdr-opacity: 0.6;
}
style-okaidia.css (night mode)
:root {
 --c-page-bg-color: #596673; 
 --c-wrap-bg-color: #212529;
 --c-wrap-fg-color: #d9d9d9;
 --c-heading-fg-color: #d9d9d9; 
 --c-anchor-color: #7299df;
 --c-box-hdr-opacity: 0.7;
}

Depending on the user’s choice, the appropriate stylesheet is loaded before the main one. To manage later the custom stylesheet using Javascript, an identifier is applied.

day mode
<head>
 …
 <link href="/css/custom/style-default.css" rel="stylesheet" id="customStyle">
 <link href="/css/style.css" rel="stylesheet">
 …
</head>
night mode
<head>
 …
 <link href="/css/custom/style-okadia.css" rel="stylesheet" id="customStyle">
 <link href="/css/style.css" rel="stylesheet">
 …
</head>

The checkbox action

On the checkbox element, a listener is added on the click event when the document content is loaded (DOMContentLoaded event) :

<script>
    setCustomStyle=function(customStyle) {…};
    
    window.addEventListener('DOMContentLoaded', function() {
      cb = document.querySelector('div[class*="toggle-container"] > input[type="checkbox"]');
      if (cb !==null ) {
        cb.addEventListener('click',function() {
                st = (this.checked) ? 'okaidia' : 'default';
                setCustomStyle(st);
        });
      }
    });        
</script>

The function setCustomStyle will :

  • set the cookie variable customStyle to okaidia or default.
  • remove the existing custom stylesheet and add the selected one in the DOM tree, forcing thus variables evaluation and the page to be repaint.
setCustomStyle=function(customStyle) {

  /** Set Cookie value */
  let d = new Date();
  let expiredays=60;
	d.setTime(d.getTime() + (expiredays*24*60*60*1000));
	let expires = 'expires='+ d.toUTCString();
	document.cookie = `customStyle=${customStyle};${expires};path=/`;
  
  /** Replace custom stylesheet */
  if (document.querySelector('link[id="customStyle"]')) {
    document.head.removeChild(document.querySelector('link[id="customStyle"]'));
  }
  lk = document.createElement('link');
  lk.setAttribute('id','customStyle');
  lk.setAttribute('rel','stylesheet');
  lk.setAttribute('href',`/css/custom/style-${customStyle}.css`);
  document.head.appendChild(lk);

};

That’s all ? Not yet, 2 remaining details to manage :

  • Keeping the user’s choice if the user reloads or visits the page again.
  • PrismJS syntax highlighter adjustment in this page.

Keeping the user’s choice

The custom stylesheet should be added dynamically before the main stylesheet : in this context, the appropriate custom stylesheet is loaded if the user reloads or visits the page again, thus the user’s preference is kept.

The necessary piece of code checks if the cookie variable customStyle exists. If not exists, the default custom stylesheet is loaded, otherwise the custom stylesheet according to the cookie variable value.

Using PHP and the global variable array $_COOKIE :

<head>
 …
    <?php
      $customStyle = isset($_COOKIE['customStyle']) ? $_COOKIE['customStyle'] : 'default';
      echo "<link id=\"customStyle\" href=\"/css/custom/style-".$customStyle.".css\" rel=\"stylesheet\">";       
    ?>

 <link href="/css/style.css" rel="stylesheet">
 …
</head>

Using Javascript in synchronous mode (a little bit more complex if no generic function to decode cookie variables exists) :

<head>
 …
 <script>
      cookie_get=function(cn) {
        let decodedCookie = decodeURIComponent(document.cookie).replace(new RegExp('; ', 'g'),';');
        let ca = decodedCookie.split(';').filter(function(cookie) { return cookie.split('=')[0] == cn; });
        if (ca.length == 1) { return ca[0].split('=')[1]; }
        return false;	 
      };
      
      var customStyle = (cookie_get('customStyle')) ? cookie_get('customStyle') : 'default';
      lk=document.createElement('link');
      lk.setAttribute('rel','stylesheet');
      lk.setAttribute('id','customStyle');
      lk.setAttribute('href',`/css/custom/style-${customStyle}.css`);
      document.head.appendChild(lk);
 </script>

 <link href="/css/style.css" rel="stylesheet">
 …
</head>

Now the checkbox is checked if the style is not default.

PHP :

                       <div class="toggle-container">
              <input type="checkbox" <?php if (isset($customStyle) && $customStyle != 'default') { echo " checked "; } ?> >
              <div class="slider round"></div>
            </div>
          

Javascript :

<div class="toggle-container">
    <input type="checkbox">
    <div class="slider round"></div>
</div>
<script>
    if (typeof(customStyle) != 'undefined' && customStyle !=='default') {
        document.querySelector('div[class*="toggle-container"] > input[type="checkbox"]').checked=true;
    }
</script>

PrismJS syntax highlighter adjustment

The stylesheets names default and okaidia have not been randomly chosen. They match PrismJS syntax highlighter CSS file names.

PrismJS stylesheets (prism.default.css, prism.okaidia.css…) are installed in /common/external/prism, so let’s enhance the function code to also update PrismJS CSS stylesheet :

setCustomStyle=function(customStyle) {

  /** Set Cookie value */
  …

  /** Replace custom stylesheet */
  if (document.querySelector('link[id="customStyle"]')) {
    document.head.removeChild(document.querySelector('link[id="customStyle"]'));
  }
  lk = document.createElement('link');
  lk.setAttribute('id','customStyle');
  lk.setAttribute('rel','stylesheet');
  lk.setAttribute('href',`/css/custom/style-${customStyle}.css`);
  document.head.appendChild(lk);
  
  /** Update PrismJS stylesheet */
  cprism=document.querySelector(`link[rel="stylesheet"][href*="/common/external/prism/prism"]`);
  if (cprism !== null) {
    document.head.removeChild(cprism);
    lkp = document.createElement('link');
    lkp.setAttribute('rel','stylesheet');
    lkp.setAttribute('href',`/common/external/prism/prism.${customStyle}.css`);
    document.head.appendChild(lkp);
  }
  
};

Obviously, the right PrismJS CSS stylesheet is loaded if the user reloads or visits again the page depending on the cookie variable customStyle if exists.