Next.jsBeginner10 min readUpdated

Build a Calculator in Next.js Step by Step

By Mudassir Khan — Agentic AI Consultant & AI Systems Architect, Islamabad, Pakistan

Cover illustration for: Build a Calculator in Next.js Step by Step

Section 01 · Setup

Project setup in three commands

If you finished the previous post you can skip the create-next-app step and start at the file creation.

Quick answer

How do you build a calculator in Next.js? Create a new route folder at app/calculator/. Add a page.tsx file marked with 'use client' so it can hold state. Use useState to track the display, the previous value, and the pending operator. Render a CSS grid of buttons and a display panel above them. Wire each button to a handler that updates state. The math is plain JavaScript.

Start with a fresh Next.js project (or open the one you generated in the installation post). The folder structure follows the rules from the structure guide: a folder under app/ becomes a URL.

bash
mkdir -p app/calculator
touch app/calculator/page.tsx

Navigate to http://localhost:3000/calculator in your browser. You will see a blank page (because page.tsx is empty). That is the starting point. The rest of this post fills in that file.

Section 02 · Plan

The shape of the calculator before any code

Two pieces: a display panel on top, a four by four button grid below. Three pieces of state hold everything.

Calculator wireframe with a display on top and a four by four grid of digit and operator buttons.
Display on top, button grid below. The whole UI fits in one Client Component.

display

A string. What the user currently sees. Starts as '0'. Grows as the user presses digits.

previous

A string or null. The value entered before the user pressed an operator. Null until the first operator is pressed.

operator

One of '+', '-', '×', '÷', or null. The operator the user has chosen to apply.

Equals takes previous, the current display, and the chosen operator, computes the result, and writes it back to the display. That is the whole logic.

Section 03 · Build the UI

The first version: a display and twelve buttons

No math yet. Just the visible UI and an onClick handler on every button that updates the display.

Open app/calculator/page.tsx and paste this. We start with the digits and dot, plus a Clear button. Operators come in the next section.

tsx
"use client";

import { useState } from "react";

type Op = "+" | "-" | "×" | "÷" | null;

export default function CalculatorPage() {
  const [display, setDisplay] = useState("0");
  const [previous, setPrevious] = useState<string | null>(null);
  const [operator, setOperator] = useState<Op>(null);

  function inputDigit(d: string) {
    setDisplay(display === "0" ? d : display + d);
  }

  function inputDot() {
    if (!display.includes(".")) setDisplay(display + ".");
  }

  function clearAll() {
    setDisplay("0");
    setPrevious(null);
    setOperator(null);
  }

  const Btn = ({ label, onClick, className = "" }: {
    label: string;
    onClick: () => void;
    className?: string;
  }) => (
    <button
      onClick={onClick}
      className={`h-16 rounded-xl text-xl font-semibold transition active:scale-95 ${className}`}
    >
      {label}
    </button>
  );

  return (
    <main className="mx-auto mt-12 max-w-sm space-y-4 px-4">
      <div className="rounded-2xl bg-slate-900 p-6 text-right text-4xl font-mono text-white shadow">
        {display}
      </div>
      <div className="grid grid-cols-4 gap-3">
        <Btn label="C" onClick={clearAll} className="col-span-2 bg-rose-100 text-rose-700" />
        <Btn label="." onClick={inputDot} className="bg-slate-100 text-slate-700" />
        <Btn label="÷" onClick={() => {}} className="bg-indigo-500 text-white" />
        {/* digit buttons */}
        {["7","8","9"].map((d) => (
          <Btn key={d} label={d} onClick={() => inputDigit(d)} className="bg-white text-slate-800 border border-slate-200" />
        ))}
        <Btn label="×" onClick={() => {}} className="bg-indigo-500 text-white" />
        {["4","5","6"].map((d) => (
          <Btn key={d} label={d} onClick={() => inputDigit(d)} className="bg-white text-slate-800 border border-slate-200" />
        ))}
        <Btn label="-" onClick={() => {}} className="bg-indigo-500 text-white" />
        {["1","2","3"].map((d) => (
          <Btn key={d} label={d} onClick={() => inputDigit(d)} className="bg-white text-slate-800 border border-slate-200" />
        ))}
        <Btn label="+" onClick={() => {}} className="bg-indigo-500 text-white" />
        <Btn label="0" onClick={() => inputDigit("0")} className="col-span-2 bg-white text-slate-800 border border-slate-200" />
        <Btn label="=" onClick={() => {}} className="col-span-2 bg-emerald-500 text-white" />
      </div>
    </main>
  );
}

Save the file. The page now shows a working keypad: digits append to the display, the dot adds a decimal once, and Clear resets everything. The operator buttons do nothing yet. That is the next section.

Why 'use client' at the top

The calculator uses useState and onClick. Both require a Client Component. Without 'use client' on the first line, Next.js renders this as a Server Component and the hooks throw an error.

Section 04 · Math

Wire up the operators and equals

Two handlers do everything: chooseOperator (remembers the operator and stashes the current display) and computeEquals (does the math).

Add these two functions inside CalculatorPage, above the JSX:

tsx
function chooseOperator(next: Exclude<Op, null>) {
  // If the user already picked an operator, compute the running total first.
  if (operator && previous !== null) {
    const result = compute(previous, display, operator);
    setDisplay(result);
    setPrevious(result);
  } else {
    setPrevious(display);
  }
  setOperator(next);
  setDisplay("0");
}

function computeEquals() {
  if (!operator || previous === null) return;
  const result = compute(previous, display, operator);
  setDisplay(result);
  setPrevious(null);
  setOperator(null);
}

