Dark/Light Mode with Cookie

A theme toggle button that flips a data-theme-status attribute between "light" and "dark", persisting the choice in a JS-Cookie. Includes a Shift+T keyboard shortcut, CSS icon swap animation, and a localStorage alternative variant.

dark-modelight-modethemecookiejavascriptaccessibilitykeyboard

Setup — External Scripts

Setup: External Scripts
html
<script src="https://cdn.jsdelivr.net/npm/js-cookie@3.0.5/dist/js.cookie.min.js"></script>

Code

index.html
html
<button data-theme-toggle="" class="btn-darklight">
  <div class="btn-darklight__icon">
    <div class="btn-darklight__icon-box">
      <svg xmlns="http://www.w3.org/2000/svg" width="100%" viewbox="0 0 24 24" fill="none"><path d="M15.5355 8.46447C17.4882 10.4171 17.4882 13.5829 15.5355 15.5355C13.5829 17.4882 10.4171 17.4882 8.46447 15.5355C6.51184 13.5829 6.51184 10.4171 8.46447 8.46447C10.4171 6.51184 13.5829 6.51184 15.5355 8.46447Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M12 4V2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M12 22V20" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M18.3599 5.63999L19.0699 4.92999" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M4.93018 19.07L5.64018 18.36" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M20 12H22" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M2 12H4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M18.3599 18.36L19.0699 19.07" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M4.93018 4.92999L5.64018 5.63999" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>
    </div>
    <div class="btn-darklight__icon-box is--absolute">
      <svg xmlns="http://www.w3.org/2000/svg" width="100%" viewbox="0 0 24 24" fill="none"><path d="M18.395 13.027C18.725 12.872 19.077 13.197 18.985 13.55C18.671 14.752 18.054 15.896 17.104 16.846C14.283 19.667 9.77001 19.726 7.02201 16.978C4.27401 14.23 4.33401 9.71601 7.15501 6.89501C8.10501 5.94501 9.24801 5.32801 10.451 5.01401C10.804 4.92201 11.128 5.27401 10.974 5.60401C9.97201 7.74301 10.301 10.305 11.998 12.002C13.694 13.7 16.256 14.029 18.395 13.027Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>
    </div>
  </div>
  <div class="btn-darklight__word">
    <p class="btn-darklight__word-p">Light</p>
    <p class="btn-darklight__word-p is--absolute">Dark</p>
  </div>
  <p class="btn-darklight__word-p">mode</p>
</button>
styles.css
css
.btn-darklight {
  color: #131313;
  cursor: pointer;
  background-color: #efeeec;
  border: 0 solid transparent;
  border-radius: .25em;
  outline: 0 transparent;
  justify-content: center;
  align-items: center;
  padding: 0 1.125em 0 .75em;
  display: flex;
  position: relative;
  overflow: hidden;
}

.btn-darklight__icon {
  width: 1.25em;
  height: 100%;
  margin-right: .25em;
  position: relative;
}

.btn-darklight__word {
  padding-right: .25em;
  position: relative;
}

.btn-darklight__word-p {
  margin-top: .05em;
  margin-bottom: 0;
  line-height: 1.2;
  position: relative;
}

.btn-darklight__word-p.is--absolute {
  opacity: 0;
  letter-spacing: .025em;
  position: absolute;
  top: 0;
}

.btn-darklight__icon-box {
  height: 100%;
  padding-top: .66em;
  padding-bottom: .66em;
  display: flex;
  position: relative;
}

.btn-darklight__icon-box.is--absolute {
  position: absolute;
}

/* Background */
[data-theme-status] {
  transition: background-color 0.4s cubic-bezier(0.35, 1, 0.6, 1);
}

[data-theme-status="dark"] {
  background-color: #070915 !important;
}

/* Button Word */
[data-theme-status="dark"] .btn-darklight .btn-darklight__word .btn-darklight__word-p,
[data-theme-status="light"] .btn-darklight .btn-darklight__word .btn-darklight__word-p.is--absolute {
  opacity: 0;
}

[data-theme-status="light"] .btn-darklight .btn-darklight__word .btn-darklight__word-p,
[data-theme-status="dark"] .btn-darklight .btn-darklight__word .btn-darklight__word-p.is--absolute {
  opacity: 1;
}

/* Button Icon */
.btn-darklight .btn-darklight__icon-box {
  transition: transform 0.8s cubic-bezier(0.35, 1.5, 0.6, 1);
  transform: translateY(0%) rotate(-90deg);
}

[data-theme-status="dark"] .btn-darklight .btn-darklight__icon-box {
  transform: translateY(-100%) rotate(0.001deg);
}
script.js
javascript
function initCookieDarkLight() {

  // Function to toggle theme
  function initThemeCheck() {
    const dashThemeElement = document.querySelector('[data-theme-status]');
    if (!dashThemeElement) return;

    const currentTheme = dashThemeElement.getAttribute('data-theme-status');
    const newTheme = (currentTheme === 'light') ? 'dark' : 'light';

    dashThemeElement.setAttribute('data-theme-status', newTheme);
    Cookies.set('theme', newTheme, { expires: 365 });
  }

  // Shift+T keyboard shortcut
  document.addEventListener('keydown', function(e) {
    const tagName = e.target.tagName.toLowerCase();
    if (tagName === 'input' || tagName === 'textarea' || e.target.isContentEditable) {
      return; // Do nothing if typing into a field
    }

    if (e.shiftKey && e.keyCode === 84) { // Shift+T
      e.preventDefault();
      initThemeCheck();
    }
  });

  // Click handler for all toggle buttons
  document.querySelectorAll('[data-theme-toggle]').forEach(function(button) {
    button.addEventListener('click', initThemeCheck);
  });

  // Restore theme from cookie on load
  if (Cookies.get('theme') === 'dark') {
    const themeElement = document.querySelector('[data-theme-status]');
    if (themeElement) {
      themeElement.setAttribute('data-theme-status', 'dark');
    }
  }
}

// Initialize Cookie Dark/Light Theme
document.addEventListener('DOMContentLoaded', function() {
  initCookieDarkLight();
});

Guide

Attributes

Add data-theme-status="light" to the <body> element to set the initial theme. Use data-theme-toggle on any button or element to wire up the click handler. The script toggles data-theme-status between "light" and "dark" and writes the result to a cookie.

Cookie Persistence

The user's preference is stored in a cookie named "theme" with a 365-day expiry using the JS-Cookie library. On the next page load the script reads the cookie and restores the saved theme before the page is visible.

Keyboard Shortcut

Pressing Shift+T toggles the theme from anywhere on the page. The shortcut is suppressed when focus is inside an input, textarea, or contenteditable element to avoid interfering with typing.

Icon Animation

The button contains two stacked icon boxes (sun and moon). The CSS transitions translateY and rotation when data-theme-status changes, sliding the active icon into view. The word label ("Light" / "Dark") fades between the two states using opacity.

Styling Dark Mode

Target [data-theme-status="dark"] in CSS to apply dark-mode styles to any element on the page. Use CSS custom properties (e.g. var(--color-dark)) for colour tokens to keep overrides manageable.

localStorage Alternative

To avoid the JS-Cookie dependency, replace Cookies.set('theme', newTheme, { expires: 365 }) with localStorage.setItem('theme', newTheme), and replace Cookies.get('theme') === 'dark' with localStorage.getItem('theme') === 'dark'.