How to Build a Scientific Calculator in JavaScript with Exprify.js

scientific calculator in the browser with-exprify js

The simplest solution can sometimes be the most dangerous in Javascript. Run the code eval("2 + 2") and you will get 4 back. But running eval("...SECRET") can give you information that you didn't want to show anyone.

This is where Math Expression Libraries will help you.

Hi, I'm Hemanta Gayen. In this guide, we'll use a math expression library to build a scientific calculator. By the end of this guide you'll have a working calculator that runs in the browser.

It will handle basic arithmetic, trigonometric functions in degree mode, logarithms, constants like pi and e, memory operations, and error handling. All of this is in a single HTML file, and no build tools are required.

You can find the code for the scientific calculator on GitHub.

Table of Contents


Stop Using eval(): A Smarter Way to Handle Math in JavaScript

If you've ever needed to evaluate a math expression from a string in JavaScript, you've probably seen this pattern:

const result = eval("2 + 3 * 4"); // 14

It works, but it also has security risks. eval() executes not only mathematical code, but also any JavaScript code. That's why every reputable source like MDN Web Docs, Google Web Fundamentals tells you: don't use eval().

There are currently many open-source math expression libraries available, such as math.js, expr-eval, and formula.js. But today we will use the Exprify library created by our team member Nirmal Paul. It's a small JavaScript library that evaluates math expressions. It understands math functions, variables, and more. However, it's designed to evaluate math expressions rather than execute JavaScript code. Think of it as a calculator engine you can add to any project.

import Exprify from "exprify";

const expr = new Exprify();
expr.evaluate("2 + 3 * 4");   // 14
expr.evaluate("sind(90)");    // 1  degree
expr.evaluate("2 inch to cm");// 5.08 cm unit conversion

Exprify tokenizes your input, builds an abstract syntax tree and processes it to compute the result behind the scenes. Each step is evaluated within Exprify's parsing and evaluation pipeline. You can also do expr.parse('2 + 2') to see the tokens and AST.

Disclosure: This article focuses on Exprify, but the same concepts can be applied to other expression evaluators such as math.js and expr-eval. The Exprify library used in this tutorial was created by a member of our team.

Prerequisites

This is a hands-on guide, but you don't need much to follow along:

  • Basic JavaScript: If you know what functions, variables, and event listeners are, you're ready.
  • A text editor: VS Code, Notepad++, or even Notepad will do.
  • Optional: Node.js if you want to try Exprify on the command line.

Installing Exprify

Exprify is available as an npm package and as a standalone UMD bundle through CDN. Since we're creating a browser-based calculator, we'll use the CDN method.

Add this <script> tag to your HTML:

<script src="https://unpkg.com/exprify"></script>

This loads Exprify as a global Exprify constructor. Again, if you prefer Node or Bundler, install via npm.

npm install exprify

Then import it:

import Exprify from "exprify";
// or
const Exprify = require("exprify");

For this tutorial, stick with the CDN. It allows us to focus on building the calculator without worrying about module loaders.

Project Setup

Create a file called calculator.html and open it in your editor. We'll build everything in this single file. Start with the HTML skeleton:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Exprify Calculator</title>
  <script src="https://unpkg.com/exprify"></script>
</head>
<body>
  <!-- calculator will go here -->
  <script>
    const expr = new Exprify();
  </script>
</body>
</html>

The line const expr = new Exprify() creates a new instance of evaluation with it own separate state. Variables, custom functions and expression cache are limited to this instance. You can create as many instances as you want and they will not interfere with each other.

Next, add the HTML for the calculator display and button grid inside <body>:

<div class="calculator">
  <input type="text" id="display" class="display" value="0" readonly />
  <div class="buttons" id="buttons">
    <!-- populated by JavaScript -->
  </div>
</div>

We'll create the buttons dynamically from a configuration array. This keeps the HTML clean and makes it simple to add buttons later.

Calculator Functionality

Let's define the button layout and connect the logic. Create an array that describes each button:

const buttons = [
  // row 1
  { label: "C", action: "clear" },
  { label: "±", action: "negate" },
  { label: "%", action: "append", value: "%" },
  { label: "÷", action: "append", value: "/" },
  // row 2
  { label: "7", action: "append", value: "7" },
  { label: "8", action: "append", value: "8" },
  { label: "9", action: "append", value: "9" },
  { label: "×", action: "append", value: "*" },
  // row 3
  { label: "4", action: "append", value: "4" },
  { label: "5", action: "append", value: "5" },
  { label: "6", action: "append", value: "6" },
  { label: "−", action: "append", value: "-" },
  // row 4
  { label: "1", action: "append", value: "1" },
  { label: "2", action: "append", value: "2" },
  { label: "3", action: "append", value: "3" },
  { label: "+", action: "append", value: "+" },
  // row 5
  { label: "0", action: "append", value: "0", span: 2 },
  { label: ".", action: "append", value: "." },
  { label: "=", action: "evaluate" },
];

