(Need Help with Dark Mode Toggle in Twine 2 Harlawe 3 using JavaScript)

Hello, I am somewhat new to Twine, and I’m currently working on a project in Twine 2 Harlawe 3. I’m trying to implement a dark mode toggle feature that saves the user’s preference to local storage, i am trying to implement something that lasts through hard game restarts.

I’ve been experimenting with JavaScript, but I’m still figuring out how to effectively use it within Twine. If anyone has experience with this or could point me in the right direction, I would really appreciate your guidance!

Are you finding difficulty with the dark mode implementation or just with saving the player preferences to local storage?

I’m trying to copy this but I can’t find the code they used.

You can add this this code to your Harlowe story (modifying as needed):

  <label class="switch">
  <input type="checkbox" class="dark-mode-toggle">
  <span class="slider round"></span>
</label>

Try this macro:

(set: $darkMode = false)

And then you can use the state of the toggle button to turn dark mode on or off.


<script>
document.addEventListener('DOMContentLoaded', (event) => {
    const toggleSwitch = document.querySelector('.dark-mode-toggle');
    toggleSwitch.addEventListener('change', function() {
        document.body.classList.toggle('dark-mode');
        localStorage.setItem('darkMode', document.body.classList.contains('dark-mode'));
    });
    if (localStorage.getItem('darkMode') === 'true') {
        document.body.classList.add('dark-mode');
    }
});
</script>

This code assumes that you’re toggling a dark-mode CSS class on or off for your HTML <body>.

Here’s a simple version of that. This changes the HTML page color to black and makes the text white:


.dark-mode {
    background-color: black;
    color: white;
}

This is an interesting trick for applying a dark mode to an HTML page without specifying custom colors, using CSS filters:


.dark-mode {
    filter: invert(1) hue-rotate(180deg);
}

.dark-mode img {
    filter: invert(1) hue-rotate(180deg); /* This will revert the inversion for images */
}

This is code you could use to style the dark mode button, but it’s optional. You can decide how you want everything to look.


<style>
.switch {
  position: relative;
  display: inline-block;
  width: 60px;
  height: 34px;
}

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

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

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

input:checked + .slider {
  background-color: #2196F3;
}

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

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

.slider.round:before {
  border-radius: 50%;
}
</style>

Let me know how it goes!

2 Likes

Thanks but i already have code for the dark mode but I am wanting to save the data to the users localStorage.

This

Maybe you can share the code you have so far so I can be more helpful.


You can save to localStorage using setItem. You can retrieve what you saved with getItem.


// saving the player preference
localStorage.setItem('darkMode', userPrefs)

// getting the player preference from local storage
let userPrefs = localStorage.getItem('darkMode')

This is what I said earlier:

Try this macro.

(set: $darkMode = false)

And then you can use the state of the toggle button to turn dark mode on or off.

<script>
document.addEventListener('DOMContentLoaded', (event) => {
    const toggleSwitch = document.querySelector('.dark-mode-toggle');
    toggleSwitch.addEventListener('change', function() {
        document.body.classList.toggle('dark-mode');
        localStorage.setItem('darkMode', document.body.classList.contains('dark-mode'));
    });
    if (localStorage.getItem('darkMode') === 'true') {
        document.body.classList.add('dark-mode');
    }
});
</script>

This code assumes that you’re toggling a dark-mode CSS class on or off for your HTML <body> .

It doesn’t work :frowning:

Could you share the dark mode related code in your story? That would allow me to be more helpful. What I tried to share probably wouldn’t work in isolation.

I shared a long post at first because I didn’t want to make any assumptions about the code you had already written.

Well, I could also be wrong about the macro in particular. I’m more familiar with JavaScript than Twine.


The issue could be the macro. Maybe try

(set: $userPrefs = false)

and use that to store the player dark mode preferences? I think something else is probably going on though. I will wait for someone else more knowledgeable to answer!

This is my code the localstorage part does not work tho


   // Initialize dark mode preference on page load
    document.addEventListener("DOMContentLoaded", function () {
        // Retrieve preference from localStorage
        const savedPreference = localStorage.getItem("toggle");
        // Define the $toggle variable as false if it doesn't exist in the story
        if (typeof State.variables.toggle === "undefined") {
            State.variables.toggle = savedPreference === "true";
        }
        // Apply dark mode based on the saved or current toggle state
        if (State.variables.toggle) {
            enableDarkMode();
        } else {
            disableDarkMode();
        }
        // Set the toggle switch position
        const toggleSwitch = document.getElementById("darkModeToggle");
        if (toggleSwitch) {
            toggleSwitch.checked = State.variables.toggle;
        }
    });
    // Enable dark mode
    function enableDarkMode() {
        document.body.classList.add("dark-mode");
        localStorage.setItem("toggle", "true"); // Save preference in localStorage
        State.variables.toggle = true;
    }
    // Disable dark mode
    function disableDarkMode() {
        document.body.classList.remove("dark-mode");
        localStorage.setItem("toggle", "false"); // Save preference in localStorage
        State.variables.toggle = false;
    }
    // Toggle dark mode on switch interaction
    window.switch1 = function (element) {
        if (!element) return;
        if (element.checked) {
          enableDarkMode();
        } else {
            disableDarkMode();
        }
    };

Harlowe doesn’t support using = as an assignment operator, the correct operator to use is the to keyword.

(set: $darkMode to false)

Harlowe has been deliberately designed to limit an Author’s ability to use JavaScript to extend the functionality of their project or of the Harlowe runtime engine itself. And Harlowe doesn’t have any JavaScript APIs for accessing the features of its runtime engine.

Your JavaScript code is making reference to a State.variables property, which is part of SugarCube’s State API, which is why it’s not working in Harlowe.

Recent versions of Harlowe 3.x do support access Story & Temporary variables from JavaScript that is executed within a <script> HTML element that is placed within the contents of a Passage…

(set: $thing to "abc")
thing: before: (print: $thing) (should be abc)
<script>
	$thing = "def";
</script>
thing: after: (print: $thing) (should now be def)

…but the Story or Temporary variable does need to be initialised some time before it is referenced in JavaScript code.

Depending on the Operating System, the Brand of web-browser, and if Privacy Mode is being used, there are times when localStorage may not be accessible. So if you intend to use it then you will likely need to write additional code to handle any access/authorisation errors that may occur.

And if your Story HTML file is to be run locally from the end-user’s drive, then you may want to add an unique identifier to the Key Name you’re using, so you don’t interfere with data being stored in localStorage be any other such run HTML file.
Harlowe itself does this with the Key Names it uses to store its saves. It adds the unique IFID of the Twine Project to the Key Name. Twine stores the IFID in attributes of the <tw-storydata> element contained within the HTML file.

Sorry but i am so confused

About which part(s) exactly?

all of it…