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

A0mineTV
2026-01-12T11:49:39Z
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
πande - trig + hyperbolic functions
-
SHIFTbehavior (inverse trig, etc.) -
ANS(last result) andRAN#(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:
- Tokenize the expression string
- Convert infix to RPN using shunting-yard
- 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 callingMath.sin/cos/tan - If
RAD, use the raw value
For inverse trig in SHIFT mode:
-
asin/acos/atanreturn radians → convert back to degrees ifDEGis 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:
-
sin→asin -
cos→acos -
tan→atan -
sinh→asinh(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
Optional (if included in the repo):
npm run test
npm run build
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 🙂