function compute(a: string, b: string, op: Exclude<Op, null>): string {
  const x = parseFloat(a);
  const y = parseFloat(b);
  let result: number;
  switch (op) {
    case "+": result = x + y; break;
    case "-": result = x - y; break;
    case "×": result = x * y; break;
    case "÷":
      if (y === 0) return "Error";   // divide by zero guard
      result = x / y;
      break;
  }
  // Trim long floating point tails like 0.30000000000000004
  return parseFloat(result.toFixed(10)).toString();
}

Now wire the operator buttons to chooseOperator:

tsx
<Btn label="÷" onClick={() => chooseOperator("÷")} className="bg-indigo-500 text-white" />
<Btn label="×" onClick={() => chooseOperator("×")} className="bg-indigo-500 text-white" />
<Btn label="-" onClick={() => chooseOperator("-")} className="bg-indigo-500 text-white" />
<Btn label="+" onClick={() => chooseOperator("+")} className="bg-indigo-500 text-white" />
<Btn label="=" onClick={computeEquals} className="col-span-2 bg-emerald-500 text-white" />

Save and try it. 7 + 3 = shows 10. So does 2 × 3 + 4 = (which shows 10, computed left to right because we compute the running total each time the user presses an operator). Divide by zero shows "Error" instead of Infinity.

Why we compute on the next operator press

A pocket calculator computes left to right, not by operator precedence. 2 × 3 + 4 returns 10, not 10 (which would be the same here, but 2 + 3 × 4 returns 20 on a pocket calculator, not 14). If you want true precedence (PEMDAS) you need a real expression parser. For a first calculator, left to right is the right call.

Section 05 · Keyboard

Bonus: keyboard support in fifteen lines

Users expect to type 7 + 3 = on their real keyboard. Adding it takes one useEffect and a switch statement.

Add this above the JSX, importing useEffect from React:

tsx
import { useEffect, useState } from "react";

// inside CalculatorPage component:
useEffect(() => {
  const onKey = (e: KeyboardEvent) => {
    const k = e.key;
    if (/^[0-9]$/.test(k)) inputDigit(k);
    else if (k === ".") inputDot();
    else if (k === "+") chooseOperator("+");
    else if (k === "-") chooseOperator("-");
    else if (k === "*") chooseOperator("×");
    else if (k === "/") { e.preventDefault(); chooseOperator("÷"); }
    else if (k === "Enter" || k === "=") computeEquals();
    else if (k === "Escape" || k === "c" || k === "C") clearAll();
  };
  window.addEventListener("keydown", onKey);
  return () => window.removeEventListener("keydown", onKey);
}, [display, previous, operator]);

The e.preventDefault() on the slash key stops the browser from opening its quick find bar. The dependency array ([display, previous, operator]) ensures the handler always sees the latest state. Without it the handler captures stale values from the first render and the calculator computes wrong answers.

Section 06 · Test it

Five test cases that catch every common bug

If your calculator passes all five, the logic is sound.

7 + 3 = should show 10

The basic addition path. If this fails, your compute function is wrong.

10 ÷ 0 = should show Error

The divide by zero guard. If you see Infinity, the guard is not firing.

2 × 3 + 4 = should show 10

The chained operation test. If you see 14, you forgot to compute the running total when chooseOperator was called the second time.

0.1 + 0.2 = should show 0.3, not 0.30000000000000004

The floating point rounding test. If you see the long tail, your compute function is not calling toFixed.

C should reset everything

Type any sequence, press C, then 5 = and see 5. If you see something else, clearAll is missing one of the setters.

Section 07 · Next steps

What to build next

The calculator is your first useState heavy Client Component. The next tutorial moves on to the other staple of every web app: forms.

Push this code to GitHub. It is the most common React example in any portfolio and it shows you understand state, events, and conditional rendering. Then move on to the next post, Build a Form in Next.js With Server Actions and Zod. Forms touch every part of the framework: Server Actions, validation, error display, and the way data flows from the browser back to the server.

If you want to extend the calculator, three solid next moves are: add a history list of recent calculations using useState with an array, add memory keys (M+, M-, MR) the same way, or persist the running total to localStorage so it survives a page refresh.

Section 08 · Questions

Frequently asked questions

Common stumbles when building this calculator.

Why does my calculator say Hooks can only be called inside a function component?

You forgot 'use client' on the very first line of page.tsx. Server Components cannot use useState. Add the directive (with the quotes), save, and the error disappears.

Why is 0.1 plus 0.2 not exactly 0.3?

It is a JavaScript number precision quirk, not a bug in your code. JavaScript stores numbers as 64 bit floats and some decimals (including 0.1) cannot be represented exactly. The toFixed(10) call in compute trims the long tail back to a sensible value.

Can I use React 19 hooks like useActionState here?

Not for a calculator. useActionState is for forms that submit to a Server Action. The calculator does all its work on the client; plain useState is the right choice.

How do I style this with my own colours?

Replace the Tailwind classes on each button. The colours used here (indigo for operators, emerald for equals, rose for clear) are the Tailwind defaults. Swap them for slate, teal, or any other palette you prefer; nothing else in the code depends on the colours.

Do I need a backend or database for this?

No. Everything happens in the browser. The only reason to add a backend is if you wanted to save calculation history across devices, which is more product feature than tutorial.

Written by Mudassir Khan

Agentic AI consultant and AI systems architect based in Islamabad, Pakistan. CEO of Cube A Cloud. 38+ agentic AI launches delivered for global founders and CTOs.

View agentic AI consulting service

Related service

Agentic AI Consulting

See scope & pricing →

More on this topic

Need an AI systems architect?

Book a 30-minute architecture call. I will sketch the high-level design for your use case and give you an honest view of the trade-offs.

Book a strategy call →