Reactivity in Vanilla JS

May 14th, 2021

A code newbie recently asked me for some help with a project. With an HTML and CSS web page, she wasn't sure how to leverage her JavaScript basics to make it interactive.

The Dilemma

How to make a simple web app, built with HTML, CSS, and vanilla Javascript, respond to user interaction.

The Solution

Variables and Event Handlers

Cookies for Class

Let's solve this by coding out an example: Cookies for Class.

cookies-for-class-start.png

We have some cookies. Let's say we have 15 cookies in total, and we want to divide them evenly among the class. We've prepared the cookies ahead of time, but we're not sure how many students will be attending lessons today. So, we need to adjust the number of cookies to divide evenly among the children, and set their cookie limit appropriately. In our example we cannot have fractional cookies (they're individually wrapped).

Total cookies = 15

Cookies per child = 15 / number of children attending

Example: 15 cookies / 4 children = 3 cookies per child

Available cookies = 15 / number of children attending * number of children attending

Example: 3 cookies per child * 4 children = 12 cookies available to this class

Starting Code

I want to focus on vanilla JavaScript in this article, so here are the HTML and CSS files already laid out.

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Cookies for Kindergarten</title>
    <link rel="stylesheet" href="./style.css" />
  </head>
  <body>
    <div>
    <h1>Cookies for Class</h1>
  <button class="simpleButton" onclick="onStart()" id='startButton'>Ready?  Let's eat!</button> 

  <button class="hidden simpleButton" onclick="restart()" id='restartButton' >Restart</button> 
  </div>
    <div id="container">
      <div id="left-container">
        <div id="kidsBox">
          <div class="kid">
            <p>Child 1</p>
            <input class="cookieSlider" type="range" value="0" max="10" />
            <button class="getCookie disabledButton">Get a cookie</button>
          </div>
        </div>
        <button class="simpleButton" onclick="addChild()" id="addButton">Add Children</button>
      </div>

      <div id="right-container">
        <div id="cookieBox">
        <div class="cookie">🍪</div>
      </div>
      </div>
    </div>
    <script src="./index.js"></script>
  </body>
</html>
HTML
/* style.css */
* {
  box-sizing: border-box;
  border-radius: 3px;
  background-color: #fdfdfd;
}

h1 {
  text-align: center;
  color: #f21170;
}

p {
    font-size: 1.25rem;
    color: #890596;
}

#container {
  display: flex;
  justify-content: space-between;
  max-width: 900px;
  margin: auto;
}

@media screen and (max-width: 700px) {
  #container {
    flex-direction: column;
  }
}

button {
  border: 0.25px solid #6b0374;
  background-color: #890596;
  color: #fff;
  padding: 0.25rem 0.35rem;
  margin: 0 0.25rem;
  cursor: pointer;
}
button:hover {
  box-shadow: 0px 1px 3px #8a8a8a;
}

.kid {
  margin: 0.25rem;
  width: 21rem;
  display: flex;
  justify-content: flex-start;
  align-items: baseline;
}

.name {
  width: 7rem;
  margin-right: 0.25rem;
  border: 0.5px solid #8a8a8a;
}
.name:focus {
  border: 0.5px solid #f21170;
  outline: none;
}

.simpleButton {
  width: 14.5rem;
  margin: 0.5rem 0.25rem;
  background-color: #fff;
  color: #6b0374;
}
.simpleButton:hover {
  border-color: #f21170;
  color: #f21170;
}

.hidden{
    visibility: hidden;
}

.disabledButton {
    cursor: not-allowed;
    background-color: #d7d7d7;
    color: #f21170;
}

#cookieBox {
  margin: 0.25rem;
  border-radius: 3px;
  display: flex;
  flex-wrap: wrap;
  width: 20rem;
  height: 12rem;
  border: 1px solid #6b0374;
  align-items: center;
  justify-content: space-evenly;
  flex-wrap: wrap;
  font-size: 1.5rem;
  padding: 0.5rem;
}
.cookie {
    padding: 10px;
}

input[type='range'] {
  height: 0.75rem;
  -webkit-appearance: none;
  width: 7rem;
  margin-right: 0.25rem;
}
input[type='range']:focus {
  outline: none;
}
input[type='range']::-webkit-slider-runnable-track {
  width: 5rem;
  height: 0.5rem;
  animation: 0.2s;
  box-shadow: 0px 0px 0px #000000;
  background: #eeeded;
  border-radius: 25px;
  border: 0.5px solid #8a8a8a;
}
input[type='range']::-webkit-slider-thumb {
  box-shadow: 1px 1px 1px #828282;
  border: 1px solid #146d7b;
  height: 1rem;
  width: 0.5rem;
  border-radius: 6px;
  background: #1cc5dc;
  -webkit-appearance: none;
  margin-top: -5px;
}
input[type='range']:focus::-webkit-slider-runnable-track {
  background: #fff;
}
input[type='range']::-moz-range-track {
  width: 100%;
  height: 16px;
  animation: 0.2s;
  box-shadow: 0px 0px 0px #000000;
  background: #eeeded;
  border-radius: 25px;
  border: 1px solid #8a8a8a;
}
input[type='range']::-moz-range-thumb {
  box-shadow: 1px 1px 1px #828282;
  border: 1px solid #146d7b;
  height: 1rem;
  width: 0.5rem;
  border-radius: 6px;
  background: #1ca5ba;
}
CSS