Now write the rendering logic:

const display = document.getElementById("display");
const container = document.getElementById("buttons");

let expression = "";

buttons.forEach((btn) => {
  const el = document.createElement("button");
  el.textContent = btn.label;
  if (btn.span) el.style.gridColumn = `span ${btn.span}`;
  el.addEventListener("click", () => handleButton(btn));
  container.appendChild(el);
});

function handleButton(btn) {
  switch (btn.action) {
    case "append":
      expression += btn.value;
      display.value = expression;
      break;
    case "clear":
      expression = "";
      display.value = "0";
      break;
    case "negate":
      if (expression.startsWith("-")) {
        expression = expression.slice(1);
      } else if (expression) {
        expression = "-" + expression;
      }
      display.value = expression || "0";
      break;
    case "evaluate":
      try {
        const result = expr.evaluate(expression);
        display.value = result;
        expression = String(result);
      } catch {
        display.value = "Error";
        expression = "";
      }
      break;
  }
}

Notice the evaluate case. It calls expr.evaluate(expression) which parses and computes the result in one step. If the expression is invalid, for example, "2 +", Exprify throws an error. We catch that error to show Error instead of crashing the UI.

This is the core of the calculator, that handles the entire interaction loop. No external libraries, just a simple expression string.

Adding Scientific Functions

A simple four-function calculator is sufficient for accounting, but we are building a scientific calculator. Exprify has over 130 built-in functions, many of which we can add as buttons.

const scientificButtons = [
  { label: "sin", action: "append", value: "sind(" },
  { label: "cos", action: "append", value: "cosd(" },
  { label: "tan", action: "append", value: "tand(" },
  { label: "log", action: "append", value: "log(" },
  { label: "ln", action: "append", value: "ln(" },
  { label: "√", action: "append", value: "sqrt(" },
  { label: "x²", action: "append", value: "^2" },
  { label: "xⁿ", action: "append", value: "^" },
  { label: "π", action: "append", value: "pi" },
  { label: "e", action: "append", value: "e" },
  { label: "(", action: "append", value: "(" },
  { label: ")", action: "append", value: ")" },
];

Why sind, cosd, tand?

Most scientific calculators are set to degrees by default. Exprify library sind(), cosd(), and tand() accept degrees directly, so you don't need to convert from radians.

The regular sin() uses radians, which is a common source of bugs. We include both options on purpose to make Exprify more convenient for daily use.

We also get constants for free: pi and e are built-in. expr.evaluate("pi") returns 3.141592653589793, and expr.evaluate("e") returns 2.718281828459045.

Render the scientific buttons in a separate panel:

<div class="sci-panel" id="sciPanel"></div>
const sciContainer = document.getElementById("sciPanel");

scientificButtons.forEach((btn) => {
  const el = document.createElement("button");
  el.textContent = btn.label;
  el.addEventListener("click", () => handleButton(btn));
  sciContainer.appendChild(el);
});

Add a toggle button in the main button grid to show/hide the scientific panel. This gives you a dual-mode calculator.

Working with Variables and Memory

Real calculators have memory buttons: MC (clear memory), MR (recall memory), M+ (add to memory), M- (subtract from memory). The variable system of Exprify makes this easy to implement.

Add memory buttons to the config:

const memoryButtons = [
  { label: "MC", action: "memoryClear" },
  { label: "MR", action: "memoryRecall" },
  { label: "M+", action: "memoryAdd" },
  { label: "M−", action: "memorySubtract" },
];

And the handlers:

function handleButton(btn) {
  switch (btn.action) {
    // ... existing cases ...
    case "memoryClear":
      expr.setVariable("mem", 0);
      break;
    case "memoryRecall":
      expression += String(expr.getVariable("mem") || 0);
      display.value = expression;
      break;
    case "memoryAdd":
      try {
        const val = expr.evaluate(expression);
        const mem = expr.getVariable("mem") || 0;
        expr.setVariable("mem", mem + val);
      } catch {
        display.value = "Error";
      }
      break;
    case "memorySubtract":
      try {
        const val = expr.evaluate(expression);
        const mem = expr.getVariable("mem") || 0;
        expr.setVariable("mem", mem - val);
      } catch {
        display.value = "Error";
      }
      break;
  }
}

Here's what's going on:

  • expr.setVariable("mem", value) stores a value in exprify internal variable store.
  • expr.getVariable("mem") gets that value.

The variable remains active across all evaluations on this instance. This is exactly the behavior you want for calculator memory.

You can verify that it works by trying something like "5 + 3", then pressing M+ to store 8, clearing the display, and then pressing MR. The 8 will appear. That’s the whole memory system summed up in a few lines.

