How to build a JavaScript calculator

Reading Time: 9 minutes 🕑

Last updated: October 19, 2021.

In this tutorial, we will learn how to build a JavaScript calculator using HTML, CSS and (vanilla) JavaScript. The calculator will accept mouse and keyboard input, display numbers with comma notation and include a filter on user input to prevent obvious bugs from occuring.

What we will build

Below is a live preview of the completed project:

See the Pen Untitled by James (@openjavascriptadmin) on CodePen.

We will start with the HTML markup. Then we will add CSS so that our calculator looks like one! The main focus of the tutorial will be the third step, where we program the functionality of the calculator using JavaScript.

Step 1: Create the HTML markup

To create the calculator in HTML, we create a wrapper div with two children: (i) a display div that will contain the display value of the calculator and (ii) a table with an id of keys. Inside of the table, we create the input keys we want to include in the calculator. Each one of these is a td element within a table row element (tr). Inside of the display div, we nest another div with the id of display-text. This is useful for styling because it separates the display text from the display itself.

Note that each input key has an id value that is identical to its display value (e.g. the id for the = key is "=").

Note that it is possible to extend an input key across multiple cells horizontally horizontally using colspan or vertically using the rowspan attribute. In this example, we span the AC input key across two columns and the = key across two rows.

At this point, it is important to play around with the HTML markup until you are happy with the layout of the keys.

    <div id="content-wrapper">
        <div id="display"><div id="display-text"></div></div>
        <table id="keys">
            <tr><td colspan ="2" id="AC">AC</td><td id="DEL">DEL</td></td><td id="*">*</td></tr>
            <tr><td id="7">7</td><td id="8">8</td><td id="9">9</td><td id="/">/</tr>
            <tr><td id="4">4</td><td id="5">5</td><td id="6">6</td><td id="-">-</td></tr>
            <tr><td id="1">1</td><td id="2">2</td><td id="3">3</td><td id="+">+</td></tr>
            <tr><td id="0">0</td><td id=".">.</td><td colspan ="2" id="=">=</td></tr>
        </table>
    </div>

Step 2: CSS styling

To style our markup so it now looks like a calculator (and is nicer to work with when add JavaScript!), add the following CSS. You calculator should now look like the one in the live preview.

        body {  /* Sets the background to a linear gradient */
            background: linear-gradient(#e66465, #9198e5);
            height: 100%;
            margin: 0;
            background-repeat: no-repeat;
            background-attachment: fixed;
        }
        #content-wrapper {
            margin: 0px auto;
            margin-top: 2rem;
            width: 20rem; /* Calculator width set using responsive rem unit */
        }
        #display {
            height: 3.5rem;
            padding-top: 0.5rem;
            padding-bottom: 0.2rem;
            border:rgb(70, 70, 70) solid 0.7rem;
            background-color: white;
            text-align: right; /* Number display starts from right */
            vertical-align: middle;
            font-size: 2.5rem;
            overflow:hidden; /* Off-display numbers not visible */
        }
        #display-text {
            background-color: white;
            margin-left: 1rem;
            margin-right: 1rem;
        }
        #keys {
            font-size: 1.7rem;
            text-align: center;
            vertical-align: middle;
        }
        table {
            background-color: skyblue;
            width: 20rem;
            border-spacing: 0.3rem;
            border:rgb(70, 70, 70) solid 0.15rem;
            padding: 0.2em;
        }
        table td { /* Styling on input keys */
            width: 3.7rem;
            height: 3rem;
            padding: 0.1rem;
            border: 0.1rem solid grey;
            border-radius: 3rem;
            background-color: rgb(245, 245, 245);
            color: rgb(30, 30, 30);
        }

Step 3: JavaScript

#2.1 Adding event listeners and selecting elements

