2

When running on a browser, the switch below activates/disactivates darkmode from the first click, but get checked/un-checked only from the second click.

** Originally I used the checked property only and later added the checked attribute to try and fix the issue (as you can see- it didn't help). It acts the same also in case the JavaScript handles the checked attribute only.

The purpose is to have it work in a multiple pages site (hence the localStorage involvment).

I failed to understand why it happens.

Any suggestions?

all-pages.js:

const htmlTag = document.querySelector('html');
const switchBtn = document.querySelector('.slider');
const darkCheckbox = document.querySelector('.dark-cb');

function darkMode() {
  if (localStorage.getItem('dark') == 'true') {
    darkCheckbox.checked = true;
    darkCheckbox.setAttribute('checked', 'checked');
    htmlTag.classList.add('dark');
    switchBtn.classList.add('dark');
  } else {
    darkCheckbox.checked = false;
    darkCheckbox.removeAttribute('checked');
    htmlTag.classList.remove('dark');
    switchBtn.classList.remove('dark');
  }
}

switchBtn.addEventListener('click', function() {
  localStorage.setItem('dark', htmlTag.className.indexOf('dark') == -1);
  darkMode();
});


window.onload = darkMode();

style.css:

html {
  min-height: 100%;
}

.switch {
  position: absolute;
  top: 10px;
  left: 20px;
  width: 60px;
  height: 34px;
}


/* Hide default HTML checkbox */

.switch input {
  opacity: 0;
  width: 0;
  height: 0;
}


/* The slider */

.slider {
  position: absolute;
  cursor: pointer;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: skyblue;
  -webkit-transition: .4s;
  transition: .4s;
}

.slider::before {
  position: absolute;
  content: "";
  height: 26px;
  width: 26px;
  left: 4px;
  bottom: 4px;
  background-color: #FFFF66;
  -webkit-transition: .4s;
  transition: .4s;
}

input:checked+.slider {
  background-color: darkblue;
}

input:focus+.slider {
  box-shadow: 0 0 1000px darkblue;
}

input:checked+.slider::before {
  -webkit-transform: translateX(26px);
  -ms-transform: translateX(26px);
  transform: translateX(26px);
}


/* Rounded sliders */

.slider.round {
  border-radius: 34px;
}

.slider.round::before {
  border-radius: 50%;
}

.dark {
  filter: invert(1);
}

index.html:

<html lang="en">

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

  <body>
    <nav id="nav-bar">
      <label class="switch">
            <input type="checkbox" class='dark-cb'>
            <span class="slider round" title="light/dark mode"></span>
          </label>
    </nav>
    <script src="all-pages.js"></script>
  </body>

</html>
Yaniv Aflalo
  • 239
  • 2
  • 11

4 Answers4

1

IF I understood correctly you are having problems with the fancy checkbox appearing to be checked upon first clicking but the toggling of classes to the HTML element etc works fine. To that end I hope the following might prove useful.

Because of the restrictions imposed here with Snippets not being able to access localStorage I cannot create a snippet to demonstrate but perhaps this jsFiddle will help illustrate

<html lang="en">
    <head>
        <style>
            /* variables */
            :root{
                --w:2rem;
                --h:2rem;
                --l:0rem;
                --t:0rem;
                --r:10rem;
                
                --sw:5rem;
                --sh:2rem;
                
                --pt:1rem;
                --pl:2rem;
            }


            html {
                min-height: 100%;
            }
            .switch {
                position:absolute;
                
                top:var(--pt);
                left:var(--pl);
                width:var(--sw);
                height:var(--sh);
            }


            /* Hide default HTML checkbox */
            .switch input {
                opacity:1;
                width:2rem;
                height:10rem;
            }
            .switch input {
              opacity: 0;
              width: 0;
              height: 0;
            }

            /* The slider */
            .slider {
                position:absolute;
                cursor:pointer;
                
                top:0;
                left:0;
                right:0;
                bottom:0;
                
                background-color:skyblue;
                transition:0.4s;
            }

            .slider::before {
                position:absolute;
                content:'';
                
                height:var(--h);
                width:var(--w);
                left:var(--l);
                bottom:var(--t);
                
                background-color:#FFFF66;
                transition:0.4s;
            }

            input:checked + .slider {
                background-color:darkblue;
            }

            input:focus + .slider {
                box-shadow:0 0 1000px darkblue;
            }

            input:checked + .slider::before {
                transform:translateX( calc( var(--sw) - var(--w) ) );
            }


            /* Rounded sliders */
            .slider.round {
                border-radius:var(--r);
            }
            .slider.round::before {
                border-radius:var(--r);
            }
            .dark {
                filter:invert(1);
            }
            
            /* debug */
            html.dark{
                background:gray;
                color:white;
            }
        </style>
    </head>
    <body>
        <nav id='nav-bar'>
            <label class='switch'>
                <input type='checkbox' name='dark-cb' class='dark-cb'>
                <span class='slider round' title='light/dark mode'></span>
            </label>
        </nav>
        <script>
      
            document.addEventListener('DOMContentLoaded',()=>{
                const store='darkmode_toggle';
                const cn='dark';
                
                const html=document.querySelector('html');
                const bttn=document.querySelector('.slider');
                const chk=document.querySelector('input[type="checkbox"][name="dark-cb"]');
                
                
                function darkMode() {
                    // get the stored value. This will be null if it does not exist.
                    let i=localStorage.getItem(store);
                    
                    // set the initial value if null.
                    if( i==null ){
                        localStorage.setItem(store,0);
                        i=Number(localStorage.getItem(store));
                    }else i=Number(i);
                    
                    
                    // if `darkMode` was invoked by a `click` event - toggle the saved value
                    if( typeof( arguments[0] )!='undefined' && arguments[0].type=='click' ){
                        localStorage.setItem( store, 1 - i );
                        i=Number(localStorage.getItem(store));
                    }
                    
                    
                    // set attributes and classes as appropriate to current state
                    switch(i){
                        case 1:
                            html.classList.add(cn)
                            bttn.classList.add(cn);
                            
                            // force the checkbox state on page load only
                            if( typeof( arguments[0] )=='undefined' )chk.checked=1;
                        break;
                        case 0:
                            html.classList.remove(cn)
                            bttn.classList.remove(cn);
                            if( html.classList.length==0 )html.removeAttribute('class');
                            if( bttn.classList.length==0 )bttn.removeAttribute('class');
                            
                            // force the checkbox state on page load only
                            if( typeof( arguments[0] )=='undefined' )chk.checked=0;
                        break;
                    }
                }

                
                bttn.addEventListener('click', darkMode );
                darkMode();
                
            });
        </script>
    </body>
</html>

Modified the darkMode function so that the checking / unchecking of the checkbox programmatically nowonly happens when there is no MouseEvent/Click. Also tweaked the CSS to use variables so that the layout can be modified quickly and programmatically if required.

Professor Abronsius
  • 33,063
  • 5
  • 32
  • 46
  • Hi, thanks! In your solution it works on a **single page** site. However, the purpose is to have it work in a **multiple pages** site (hence the `localStorage` involvment). If testing on such a site- after moving to another page the darkmode toggles onload and then only toggles the darkmode from the second click. (the switch get checked/un-checked from the fist click as needed). – Yaniv Aflalo Apr 07 '21 at 18:18
  • it should not `toggle` on page load. How can I emulate this effect to test for myself? Have a 2nd page with the same code in? – Professor Abronsius Apr 07 '21 at 18:37
  • 1
    just tested with 2 pages - have seen a small issue. Will look at this tomorrow ~ seems like an easy fix – Professor Abronsius Apr 07 '21 at 18:44
  • I think that is now resolved. Yesterday I could not see the wood for the trees as they say.. the obvious think I missed was the setting of the checkbox in the `darkMode` function... that should only have been called when there was no `click` because the actual physical click set the state of the checkbox anyway. – Professor Abronsius Apr 08 '21 at 08:43
1

Ok. I see what you mean. Please try this one:

    <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Dark/Light Mode Switch</title>
    <style>
        :root {
            --primary-color: #302AE6;
            --secondary-color: #536390;
            --font-color: #424242;
            --bg-color: #fff;
            --heading-color: #292922;
        }

        [data-theme="dark"] {
            --primary-color: #9A97F3;
            --secondary-color: #818cab;
            --font-color: #e1e1ff;
            --bg-color: #161625;
            --heading-color: #818cab;
        }

        html {
            min-height: 100%;
        }

        body {
            background-color: var(--bg-color);
            color: var(--font-color);
            max-width: 90%;
            margin: 0 auto;
            font-size: calc(1rem + 0.25vh);
        }


        .switch {
            position: absolute;
            top: 10px;
            left: 20px;
            width: 60px;
            height: 34px;
        }


        /* Hide default HTML checkbox */

        .switch input {
            opacity: 0;
            width: 0;
            height: 0;
        }


        /* The slider */

        .slider {
            position: absolute;
            cursor: pointer;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background-color: #044660;
            -webkit-transition: .4s;
            transition: .5s;
        }

        .slider::before {
            position: absolute;
            content: "";
            height: 26px;
            width: 26px;
            left: 4px;
            bottom: 4px;
            background-color: #FFFF66;
            -webkit-transition: .4s;
            transition: .4s;
        }

        input:checked + .slider {
            background-color: darkblue;
        }

        input:focus + .slider {
            box-shadow: 0 0 1000px darkblue;
        }

        input:checked + .slider::before {
            -webkit-transform: translateX(26px);
            -ms-transform: translateX(26px);
            transform: translateX(26px);
        }


        /* Rounded sliders */

        .slider.round {
            border-radius: 24px;
        }

        .slider.round::before {
            border-radius: 50%;
        }

        .dark {
            filter: invert(1);
        }
    </style>
</head>
<body>

<nav id='nav-bar'>
    <label class='switch' for="checkbox">
        <input type='checkbox' name='checkbox' id="checkbox" class='dark-cb'>
        <span class='slider round' title='Enable Dark Mode!'></span>
    </label>
</nav>

<div style="margin: 100px">
    Testing Dark/Light Mode Switch
</div>


<script>
    const toggleSwitch = document.querySelector('.switch input[type="checkbox"]');

    function switchTheme(e) {
        if (e.target.checked) {
            document.documentElement.setAttribute('data-theme', 'dark');
            localStorage.setItem('theme', 'dark'); //add this
        } else {
            document.documentElement.setAttribute('data-theme', 'light');
            localStorage.setItem('theme', 'light'); //add this
        }
    }

    const currentTheme = localStorage.getItem('theme') ? localStorage.getItem('theme') : null;

    if (currentTheme) {
        document.documentElement.setAttribute('data-theme', currentTheme);

        if (currentTheme === 'dark') {
            toggleSwitch.checked = true;
        }
    }

    toggleSwitch.addEventListener('change', switchTheme, false);
</script>

</body>
</html>
Ali Safari
  • 1,535
  • 10
  • 19
  • Thank you. This works, however it is too far from my current structure and will require too many canges to HTML, CSS and JS. I am trying to find the root cause to the issue – Yaniv Aflalo Apr 08 '21 at 07:58
  • The reason was that your js code doesn't initialise the first window loaded condition of checkbox. – Ali Safari Apr 08 '21 at 22:59
0

Change your darkMode function to the one below. Then your problem is solved!

function darkMode() {
    if (localStorage.getItem('dark') === 'false' ) {
        darkCheckbox.checked = false;
        darkCheckbox.removeAttribute('checked');
        htmlTag.classList.remove('dark');
        switchBtn.classList.remove('dark');
    } else {
        darkCheckbox.checked = true;
        darkCheckbox.setAttribute('checked', 'checked');
        htmlTag.classList.add('dark');
        switchBtn.classList.add('dark');
    }
}
Ali Safari
  • 1,535
  • 10
  • 19
  • Thanks for the answer, but I don't see any effect by the suggsted change :\ – Yaniv Aflalo Apr 07 '21 at 12:54
  • I have tested it on my PC and it works as you wished. I checked again and it works well. Refresh the page and try it again please. You should see the reaction with the first click on the button, not with a double click like before the change. – Ali Safari Apr 07 '21 at 17:42
  • Did you copy paste the modified function on the original one? – Ali Safari Apr 07 '21 at 17:43
  • Yes. Please note that the purpose is to have it work in a multiple pages site (hence the `localStorage` involvment). – Yaniv Aflalo Apr 07 '21 at 18:23
0

Some modification solved the issue.

Slightly modified CSS:

.switch input {
  opacity: 0;
  z-index: 100;
  cursor: pointer;
  width: 52px;
  height: 30px;
}

Modified JS:

const htmlTag = document.querySelector('html');
const switchBtn = document.querySelector('.slider');
const darkCheckbox = document.querySelector('.dark-cb');

function darkMode() {
  if (localStorage.getItem('dark') == 'true') {
    darkCheckbox.defaultChecked = true;
    htmlTag.classList.add('dark');
    switchBtn.classList.add('dark');
  } else {
    darkCheckbox.defaultChecked = false;
    htmlTag.classList.remove('dark');
    switchBtn.classList.remove('dark');
  }
}

darkCheckbox.addEventListener('click', function() {
  let isDark = (localStorage.getItem('dark') == 'true') ? 'false' : 'true';
  if (localStorage.getItem('dark') == null) {isDark = 'true';}
  localStorage.setItem('dark',isDark);
  darkMode();
});

window.onload = darkMode;
  1. I changed the localStorage.getItem('dark') value to be accepted by the isDark variable
  2. I used .defaultChecked instead of .checked for the input.
  3. I made the .addEventListnter() be triggered by the darkCheckbox, not switchBtn (that is why I used z-index: 100 on the CSS, to bring the input to the front).
Yaniv Aflalo
  • 239
  • 2
  • 11