Darkmode checkbox input get checked only from the second click

Viewed 611

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>
4 Answers

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.

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>

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');
    }
}

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).
Related