To begin create the app functionality, we add event listeners to each of the input keys. For most keys, we add an event listener that will fire an input function upon a click. We define this function in the next section.

        // Clicking an input fires the input function
        document.getElementById('0').addEventListener('click', input);
        document.getElementById('1').addEventListener('click', input);
        document.getElementById('2').addEventListener('click', input);
        document.getElementById('3').addEventListener('click', input);
        document.getElementById('4').addEventListener('click', input);
        document.getElementById('5').addEventListener('click', input);
        document.getElementById('6').addEventListener('click', input);
        document.getElementById('7').addEventListener('click', input);
        document.getElementById('8').addEventListener('click', input);
        document.getElementById('9').addEventListener('click', input);
        document.getElementById('+').addEventListener('click', input);
        document.getElementById('-').addEventListener('click', input);
        document.getElementById('*').addEventListener('click', input);
        document.getElementById('/').addEventListener('click', input);
        document.getElementById('.').addEventListener('click', input);

Next, because the input keys =, AC and DEL should perform a different task when pressed, we add an event listener to each of these that trigger separately defined functions:

        // Equals fires the calculate function
        document.getElementById('=').addEventListener('click', calculate);

        // AC fires the reset function
        document.getElementById('AC').addEventListener('click', reset);

        // DEL fires the del function
        document.getElementById('DEL').addEventListener('click', del);

We also need to select the display-text element so we can display the result of user input. We store this in a variable called display.

For print to display-text, we need another variable that stores the ongoing result of user input. We name this displayCurrent and initialize this with the starting value of an empty string:

        // Get display element as constant
        const display = document.getElementById('display-text');

        // Set current display to empty string       
        let displayCurrent = "";

In our functions, the calculator display value will be updated by printing the value of displayCurrent to display.

So that JavaScript does not throw an error when trying to fire any one of these functions while we are testing our app, we can define our referenced functions with no code inside. This will prevent this behaviour.

        function input() {
        }

        function calculate() {
        }

        function reset() {
        }

        function del() {
        }

We are now ready to define our functions.

#2.2 Defining the input function

First, we define the input function, which will is fired when most input keys are pressed (except =, AC and DEL).

Because the function is triggered by an event, we have available to us the event itself, which we can pass into the function. Sticking to convention, we name this e in our example.

We can access the target element of the event by calling e.target (this will be the input key pressed). We want to access the innerText value of the pressed key (also its value as displayed to the user). We can access this by calling e.target.innerText.

We now want to append this value to the displayCurrent variable (initialized in the previous step with a starting value of an empty string). For appending, we can make use of the assignment operator (+=):

        function input(e) {
            let inputValue = e.target.innerText; // save value of input key
            displayCurrent += inputValue; // append to displayCurrent
        }

Now, whatever key is pressed by the user (except =, AC and DEL) is appended to the displayCurrent string.

To render this value live to the user, we need to set the innerText value of the display element to displayCurrent at the end of our function.

With this code, the calculation we want to make is already appearing on screen.

        function input(e) {
            let inputValue = e.target.innerText; // save value of input key
            displayCurrent += inputValue; // append to displayCurrent
            display.innerText = displayCurrent;
        }

#2.3 Defining the calculate, reset and del functions

calculate function

For the calculate function, we want to take the live value of displayCurrent and, to calculate it, execute it as if it were a line of JavaScript code.

For this functionality, we have available to us the eval() function (stored in the global window object). All we need to do is pass in For evaluating a value as a line of JavaScript code, we can use the globally available eval() function. All we need to do is pass in displayCurrent.

Immediately afterwards, we want to set the display value of the calculator to be the result of this calculation. So we set the value of display.innerText to its outcome:

        function calculate() {
            display.innerText = eval(displayCurrent);
        }

Our calculator now…calculates!

But it doesn’t yet behave quite like a real calculator: when another input key is pressed after the calculation, the calculator display the value of displayCurrent (plus the new input key) as if the calculation never took place.

