Building a calculator is often one of the first projects you tackle when learning a new JavaScript framework. It's the perfect exercise: it combines state management (tracking numbers and operations), UI logic, and event handling.
But why stop at a basic, functional calculator? In this article, we'll not only build the logic for a calculator using Vue.js 3 and its fantastic Composition API, but more importantly, we'll give it a modern, professional look inspired by a popular design, leveraging the power of CSS Grid.
The final result is live here, and all the code is available in this GitHub repository:
➡️ https://github.com/VincentCapek/calculator
Step 1: Setting Up the Vue.js Project
To get started, nothing beats Vue's official project scaffolding tool:
npm create vue@latest
For this project, we don't need complex features like Vue Router or Pinia. I simply enabled ESLint and Prettier to ensure clean, consistent code.
Once the project is created, navigate into the directory, install the dependencies, and start the development server:
cd your-project-name
npm install
npm run dev
We'll be working in a single file for this entire tutorial: src/App.vue.
Step 2: The Calculator Logic with the Composition API
This is the "brain" of our application. Thanks to the Composition API and the <script setup> syntax, our logic is both reactive and highly readable.
We need a few state variables to keep track of what's happening:
import { ref } from 'vue';
// The main display of the calculator
const current = ref('');
// The previous number, stored after an operator is clicked
const previous = ref(null);
// The selected operator (+, -, etc.)
const operator = ref(null);
// A flag to know if an operator was just clicked
const operatorClicked = ref(false);
ref() is Vue 3's way of creating reactive variables. Whenever their .value changes, the UI updates automatically. It's like magic!
Next, we define the functions that bring our calculator to life:
- 
clear(): Resets everything. This is our "AC" button.
- 
append(char): Adds a digit or a decimal point to the current value.
- 
setOperator(op): Stores the current value, saves the operator, and gets ready for the next number.
- 
calculate(): Performs the calculation and updates the display.
Here's the complete script, which also includes a little bonus: handling division by zero.
<script setup>
import { ref } from 'vue';
const current = ref('');
const previous = ref(null);
const operator = ref(null);
const operatorClicked = ref(false);
const clear = () => {
  current.value = '';
  previous.value = null;
  operator.value = null;
  operatorClicked.value = false;
};
const append = (char) => {
  if (operatorClicked.value) {
    current.value = '';
    operatorClicked.value = false;
  }
  if (char === '.' && current.value.includes('.')) return;
  current.value = `${current.value}${char}`;
};
const setOperator = (op) => {
  if (current.value === '') return;
  if (operator.value !== null) {
    calculate();
  }
  previous.value = current.value;
  operator.value = op;
  operatorClicked.value = true;
};
const calculate = () => {
  if (operator.value === null || previous.value === null) return;
  let result;
  const prev = parseFloat(previous.value);
  const curr = parseFloat(current.value);
  // Handle division by zero
  if (operator.value === '÷' && curr === 0) {
    current.value = 'Error';
    previous.value = null;
    operator.value = null;
    return;
  }
  switch (operator.value) {
    case '+': result = prev + curr; break;
    case '-': result = prev - curr; break;
    case 'x': result = prev * curr; break;
    case '÷': result = prev / curr; break;
  }
  current.value = String(result);
  previous.value = null;
  operator.value = null;
};
</script>
Step 3: The HTML Structure (The Skeleton)
For better semantics and accessibility, we'll use appropriate HTML elements:
- An <input type="text" readonly>for the display screen.
- Real <button>elements for all the keys.
The template is directly tied to our logic. Notice how we call the functions with arguments directly from the @click event handler, which is a clean and robust approach.
<template>
  <div class="calculator">
    <input type="text" class="calculator-screen" :value="current || '0'" readonly />
    <div class="calculator-keys">
      <!-- Operators -->
      <button type="button" class="operator" @click="setOperator('+')">+</button>
      <!-- ... other operators -->
      <!-- Digits -->
      <button type="button" @click="append('7')">7</button>
      <!-- ... other digits -->
      <!-- Special Functions -->
      <button type="button" class="all-clear" @click="clear">AC</button>
      <button type="button" class="equal-sign operator" @click="calculate">=</button>
    </div>
  </div>
</template>
Step 4: The Magic of CSS (The Design)
This is where our calculator truly comes to life !
1. The Background and "Glassmorphism" Effect
The page body gets a beautiful linear gradient, while the calculator itself has a semi-transparent background with a backdrop-filter. This creates the trendy "glassmorphism" effect, making it look like frosted glass.
body {
  background: linear-gradient(to right, #6190e8, #a7bfe8);
}
.calculator {
  box-shadow: 0 0 40px 0px rgba(0, 0, 0, 0.15);
  background-color: rgba(255, 255, 255, .75);
  backdrop-filter: blur(5px);
}
2. A Perfect Layout with CSS Grid
The button layout is managed with display: grid, making it robust and easy to maintain. grid-gap creates a uniform spacing between each button.
.calculator-keys {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-gap: 20px;
  padding: 20px;
}
3. The Spanning "Equals" Button
The highlight of this design is the = button, which spans multiple rows. This is incredibly simple to achieve with the grid-area property. The syntax grid-area: row-start / col-start / row-end / col-end; gives us complete control.
.equal-sign {
  /* Starts on grid row 2, column 4, and spans down to row 6, column 5 */
  grid-area: 2 / 4 / 6 / 5;
  height: 100%;
}
4. Hover Effects that Respect Contrast
This is a crucial UX detail. A generic grey :hover state on all buttons would ruin the design of the colored ones (white text on a light grey background is nearly unreadable). The solution is to define specific hover states for each button type, simply by making their base color slightly lighter.
/* Hover for numeric buttons */
button:hover {
  background-color: #f0f0f0;
}
/* SPECIFIC hover for operators */
.operator:hover {
  background-color: #469dcb; /* A lighter blue */
}
/* SPECIFIC hover for the "AC" button */
.all-clear:hover {
  background-color: #f26f74; /* A lighter red */
}
Conclusion
And there you have it! We've built a calculator that is not only functional, thanks to the reactive power of Vue.js 3, but is also visually stunning using modern CSS techniques.
This project is a great example of how fundamentals are key, but with a bit of attention to design and user experience, you can turn a simple learning exercise into a portfolio piece you can be proud of.
Thanks for reading, and happy coding !
 
 
              

 
    
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.