For a visual indicator, toggle a CSS class on the memory button when mem is not zero. So users know data is stored.

Advanced Features

Once the core calculator is working, there's a lot more you can layer on top with minimal code.

Custom Functions with addFunction

expr.addFunction("cube", (n) => n ** 3);
expr.addFunction("avg", (a, b) => (a + b) / 2);

expr.evaluate("cube(5)");    // 125
expr.evaluate("avg(10, 20)");// 15

You can add a custom functions panel where users are able to define their own functions.

Inline Function Definitions

Define functions directly in expression syntax without leaving the calculator:

expr.evaluate("hyp(a, b) = sqrt(a^2 + b^2)");
expr.evaluate("hyp(3, 4)");  // 5

This is great for one-time helpers. Type f(x) = x^2 + 1 into the calculator. Then evaluate f(5) to get 26. The function remains for the lifetime of the instance.

Expression Chaining

Exprify chain() API creates a fluent pipeline, where the results of each step are stored in a special ans variable for the next step:

const c = expr.chain();
c.evaluate("100 / 4");   // ans = 25
c.evaluate("ans + 3");   // ans = 28
c.evaluate("ans * 2");   // ans = 56
c.done();                // 56

For a calculator, this means you can create an ans button that inserts the last result. You can also build a history panel that shows each step of a sequence of calculations. This is helpful for educational tools where you want to show your work.

Error Handling

We already have a basic try/catch around evaluation, but Exprify also lets you be more specific. Invalid expressions throw Error objects with descriptive messages:

try {
  expr.evaluate("2 + ");
} catch (e) {
  console.log(e.message); // "Unexpected end of expression"
}

You can show different messages for different error types. Syntax Error for parse failures, Math Error for things like sqrt(-1), Infinity for division by zero.

Styling the Calculator UI

A calculator should look like a calculator. Here’s a complete stylesheet that uses CSS Grid for the button layout and a dark theme:

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  background: #59598b;
  font-family: sans-serif;
}

.calculator {
  background: #2d3c63;
  border-radius: 16px;
  padding: 20px;
  width: min(100% - 32px, 400px);
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
}

.display{
  width:100%;
  height:70px;
  border:none;
  border-radius:10px;
  background:#0f3460;
  color:white;
  text-align:right;
  padding:0 15px;
  font-size:2rem;
  margin-bottom:15px;
}

.buttons,
.sci-panel  {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 10px;
  margin-bottom: 10px;
}

button {
  padding: 16px 8px;
  font-size: 1.1rem;
  border: none;
  border-radius: 10px;
  cursor: pointer;
  background: #1a1a40;
  color: #e8e8e8;
  transition: background 0.15s, transform 0.1s;
  font-family: inherit;
}

button:hover {
  background: #2a2a5e;
}

button:active {
  transform: scale(0.95);
}

button[data-action="evaluate"] {
  background: #e94560;
  color: #fff;
}

button[data-action="evaluate"]:hover {
  background: #ff6b81;
}

button[data-action="clear"] {
  background: #533483;
}

The key decisions here:

  • clamp() for font sizing makes sure the display text scales well from phone to desktop without needing media queries.
  • CSS Grid simplifies the button layout repeat(4, 1fr) creates exactly four columns. The 0 button spans 2 columns using grid-column: span 2.
  • Dark theme (#59598b to #2d3c63 to #0f3460) adds visual depth.
  • Active state with scale(0.95) provides feedback when the button is pressed.

The width: min(100% - 32px, 400px) takes care of the responsive design for you: it prevents the calculator from overflowing on small screens and sets its max-width on desktop.

Testing: How the Scientific Calculator Works

Once everything is connected, open calculator.html in your browser and go through these test cases.

scientific calculator demo

Scientific Functions

Input Expected Notes
sind(30) 0.5 Sine in degree mode
tand(45) 1 Tangent in degree mode
sqrt(144) 12 Square root
log(1000) 3 Base-10 logarithm
ln(e) 1 Natural log of e

If any of these produce an unexpected result, check your event wiring and make sure the expression string is being constructed correctly. The try/catch around expr.evaluate() should gracefully handle all error cases.

Conclusion

You've created a working scientific calculator in about 150 lines of HTML, CSS, and JavaScript. There are no frameworks, no build tools, and no eval() used at all. Exprify handles the heavy work of parsing and evaluating math expressions.

Let's recap what the calculator does:

  • Supports trigonometric functions in degree mode
  • Provides logarithmic and power functions
  • Includes built-in math constants
  • Implements memory operations
  • Handles errors gracefully without crashing

The same library that runs this simple calculator also supports symbolic algebra, unit conversions, matrix operations, complex numbers, exact fractions, arbitrary-precision decimals, lambda expressions, and calculus. You have a complete math engine at your fingertips.

Happy Coding with CodeHemu!

Post a Comment

0 Comments