To correct this, we need to set the value of displayCurrent to the outcome as well. When setting its value, we apply the toString() method so we continue to work with string data in our ongoing calculation. If we did not add this, we may encounter a bug if we tried to apply any string methods to modify the calculation elsewhere in our app.

        function calculate() {
            display.innerText = eval(displayCurrent);
            displayCurrent = eval(displayCurrent).toString();
        }

reset function

The reset function should clear any ongoing calculation and clear the display value. This is done by setting the value of displayCurrent to an empty string and also setting this as the innerText of display:

        function reset() {
            displayCurrent = "";
            display.innerText = displayCurrent;
        }

del function

Our final function, del, is similar to the reset function. Except this time, we do not want to clear any ongoing calculation value but delete its final value.

For this, we can make use of the substring array method. We use this method by specifying what part of the string we would like to keep. Because we want to keep the string values from beginning to the end minus -1, we pass in 0 and displayCurrent.length-1 as arguments. We save the outcome of the application of the substring method to displayCurrent.

        function del() {
            displayCurrent = displayCurrent.substring(0, displayCurrent.length - 1);
        }

Finally, we set this as the new display value of the calculator:

    function del() {
        displayCurrent = displayCurrent.substring(0, displayCurrent.length - 1);
        display.innerText = displayCurrent;
    }

#2.4 Refining functionalty

The calculator now works quite well: we provide input, can calculate and display the result. We can also clear input and delete the last input.

But there are still some ways in which we can improve our calculator:

  1. Add commas to the number display for better readability
  2. Enable keyboard input
  3. Apply a filter to input to prevent obvious bugs

Numbers with commas

The display of most calculators would include commas for readability. Let’s add this to our calculator.

First, we define a new function in our script. Since adding commas to numbers is a common problem, we can take a pre-defined function from codegrepper and add it to our script. This code below will accept a number and return it as a string with commas added:

        function numberWithCommas(x) {
            return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
        }

We then need to make a few modifications to our earlier functions so that whenever we set the value of display.innerText, it is first passed through the numberWithCommas() function.

Our four main functions now look like this:

        // Modified lines of code marked with *** //

        function input(e) {
            let inputValue = e.target.innerText;
            displayCurrent += inputValue;
            display.innerText = numberWithCommas(displayCurrent); // ***
        }

        function calculate() {
            display.innerText = numberWithCommas(eval(displayCurrent));  // ***
            displayCurrent = eval(displayCurrent).toString();
        }

        function reset() {
            displayCurrent = "";
            display.innerText  = numberWithCommas(displayCurrent);  // ***
        }

        function del() {
            displayCurrent = displayCurrent.substring(0, displayCurrent.length - 1);
            display.innerText  = numberWithCommas(displayCurrent);  // ***
        }

Our calculator now displays numbers with commas added.

Keypad input

Though the calculator is functional, it would be much more user-friendly if it accepted keyboard input.

To add this, we start by adding a new event listener that is attached to the document (i.e. the entire web page) and listens out for a 'keydown'. We then differentiate between which function is fired based upon which key is pressed.

To differentiate between keys pressed, we use the code value available to us automatically on the event object (e).

For example, if = is pressed, we fire the calculate function. If backspace is pressed, we fire the del function. And if delete is pressed, we run reset. If the key is of another type, we pass the event object down into the input function and handle the event there. This means we have to make some modifications to the input function to be able to do this.

But first, here is what the keydown event listener should look like:

        // Keyboard input
        document.addEventListener('keydown', (e) => {
             if (e.code == "Enter") { calculate() } 
             else if (e.code == "Backspace") { del() }
             else if (e.code == "Delete") { reset() }
             else { input(e) } 
        })

Now we need to modify our input function.

First, we initialize input value as an empty string rather setting is to e.target.innerText.

If e.which is equal to 1, this means the event is a mouse click. In this scenario, we handle the event in the same way as previously: the input value is e.target.innerText.

