
🧮 Простий калькулятор на JavaScript: покроковий посібник
Потрібен простий калькулятор на сайті, калькулятор доставки, іпотечний чи просто «порахувати щось на льоту»? Писати для цього бекенд, однаково що стріляти з гармати по горобцях. JavaScript на клієнті дає раду за вечір.
Проблема більшості гайдів: вони дають вам jQuery-версію 2017 року з eval(), а це діра в безпеці, про яку вам не скажуть. Ми підемо іншим шляхом: зберемо калькулятор на чистому JavaScript, з нормальним верстанням і без повістки «встановіть три залежності».
Наприкінці статті у вас буде робочий код, який додає, віднімає, множить і ділить, а також розуміння, чому eval() краще не використовувати.
💡 Швидкий огляд:
- HTML: зберіть розмітку калькулятора на CSS Grid, без table і center
- CSS: додайте адаптивну сітку, стилізацію кнопок і дисплея
- JavaScript: напишіть захоплення кліків, збирання виразу й обчислення без eval()
- Безпека: дізнайтеся, чим замінити eval() і чому це важливо
Крок 1: HTML, розмітка калькулятора
Спочатку збираємо скелет. Жодних <center> і <table border="4"> з 2005-го, використовуємо CSS Grid: чотири колонки для цифр і операторів плюс рядок дисплея зверху.
Створіть файл index.html і додайте таку розмітку:
1 <div class="calculator"> 2 <input class="display" type="text" readonly> 3 4 <div class="buttons"> 5 <button class="btn-clear" data-action="clear">C</button> 6 <button class="btn-operator" data-action="operator" data-value="/">/</button> 7 <button class="btn-operator" data-action="operator" data-value="*">*</button> 8 <button class="btn-operator" data-action="operator" data-value="-">-</button> 9 10 <button class="btn-number" data-action="number" data-value="7">7</button> 11 <button class="btn-number" data-action="number" data-value="8">8</button> 12 <button class="btn-number" data-action="number" data-value="9">9</button> 13 <button class="btn-operator" data-action="operator" data-value="+">+</button> 14 15 <button class="btn-number" data-action="number" data-value="4">4</button> 16 <button class="btn-number" data-action="number" data-value="5">5</button> 17 <button class="btn-number" data-action="number" data-value="6">6</button> 18 <button class="btn-equals" data-action="calculate">=</button> 19 20 <button class="btn-number" data-action="number" data-value="1">1</button> 21 <button class="btn-number" data-action="number" data-value="2">2</button> 22 <button class="btn-number" data-action="number" data-value="3">3</button> 23 <button class="btn-number btn-zero" data-action="number" data-value="0">0</button> 24 </div> 25 </div>
Тут дисплей (<input>), поле лише для читання: користувач не вводить дані з клавіатури, а натискає кнопки. Це захищає від сміттєвих символів у виразі. Кнопки розділені атрибутами data-action: «number», «operator», «calculate» і «clear», щоб скрипт знав, що робити з кліком. Атрибут data-value зберігає значення кнопки: цифру або знак операції.
Зверніть увагу: дисплей з атрибутом readonly, користувач не зможе вписати туди alert('взлом') з клавіатури.
Крок 2: CSS, зовнішній вигляд
Тепер стилі. Без CSS калькулятор виглядає як стос кнопок, сітка все виправляє. У тому ж index.html, всередині <head>, додайте:
1 <style> 2 .calculator { 3 max-width: 320px; 4 margin: 0 auto; 5 font-family: 'Segoe UI', sans-serif; 6 } 7 8 .display { 9 width: 100%; 10 padding: 16px 12px; 11 font-size: 2rem; 12 text-align: right; 13 border: 2px solid #ccc; 14 border-radius: 6px; 15 margin-bottom: 10px; 16 box-sizing: border-box; 17 background: #f9f9f9; 18 } 19 20 .buttons { 21 display: grid; 22 grid-template-columns: repeat(4, 1fr); 23 gap: 6px; 24 } 25 26 .buttons button { 27 padding: 18px 0; 28 font-size: 1.3rem; 29 border: none; 30 border-radius: 6px; 31 cursor: pointer; 32 background: #e9ecef; 33 transition: background 0.15s; 34 } 35 36 .buttons button:hover { 37 background: #dee2e6; 38 } 39 40 .btn-operator { 41 background: #ff922b !important; 42 color: #fff; 43 } 44 45 .btn-operator:hover { 46 background: #f76707 !important; 47 } 48 49 .btn-equals { 50 background: #228be6 !important; 51 color: #fff; 52 } 53 54 .btn-equals:hover { 55 background: #1971c2 !important; 56 } 57 58 .btn-clear { 59 background: #fa5252 !important; 60 color: #fff; 61 } 62 63 .btn-clear:hover { 64 background: #e03131 !important; 65 } 66 67 .btn-zero { 68 grid-column: span 2; 69 } 70 </style>
Сітка grid-template-columns: repeat(4, 1fr) дає рівну клавіатуру з чотирьох колонок, кнопки самі розподіляються. Кнопка «0» розтягнута на дві колонки через grid-column: span 2, стандартний прийом для калькуляторної розкладки.
Кольори тут функціональні: помаранчевий, оператори, синій, «дорівнює», червоний, скидання. Користувач миттєво зчитує призначення кнопки ще до прочитання тексту.
Крок 3: JavaScript, логіка без eval()
Головна частина. Підключімо скрипт у кінці <body> того ж index.html:
1 <script> 2 const display = document.querySelector('.display'); 3 4 let expression = ''; 5 6 document.querySelector('.buttons').addEventListener('click', function (e) { 7 const btn = e.target.closest('button'); 8 if (!btn) return; 9 10 const action = btn.dataset.action; 11 12 if (action === 'number') { 13 expression += btn.dataset.value; 14 display.value = expression; 15 } 16 17 if (action === 'operator') { 18 if (expression === '') return; 19 const last = expression.slice(-1); 20 if (['+', '-', '*', '/'].includes(last)) return; 21 expression += btn.dataset.value; 22 display.value = expression; 23 } 24 25 if (action === 'calculate') { 26 if (expression === '') return; 27 const result = safeCalculate(expression); 28 display.value = result; 29 expression = String(result); 30 } 31 32 if (action === 'clear') { 33 expression = ''; 34 display.value = ''; 35 } 36 }); 37 38 function safeCalculate(expr) { 39 try { 40 const fn = new Function('return ' + expr); 41 const result = fn(); 42 if (typeof result !== 'number' || !isFinite(result)) { 43 return 'Ошибка'; 44 } 45 return Math.round(result * 1e10) / 1e10; 46 } catch (e) { 47 return 'Ошибка'; 48 } 49 } 50 </script>
Розберімо, що тут відбувається, порядково.
Захоплення кліків. addEventListener вішається не на кожну кнопку, а на контейнер .buttons, це делегування подій. При кліку e.target.closest('button') знаходить найближчу кнопку (навіть якщо клікнули по тексту всередині неї). btn.dataset.action читає атрибут data-action, і далі логіка розгалужується.
Збирання виразу. Кожне натискання цифри дописує її до рядка expression. Оператор дописується, тільки якщо вираз не порожній і останній символ, не оператор (захист від подвійних ++ і */). Дисплей оновлюється на кожне натискання, користувач бачить рядок цілком, як на справжньому калькуляторі.
Обчислення. Замість eval(), конструктор new Function(). Він теж виконує рядок як код, але з однією відмінністю: eval() має доступ до локальної області видимості, а Function, лише до глобальної. Це не срібна куля, але для клієнтського калькулятора, куди користувач сам вводить вираз, достатній рівень ізоляції. Після обчислення результат округлюється до 10 знаків після коми, інакше 0.1 + 0.2 перетвориться на 0.30000000000000004.
Скидання. Кнопка «C» обнуляє і expression, і дисплей.
Перевірка на дурня. Якщо вираз порожній, calculate і operator мовчки ігноруються. Ділення на нуль дає Infinity, isFinite() ловить це й виводить «Помилка».
Крок 4: збираємо все разом
З'єднаймо три частини в одному файлі. Ось повний index.html, який можна відкрити в браузері:
1 <!DOCTYPE html> 2 <html lang="ru"> 3 <head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>Калькулятор на JavaScript</title> 7 <style> 8 .calculator { 9 max-width: 320px; 10 margin: 40px auto; 11 font-family: 'Segoe UI', sans-serif; 12 } 13 .display { 14 width: 100%; 15 padding: 16px 12px; 16 font-size: 2rem; 17 text-align: right; 18 border: 2px solid #ccc; 19 border-radius: 6px; 20 margin-bottom: 10px; 21 box-sizing: border-box; 22 background: #f9f9f9; 23 } 24 .buttons { 25 display: grid; 26 grid-template-columns: repeat(4, 1fr); 27 gap: 6px; 28 } 29 .buttons button { 30 padding: 18px 0; 31 font-size: 1.3rem; 32 border: none; 33 border-radius: 6px; 34 cursor: pointer; 35 background: #e9ecef; 36 transition: background 0.15s; 37 } 38 .buttons button:hover { background: #dee2e6; } 39 .btn-operator { background: #ff922b !important; color: #fff; } 40 .btn-operator:hover { background: #f76707 !important; } 41 .btn-equals { background: #228be6 !important; color: #fff; } 42 .btn-equals:hover { background: #1971c2 !important; } 43 .btn-clear { background: #fa5252 !important; color: #fff; } 44 .btn-clear:hover { background: #e03131 !important; } 45 .btn-zero { grid-column: span 2; } 46 </style> 47 </head> 48 <body> 49 <div class="calculator"> 50 <input class="display" type="text" readonly> 51 <div class="buttons"> 52 <button class="btn-clear" data-action="clear">C</button> 53 <button class="btn-operator" data-action="operator" data-value="/">/</button> 54 <button class="btn-operator" data-action="operator" data-value="*">*</button> 55 <button class="btn-operator" data-action="operator" data-value="-">-</button> 56 <button class="btn-number" data-action="number" data-value="7">7</button> 57 <button class="btn-number" data-action="number" data-value="8">8</button> 58 <button class="btn-number" data-action="number" data-value="9">9</button> 59 <button class="btn-operator" data-action="operator" data-value="+">+</button> 60 <button class="btn-number" data-action="number" data-value="4">4</button> 61 <button class="btn-number" data-action="number" data-value="5">5</button> 62 <button class="btn-number" data-action="number" data-value="6">6</button> 63 <button class="btn-equals" data-action="calculate">=</button> 64 <button class="btn-number" data-action="number" data-value="1">1</button> 65 <button class="btn-number" data-action="number" data-value="2">2</button> 66 <button class="btn-number" data-action="number" data-value="3">3</button> 67 <button class="btn-number btn-zero" data-action="number" data-value="0">0</button> 68 </div> 69 </div> 70 71 <script> 72 const display = document.querySelector('.display'); 73 let expression = ''; 74 document.querySelector('.buttons').addEventListener('click', function (e) { 75 const btn = e.target.closest('button'); 76 if (!btn) return; 77 const action = btn.dataset.action; 78 if (action === 'number') { 79 expression += btn.dataset.value; 80 display.value = expression; 81 } 82 if (action === 'operator') { 83 if (expression === '') return; 84 const last = expression.slice(-1); 85 if (['+', '-', '*', '/'].includes(last)) return; 86 expression += btn.dataset.value; 87 display.value = expression; 88 } 89 if (action === 'calculate') { 90 if (expression === '') return; 91 const result = safeCalculate(expression); 92 display.value = result; 93 expression = String(result); 94 } 95 if (action === 'clear') { 96 expression = ''; 97 display.value = ''; 98 } 99 }); 100 function safeCalculate(expr) { 101 try { 102 const fn = new Function('return ' + expr); 103 const result = fn(); 104 if (typeof result !== 'number' || !isFinite(result)) return 'Ошибка'; 105 return Math.round(result * 1e10) / 1e10; 106 } catch (e) { 107 return 'Ошибка'; 108 } 109 } 110 </script> 111 </body> 112 </html>
Збережіть, відкрийте в браузері, калькулятор працює одразу, без сервера, без npm install. На практиці такого коду достатньо для калькулятора доставки на лендинг або форми розрахунку знижки.

Чому не eval(): безпека, про яку мовчать початківські гайди
Якщо ви читали старі туторіали, напевно бачили eval(expression). Так, це працює. Але проблема в тому, що eval() виконує будь-який JavaScript-код, переданий рядком. Якщо в дисплей потрапить alert(document.cookie), він виконається. А якщо дисплей відкритий для ручного введення, туди можна підставити й дещо серйозніше.
У нашому випадку дисплей працює в режимі readonly, вираз збирається лише кліками по кнопках. Ризик мінімальний. Але звичка використовувати eval() погана: одного разу ви скопіюєте цей патерн у форму з користувацьким введенням і отримаєте XSS-діру.
Альтернатив три, за зростанням складності:
Підхід | Захист | Складність |
|---|---|---|
| Не бачить локальні змінні, лише глобальні | Мінімальна |
Ручний парсер (розбір рядка за операторами) | Повний контроль над виразом | Середня |
Web Worker + ізольований контекст | Вираз не торкається DOM взагалі | Висока |
Для навчального калькулятора і більшості реальних сценаріїв new Function() з перевіркою результату, достатньо. Але якщо ви вбудовуєте калькулятор у форму e-commerce (ціна × кількість), не партеся з парсингом рядка взагалі: беріть значення з полів і рахуйте напряму: priceInput.value * qtyInput.value. Жоден eval там не потрібен від самого початку.
Як розширити: клавіатурне введення та ланцюжки операцій
Коли базова версія запрацювала, додайте дві фічі, які перетворюють саморобку на інструмент:
Введення з клавіатури. Повісьте addEventListener('keydown', ...) на document і зіставте клавіші з кнопками: цифри й крапка до data-action="number", Enter до «дорівнює», Escape до «C», +-*/ до операторів. Літери та спецсимволи ігноруйте.
Ланцюжки операцій. Зараз наш калькулятор рахує «все одразу». Але якщо натиснути 5 + 3 =, а потім * 2, він не продовжить, expression уже скинуто в результат. Виправляється просто: після = не перезаписуйте expression, а зберігайте останній результат в окрему змінну lastResult. При наступному операторі починайте expression з lastResult.
Обидві доробки, на 15-20 рядків коду кожна. Спробуйте самі як вправу: це найкращий спосіб зрозуміти, що калькулятор, не «навчальний проєкт», а нормальна інженерна міні-задача.
⁉️🤔 Часті запитання
Чи можна обійтися взагалі без JavaScript?
Для чотирьох арифметичних дій на клієнті, ні. HTML і CSS — це розмітка й стилі, обчислень вони не роблять. Серверний обрахунок через PHP означає перезавантаження сторінки на кожне натискання «=». JavaScript тут, єдиний практичний варіант. Існують CSS-трюки з
calc()і чекбоксами, що імітують логіку, але це атракціон, а не калькулятор: максимум на одну дію, без скидання, з жахливим кодом.
Чим цей підхід відрізняється від «візьми готовий npm-пакет»?
Жодною залежністю.
npm install calculator-libтягне за собою ще 40 пакетів, збирач і півмегабайта бандла, заради додавання двох чисел. Наш варіант: один HTML-файл, 90 рядків JavaScript, відкривається в будь-якому браузері без сервера. Для більшості типових задач, калькулятор доставки, іпотечний розрахунок на лендингу, форма «порахувати вартість», цього достатньо.
Чому не jQuery?
jQuery для калькулятора, як мікроавтобус по хліб. У 2017-му це ще мало сенс:
$('.btn')коротше, ніжdocument.querySelectorAll('.btn'), і браузери поводилися по-різному. СьогодніaddEventListenerіclosest()працюють однаково в усіх браузерах, а тягнути 87 КБ стисненого jQuery заради п'яти обробників кліку, зайве. Якщо на проєкті jQuery вже є, використовуйте, але для нового коду пишіть на чистому JS.
Що робити з рухомою комою? 0.1 + 0.2 все одно дає 0.30000000000000004.
У коді ми поставили
Math.round(result * 1e10) / 1e10, це прибирає хвіст для десяти знаків. Для фінансових розрахунків (ціни, відсотки) використовуйте цілі числа, рахуйте в копійках, а не в $х:(100 + 200) / 100 = 3.00замість0.1 + 0.2. ЖоденevalіFunctionтут не потрібні, просто беріть числа з полів і додавайте.
Чи варто писати свій калькулятор чи взяти готовий?
Якщо вам потрібен калькулятор на одну сторінку, напишіть самі за годину за цим гайдом. Коду менше 100 рядків, залежностей нуль, поведінку контролюєте ви.
Якщо вбудовуєте розрахунок у WooCommerce або форму замовлення, не пишіть парсер виразів. Беріть значення з полів введення і рахуйте напряму: total = basePrice + optionsSum. Це надійніше, простіше й безпечніше за будь-яке рядкове обчислення.
Код зі статті, робоча основа. Змінюйте розкладку, додавайте відсотки, пам'ять (кнопки M+/M-/MR), історію операцій. Напишіть калькулятор один раз своїми руками, і питання «як це працює» закриється назавжди.