Step 1: Get Elements, Set Variables

Before we can alter an element, we need access to it. There are a variety of methods to query a DOM element. You can get an element by its tag name or id. I tend to use the generic document.querySelector for its versatility. Let's get some elements that we want to interact with and store them in constant variables. In this app, that would be our buttons. The user clicks on a button, and then cool stuff happens. We'll also set the max number of cookies to 15.

// index.js
const cookieBox = document.querySelector('#cookieBox'),
  kidsBox = document.querySelector('#kidsBox'),
  addButton = document.querySelector('#addButton'),
  startButton = document.querySelector('#startButton'),
  restartButton = document.querySelector('#restartButton'),
  maxNumberofCookies = 15
JavaScript

Now, let's think about the values we want to change when the user clicks on one of those buttons. Some values we want to set to an initial value, and some we want to access and alter after we have some more information. Thinking back to our goal: we want to change the value of the following:

  • number of children when the user adds a child

  • number of cookies when children are added

  • visible cookie elements

  • a variable to keep track of those visible cookies to remove them when eaten

  • inputs:

    • When a child is added, an input (the little slider that counts up to cookie fullness) is also added to the DOM. We won't know how many are present until the start button is clicked.

    • We need access to them in more than one function**:

      • onStart - in order to set the max value

      • getCookie - to increment the value each time the 'get cookie' button is clicked

  • getCookie buttons:

    • For the same reasons as above, the number of buttons will change based on the number of children present.

    • We need access to them in more than one function**:

      • onStart - to set an eventListener to respond to clicks

      • getCookie - to disable it when cookie fullness has been attained

** When more than one function in our javascript files needs access to a variable, we have to initiate the value outside of any particular function, because of scope. Put simply, any variables initiated between those { } curly braces, whether it be in a function, for loop, if statement, etc, can only be accessed inside those { } curly braces. So, if you want to access a variable in two separate functions - you'll have to declare it outside.

let numberofKids = 1,
  numberofCookies = 1,
  cookieSliders,
  getCookieButtons,
  cookies, cookieIndex = 0
JavaScript

When you're first creating an application, don't worry if you're not sure about all the values you may need. You can adjust as you go. Generally, if it won't change - make it a "const". If it will, use "let".

Step 2: Functions to Handle Interaction

What kind of interaction are we expecting in our application? Button clicks.

In our html file, I've already set some onclick event listeners to most of our buttons. I chose to set the click event listeners directly on these elements because it's common when utilizing frameworks, like Vue (@click) or React (onClick). We'll also use the addEventListener method to dynamically add button functionality in a moment.

Let's look at our onclick methods. We have three: onStart, restart and addChild. Let's begin with the last, because it's the first button our users will most likely interact with.

addChild

When the 'Add Children' button is clicked, it should:

  • increment the number of children

  • add DOM elements to represent that child

  • adjust the number of available cookies: both the variable and the DOM elements

In order to alter the appearance of our page, we set the innerHTML of the elements we want to change to a new string. The 'cookieString' variable is filled with as many cookie divs as cookies are available with a simple for loop. The loop runs as many times as available cookies and adds a cookie each time. The html representing a child doesn't need to be completely re-written, as we're only adding - not recalculating the number of available items.

// index.js
function addChild() {
  numberofKids++

  numberofCookies = Math.floor(maxNumberofCookies / numberofKids) * numberofKids
  let cookieString = ``

  for (let i = 1; i <= numberofCookies; i++) {
    cookieString += '<div class="cookie">🍪</div>'
  }

  cookieBox.innerHTML = cookieString

  kidsBox.innerHTML += `
    <div class="kid">
        <p>Child ${numberofKids} </p>
        <input class="cookieSlider" type="range" value="0" />
        <button class="getCookie disabledButton">Get a cookie</button>
      </div>
`
}
JavaScript

There is a problem with our function. What happens if there are more children than cookies? In order to handle the possibility, let's add an if statement to check if the numberifKids is equal to our maxNumberofCookies. We'll alert the user to the situation, and call the onStart method. Notice I'm using backticks and a template literal in the alert. This makes our code more robust. If we ever want to change the maxNumberofCookies, we only have to change it in one place and everything will continue to work as expected - as opposed to using 15.