Otherwise, we handle the event as a keyboard-initiated event. And we use a conditional statement to set the value of inputValue to the corresponding key pressed.

After this, we handle the event in the same way as previously, appending the value of inputValue to displayCurrent and printing this to the display element:

function input(e) {
            // initialize inputValue
            let inputValue = "";

            // If click, set inputValue as previously
            if (e.type == "click") {
                inputValue = e.target.innerText;
            }

            // Otherwise set input by key pressed 
            else{
            if (e.key == "0") {inputValue = "0"}
            else if (e.key == "1") {inputValue = "1"}
            else if (e.key == "2") {inputValue = "2"}
            else if (e.key == "3") {inputValue = "3"}
            else if (e.key == "4") {inputValue = "4"}
            else if (e.key == "5") {inputValue = "5"}
            else if (e.key == "6") {inputValue = "6"}
            else if(e.key == "7") {inputValue = "7"}
            else if(e.key == "8") {inputValue = "8"}
            else if(e.key == "9") {inputValue = "9"}
            else if(e.key == "+") {inputValue = "+"}
            else if(e.key == "-") {inputValue = "-"}
            else if(e.key == "*") {inputValue = "*"}
            else if(e.key == "/") {inputValue = "/"}
            }

            displayCurrent += inputValue;
            display.innerText = numberWithCommas(displayCurrent);
        }

Preventing input syntax bugs

Finally, we should probably do a little bit of housekeeping.

At present, a user can enter two symbols, one after the other. Like a real calculator, we would like to prevent this input being accepted and instead only accept the last symbol pressed by the user.

For this, we can add a kind of filter using if else, checking to see what the previous input was, what the current input is and specifying that if both are symbols, remove the final character of the displayCurrent string and append the current input value.

Our input function should now look something like this:
function input(e) {
            // initialize inputValue
            let inputValue = "";

            // If click (1), set inputValue as previously
            if (e.type == "click") {
                inputValue = e.target.innerText;
            }

            // Otherwise set input by key pressed 
            else{
            if (e.key == "0") {inputValue = "0"}
            else if (e.key == "1") {inputValue = "1"}
            else if (e.key == "2") {inputValue = "2"}
            else if (e.key == "3") {inputValue = "3"}
            else if (e.key == "4") {inputValue = "4"}
            else if (e.key == "5") {inputValue = "5"}
            else if (e.key == "6") {inputValue = "6"}
            else if(e.key == "7") {inputValue = "7"}
            else if(e.key == "8") {inputValue = "8"}
            else if(e.key == "9") {inputValue = "9"}
            else if(e.key == "+") {inputValue = "+"}
            else if(e.key == "-") {inputValue = "-"}
            else if(e.key == "*") {inputValue = "*"}
            else if(e.key == "/") {inputValue = "/"}
            }


            // *** Filter for double symbol starts *** //

            // Check if previous and current input are symbols
            if ((displayCurrent.substr(-1) == "*" || 
            displayCurrent.substr(-1) == "/" || 
            displayCurrent.substr(-1) == "-" || 
            displayCurrent.substr(-1) == "+") &&
            (inputValue == "*" || 
            inputValue == "/" || 
            inputValue == "-" || 
            inputValue == "+")
            )  {
                // If yes, remove last symbol and add new one
                displayCurrent = displayCurrent.slice(0, -1) + inputValue;
            }
            else {
            // Otherwise, append input value as normal
            displayCurrent += inputValue;
            }

            // *** Filter for double symbol ends *** //

            // Print display current
            display.innerText = numberWithCommas(displayCurrent);
        }

Summary

In this tutorial, we have seen how to build a functional calculator using HTML, CSS and JavaScript that accepts both mouse and keyboard input. We have also seen how we can filter user input to prevent bugs and add commas to numeric output to make our calculator display more readable.

If you liked this project, be sure to check out our projects section for more JavaScript projects.

And to hear about new projects as they drop, consider following us on Facebook.