Building a Casio‑Style Scientific Calculator with Vue 3 + TypeScript

A0mineTV

A0mineTV

2026-01-12T11:49:39Z

3 min read

I wanted a small project that touches real front-end skills (UI, events, state, edge cases) while still being fun. So I built a Casio fx‑991EX inspired scientific calculator in Vue 3 + TypeScript.

✅ Repo (code + updates): https://github.com/VincentCapek/vue-scientific-calculator


What I built

The app is a “real calculator” experience:

  • LCD-like display with DEG/RAD, expression preview, and a large result line
  • A Casio‑style keypad (numbers, operators, scientific functions, AC/DEL/=)
  • Mouse/touch + keyboard shortcuts (Enter, Backspace, Esc…)
  • A small but solid math engine that supports:
    • operator precedence + parentheses
    • power ^
    • constants like π and e
    • trig + hyperbolic functions
    • SHIFT behavior (inverse trig, etc.)
    • ANS (last result) and RAN# (random)

Why Vue for a calculator ?

A calculator is a surprisingly good UI exercise:

  • many small components (buttons, display, grid)
  • lots of state transitions (SHIFT, mode changes, error states)
  • tiny UX details (press states, deletion behavior, formatting)
  • and the logic must be predictable and testable

Vue 3’s composition API makes it clean to separate:

  • UI components (pure rendering)
  • state & interactions (a composable)
  • math evaluation (a standalone library)

Project structure

The goal was to keep a portfolio‑friendly structure:

  • components/
    • CalculatorShell.vue — the “device” shell (top branding + display + keypad)
    • DisplayPanel.vue — LCD display (mode, expression, value)
    • KeypadGrid.vue — keypad layout
    • CalcButton.vue — reusable button component (variants + pressed states)
  • composables/useCalculator.ts
    • single source of truth for state + input handling
  • lib/evaluator/
    • tokenize.ts — convert a string into tokens
    • toRpn.ts — shunting-yard algorithm (infix -> RPN)
    • evalRpn.ts — evaluate RPN safely
    • functions.ts — trig, DEG/RAD conversion, factorial, etc.

This way, the evaluator can be tested without touching the UI.


The UI: making it feel like a real calculator

The Casio look is mostly about:

  • spacing
  • button colors and hierarchy
  • a slightly “beveled” depth effect
  • clear separation between the screen and the keypad

One thing that helped a lot: treating the keypad as data.

Instead of hardcoding rows of buttons manually, you can define a config array (label, action, variant), then render a grid.

That makes it easy to:

  • change labels when SHIFT is enabled
  • disable keys temporarily
  • add new rows/features later without rewriting the layoutµ

Input handling (click + keyboard)

A good calculator should work both ways:

  • Clicking buttons (mobile friendly)
  • Keyboard shortcuts for quick testing

Typical mappings:

  • Enter → evaluate (=)
  • Backspace → delete (DEL)
  • Escape → clear (AC)
  • + - * / ( ) ^ % → operators and parentheses

In the composable, the main idea is to route everything to a single function, something like:

  • pressKey(key: KeyDef)
  • handleKeyboard(event: KeyboardEvent)
  • then update the expression/value based on the key type (digit, operator, function, control)

The math engine (tokenize → RPN → evaluate)

Instead of using a heavy dependency, I implemented a lightweight evaluator:

  1. Tokenize the expression string
  2. Convert infix to RPN using shunting-yard
  3. Evaluate the RPN stack

Why this approach ?

  • correct operator precedence
  • parentheses are straightforward
  • easy to extend (add functions/operators)
  • very testable

Handling trig with DEG/RAD

The only tricky part is angle conversion:

  • If mode is DEG, convert degrees to radians before calling Math.sin/cos/tan
  • If RAD, use the raw value

For inverse trig in SHIFT mode:

  • asin/acos/atan return radians → convert back to degrees if DEG is active

SHIFT behavior (the “calculator feel”)

SHIFT works like a real calculator:

  • it toggles on
  • the labels (and behavior) of some keys change
  • after one shifted action, SHIFT toggles off again

Examples:

  • sinasin
  • cosacos
  • tanatan
  • sinhasinh (etc.)

This is a small detail, but it makes the UI feel instantly more authentic.


Edge cases (the unsexy part)

Calculators are all about edge cases:

  • multiple decimals (1.2.3)
  • unary minus (-5, 2*-3)
  • division by zero
  • factorial of non-integers
  • mismatched parentheses
  • overflow / Infinity

I like handling these by returning a consistent “Error” state that the display can show, while keeping the previous answer available as ANS for recovery.


Testing (Vitest)

Even a small project benefits from tests, especially for the evaluator.

Good starter tests:

  • precedence: 2+3*4 = 14
  • parentheses: (2+3)*4 = 20
  • associativity: 2^3^2 = 512 (if right associative)
  • trig mode: sin(90) in DEG = 1
  • factorial: fact(5) = 120
  • error cases: division by zero, invalid tokens, etc.

Run it locally

npm install
npm run dev
Enter fullscreen mode Exit fullscreen mode

Optional (if included in the repo):

npm run test
npm run build
Enter fullscreen mode Exit fullscreen mode

What I’d improve next

If I extend this project, I’d add:

  • History (previous expressions/results)
  • Memory keys (M+, M-, MR, MC)
  • Better percentage rules (calculator-like percent behavior)
  • More display formats (SCI/ENG, precision settings)
  • More tests + property-based tests for random expressions

Closing thoughts

Small projects like this are perfect to sharpen “real” front-end skills:
UI structure, state management, event handling, and careful logic.

If you want to explore the code, it’s here:
https://github.com/VincentCapek/vue-scientific-calculator

Thanks for reading — and if you have ideas to make it even closer to a real fx‑991EX experience, I’m all ears 🙂