function addChild() {
  if(numberofKids === maxNumberofCookies){
    window.alert(`${maxNumberofCookies} children is the max`)
    return onStart()
  }

  numberofKids++
  ...
JavaScript

onStart

For our little app, we want to increment the value in the slider each time a child gets a cookie. It should stop once the child has gotten all cookies available to them, and they should not be able to get any more. We also want to hide our 'Add Children' button as that functionality should be disabled. We'll also hide our start button and replace it with the restart button.

Let's begin! First, toggle the hidden class on the buttons we want to hide or show. This sets each element's visibility, as you can see in the CSS file. Next, we'll use querySelectorAll to get all the slider and button elements we added to represent the children. This method will return a Node List of DOM elements, which we can iterate over with the forEach method. We'll use this to set the event handler for each 'Get a Cookie' button click and to set the max value of the sliders.

function onStart() {
  addButton.classList.toggle('hidden')
  startButton.classList.toggle('hidden')
  restartButton.classList.toggle('hidden')
  cookieSliders = document.querySelectorAll('.cookieSlider')
  getCookieButtons = document.querySelectorAll('.getCookie')

  getCookieButtons.forEach((button, index) => {
    button.addEventListener('click', function () {
      getCookie(index)
    })
    button.classList.toggle('disabledButton')
  })
  cookieSliders.forEach(
    (slider) => (slider.max = numberofCookies / numberofKids)
  )
  cookies = document.querySelectorAll('.cookie')
}
JavaScript

The forEach function takes each element in the list and, optionally, that element's index. In order to make sure the correct child gets their cookie, we'll pass that index into our next function.

getCookie

Let's eat! Each time a child gets a cookie, we want to show that they are one step closer to being full, and remove a cookie from the cookie box. We do this by incrementing the value of the correct slider, using that index number we passed in. Note that the value is of type string, so we need to parse it into a number using parseInt. We'll use the remove method to remove a cookie div.

We also want to disable our cookie getting button once a child has eaten all cookies available to them. When all the cookies have been eaten, we'll let our user know by changing the innerHTML of the cookie box.

function getCookie(index) {
  if (
    parseInt(cookieSliders[index].value) < parseInt(cookieSliders[index].max)
  ) {
    cookieSliders[index].value++
    cookies[cookieIndex].remove()
    cookieIndex++
    if(cookies.length === cookieIndex){
        cookieBox.innerHTML = `
        <p>
        No more cookies!
        </p>
        `
    }
    if (
      parseInt(cookieSliders[index].value) ===
      parseInt(cookieSliders[index].max)
    ) {
      getCookieButtons[index].disabled = 'disabled'
      getCookieButtons[index].classList.toggle('disabledButton')
      getCookieButtons[index].innerHTML = 'Full of cookies!'
    }
  }
}
JavaScript

Once all the cookies have been eaten, this is how our page should look:

cookies-for-class-end.png

Notice I've included a restart button. In the restart function, you can go through each altered element and value and reset them. Feel free to do this on your own.

Here's the finished JS file.

// get elements
const cookieBox = document.querySelector('#cookieBox'),
  kidsBox = document.querySelector('#kidsBox'),
  addButton = document.querySelector('#addButton'),
  startButton = document.querySelector('#startButton'),
  restartButton = document.querySelector('#restartButton'),
  maxNumberofCookies = 15

// set variables
let numberofKids = 1,
  numberofCookies = 1,
  cookieSliders,
  getCookieButtons,
  cookies, cookieIndex = 0

function addChild() {
  if(numberofKids === maxNumberofCookies){
    window.alert(`${maxNumberofCookies} children is the max`)
    return onStart()
  }
  numberofKids++

  numberofCookies = Math.floor(maxNumberofCookies / numberofKids) * numberofKids

  let cookieString = ``

  for (let i = 1; i <= numberofCookies; i++) {
    cookieString += '<div class="cookie">🍪</div>'
  }

  cookieBox.innerHTML = cookieString

  kidsBox.innerHTML += `
    <div class="kid">
        <p>Child ${numberofKids} </p>
        <input class="cookieSlider" type="range" value="0" />
        <button class="getCookie disabledButton">Get a cookie</button>
      </div>
`
}

function onStart() {
  addButton.classList.toggle('hidden')
  startButton.classList.toggle('hidden')
  restartButton.classList.toggle('hidden')
  cookieSliders = document.querySelectorAll('.cookieSlider')
  getCookieButtons = document.querySelectorAll('.getCookie')

  getCookieButtons.forEach((button, index) => {
    button.addEventListener('click', function () {
      getCookie(index)
    })
    button.classList.toggle('disabledButton')
  })
  cookieSliders.forEach(
    (slider) => (slider.max = numberofCookies / numberofKids)
  )
  cookies = document.querySelectorAll('.cookie')
}

function getCookie(index) {
  if (
    parseInt(cookieSliders[index].value) < parseInt(cookieSliders[index].max)
  ) {
    cookieSliders[index].value++
    cookies[cookieIndex].remove()
    cookieIndex++
    if(cookies.length === cookieIndex){
        cookieBox.innerHTML = `
        <p>
        No more cookies!
        </p>
        `
    }
    if (
      parseInt(cookieSliders[index].value) ===
      parseInt(cookieSliders[index].max)
    ) {
      getCookieButtons[index].disabled = 'disabled'
      getCookieButtons[index].classList.toggle('disabledButton')
      getCookieButtons[index].innerHTML = 'Full of cookies!'
    }
  }
}

function restart() {
  console.log("Please reset all values and elements to restart")
// reset all values and elements
}
JavaScript

I hope you enjoyed this article and found some useful content.

Happy coding!

© 2026 Rebecca Page