UI Components Medium
Calculator
A clean, functional calculator with support for basic arithmetic operations and keyboard support.
Open in Lab
MCP
vanilla-js css react tailwind vue svelte
Targets: TS JS HTML React Vue Svelte
Code
:root {
--calc-bg: #111827;
--calc-surface: #1f2937;
--calc-text: #f9fafb;
--calc-muted: #9ca3af;
--calc-accent: #f59e0b;
--calc-utility: #374151;
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
height: 100%;
}
body {
background: var(--calc-bg);
color: var(--calc-text);
font-family: "Inter", system-ui, sans-serif;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 1rem;
}
.calculator-widget {
width: 100%;
max-width: 320px;
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 32px;
padding: clamp(1rem, 4vw, 1.5rem);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
user-select: none;
}
.calc-top {
padding: 0.5rem 0.5rem 1.5rem;
text-align: right;
min-height: 110px;
display: flex;
flex-direction: column;
justify-content: flex-end;
}
.calc-history {
color: var(--calc-muted);
font-size: 0.875rem;
margin-bottom: 0.5rem;
height: 1.25rem;
overflow: hidden;
text-overflow: ellipsis;
}
.calc-display {
color: var(--calc-text);
font-size: clamp(2rem, 8vw, 3rem);
font-weight: 300;
overflow-x: auto;
white-space: nowrap;
scrollbar-width: none;
line-height: 1.1;
}
.calc-display::-webkit-scrollbar {
display: none;
}
.calc-buttons {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.75rem;
}
.calc-btn {
aspect-ratio: 1 / 1;
border: none;
border-radius: 50%;
font-size: 1.25rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
background: var(--calc-surface);
color: var(--calc-text);
}
.calc-btn:active {
transform: scale(0.95);
opacity: 0.8;
}
.calc-btn.double {
aspect-ratio: auto;
grid-column: span 2;
border-radius: 30px;
justify-content: flex-start;
padding-left: 1.75rem;
}
.calc-btn.utility {
background: var(--calc-utility);
}
.calc-btn.operator {
background: var(--calc-accent);
font-size: 1.5rem;
}
.calc-btn.equals {
background: var(--calc-accent);
}
.calc-btn.utility:hover {
background: #4b5563;
}
.calc-btn.surface:hover {
background: #374151;
}
.calc-btn.operator:hover {
background: #d97706;
}
@media (max-width: 360px) {
.calc-buttons {
gap: 0.5rem;
}
}const display = document.getElementById("calc-display");
const history = document.getElementById("calc-history");
const buttons = document.querySelectorAll(".calc-btn");
let currentInput = "0";
let previousInput = "";
let operation = null;
let shouldResetDisplay = false;
function updateDisplay() {
display.textContent = currentInput;
}
function handleNumber(num) {
if (currentInput === "0" || shouldResetDisplay) {
currentInput = num;
shouldResetDisplay = false;
} else {
currentInput += num;
}
}
function handleDecimal() {
if (shouldResetDisplay) {
currentInput = "0.";
shouldResetDisplay = false;
return;
}
if (!currentInput.includes(".")) {
currentInput += ".";
}
}
function handleOperator(op) {
if (operation !== null) calculate();
previousInput = currentInput;
operation = op;
shouldResetDisplay = true;
history.textContent = `${previousInput} ${getOpSymbol(op)}`;
}
function getOpSymbol(op) {
const symbols = {
add: "+",
subtract: "-",
multiply: "×",
divide: "÷",
};
return symbols[op] || "";
}
function calculate() {
if (operation === null || shouldResetDisplay) return;
let result;
const prev = parseFloat(previousInput);
const current = parseFloat(currentInput);
switch (operation) {
case "add":
result = prev + current;
break;
case "subtract":
result = prev - current;
break;
case "multiply":
result = prev * current;
break;
case "divide":
if (current === 0) {
alert("Cannot divide by zero");
clear();
return;
}
result = prev / current;
break;
default:
return;
}
currentInput = String(parseFloat(result.toFixed(8)));
operation = null;
history.textContent = "";
shouldResetDisplay = true;
}
function clear() {
currentInput = "0";
previousInput = "";
operation = null;
history.textContent = "";
}
function toggleSign() {
currentInput = String(parseFloat(currentInput) * -1);
}
function handlePercent() {
currentInput = String(parseFloat(currentInput) / 100);
}
// Click Listeners
buttons.forEach((btn) => {
btn.addEventListener("click", () => {
const action = btn.dataset.action;
const value = btn.textContent;
if (!action) {
if (value === ".") handleDecimal();
else handleNumber(value);
} else {
switch (action) {
case "clear":
clear();
break;
case "toggle-sign":
toggleSign();
break;
case "percent":
handlePercent();
break;
case "calculate":
calculate();
break;
default:
handleOperator(action);
}
}
updateDisplay();
});
});
// Keyboard Support
window.addEventListener("keydown", (e) => {
if (e.key >= "0" && e.key <= "9") handleNumber(e.key);
if (e.key === ".") handleDecimal();
if (e.key === "Enter" || e.key === "=") calculate();
if (e.key === "Escape") clear();
if (e.key === "+") handleOperator("add");
if (e.key === "-") handleOperator("subtract");
if (e.key === "*") handleOperator("multiply");
if (e.key === "/") {
e.preventDefault();
handleOperator("divide");
}
updateDisplay();
});<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Calculator</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="calculator-widget">
<div class="calc-top">
<div id="calc-history" class="calc-history"></div>
<div id="calc-display" class="calc-display">0</div>
</div>
<div class="calc-buttons">
<button class="calc-btn utility" data-action="clear">AC</button>
<button class="calc-btn utility" data-action="toggle-sign">±</button>
<button class="calc-btn utility" data-action="percent">%</button>
<button class="calc-btn operator" data-action="divide">÷</button>
<button class="calc-btn number">7</button>
<button class="calc-btn number">8</button>
<button class="calc-btn number">9</button>
<button class="calc-btn operator" data-action="multiply">×</button>
<button class="calc-btn number">4</button>
<button class="calc-btn number">5</button>
<button class="calc-btn number">6</button>
<button class="calc-btn operator" data-action="subtract">−</button>
<button class="calc-btn number">1</button>
<button class="calc-btn number">2</button>
<button class="calc-btn number">3</button>
<button class="calc-btn operator" data-action="add">+</button>
<button class="calc-btn number double">0</button>
<button class="calc-btn number">.</button>
<button class="calc-btn equals" data-action="calculate">=</button>
</div>
</div>
<script src="script.js"></script>
</body>
</html>import { useState } from "react";
const BUTTONS = [
["C", "+/-", "%", "÷"],
["7", "8", "9", "×"],
["4", "5", "6", "−"],
["1", "2", "3", "+"],
["0", ".", "="],
];
export default function CalculatorRC() {
const [display, setDisplay] = useState("0");
const [prev, setPrev] = useState<string | null>(null);
const [op, setOp] = useState<string | null>(null);
const [waitNext, setWaitNext] = useState(false);
function press(key: string) {
if (key === "C") {
setDisplay("0");
setPrev(null);
setOp(null);
setWaitNext(false);
return;
}
if (key === "+/-") {
setDisplay((d) => String(-parseFloat(d)));
return;
}
if (key === "%") {
setDisplay((d) => String(parseFloat(d) / 100));
return;
}
if (["÷", "×", "−", "+"].includes(key)) {
setPrev(display);
setOp(key);
setWaitNext(true);
return;
}
if (key === "=") {
if (!op || !prev) return;
const a = parseFloat(prev);
const b = parseFloat(display);
let result = 0;
if (op === "÷") result = a / b;
else if (op === "×") result = a * b;
else if (op === "−") result = a - b;
else if (op === "+") result = a + b;
const str = String(parseFloat(result.toPrecision(10)));
setDisplay(str);
setPrev(null);
setOp(null);
setWaitNext(false);
return;
}
if (key === ".") {
if (display.includes(".") && !waitNext) return;
setDisplay(waitNext ? "0." : display + ".");
setWaitNext(false);
return;
}
const next = waitNext ? key : display === "0" ? key : display + key;
setDisplay(next);
setWaitNext(false);
}
function btnClass(key: string) {
if (["÷", "×", "−", "+", "="].includes(key))
return "bg-[#f6901f] hover:bg-[#e8830a] text-white font-semibold";
if (["C", "+/-", "%"].includes(key))
return "bg-[#a5a5a5] hover:bg-[#bfbfbf] text-black font-semibold";
return "bg-[#333333] hover:bg-[#4a4a4a] text-white";
}
return (
<div className="min-h-screen bg-[#0d1117] flex items-center justify-center p-6">
<div className="w-[280px] bg-black rounded-3xl overflow-hidden shadow-2xl">
<div className="px-5 pt-6 pb-3 text-right">
<p className="text-[#8b949e] text-sm h-5 truncate">{op ? `${prev} ${op}` : ""}</p>
<p className="text-white text-[48px] font-light leading-none truncate">
{display.length > 9 ? parseFloat(display).toExponential(3) : display}
</p>
</div>
<div className="grid grid-cols-4 gap-px bg-[#1c1c1c] p-px">
{BUTTONS.flat().map((key, i) => (
<button
key={i}
onClick={() => press(key)}
className={`${btnClass(key)} ${key === "0" ? "col-span-2" : ""} h-16 rounded-none text-xl transition-opacity active:opacity-70 flex items-center ${key === "0" ? "justify-start px-6" : "justify-center"}`}
>
{key}
</button>
))}
</div>
</div>
</div>
);
}<script setup>
import { ref, computed } from "vue";
const BUTTONS = [
["C", "+/-", "%", "÷"],
["7", "8", "9", "×"],
["4", "5", "6", "−"],
["1", "2", "3", "+"],
["0", ".", "="],
];
const display = ref("0");
const prev = ref(null);
const op = ref(null);
const waitNext = ref(false);
function press(key) {
if (key === "C") {
display.value = "0";
prev.value = null;
op.value = null;
waitNext.value = false;
return;
}
if (key === "+/-") {
display.value = String(-parseFloat(display.value));
return;
}
if (key === "%") {
display.value = String(parseFloat(display.value) / 100);
return;
}
if (["÷", "×", "−", "+"].includes(key)) {
prev.value = display.value;
op.value = key;
waitNext.value = true;
return;
}
if (key === "=") {
if (!op.value || !prev.value) return;
const a = parseFloat(prev.value);
const b = parseFloat(display.value);
let result = 0;
if (op.value === "÷") result = a / b;
else if (op.value === "×") result = a * b;
else if (op.value === "−") result = a - b;
else if (op.value === "+") result = a + b;
display.value = String(parseFloat(result.toPrecision(10)));
prev.value = null;
op.value = null;
waitNext.value = false;
return;
}
if (key === ".") {
if (display.value.includes(".") && !waitNext.value) return;
display.value = waitNext.value ? "0." : display.value + ".";
waitNext.value = false;
return;
}
display.value = waitNext.value ? key : display.value === "0" ? key : display.value + key;
waitNext.value = false;
}
function btnClass(key) {
if (["÷", "×", "−", "+", "="].includes(key)) return "op";
if (["C", "+/-", "%"].includes(key)) return "func";
return "num";
}
const displayText = computed(() =>
display.value.length > 9 ? parseFloat(display.value).toExponential(3) : display.value
);
const subDisplay = computed(() => (op.value ? `${prev.value} ${op.value}` : ""));
const flat = BUTTONS.flat();
</script>
<template>
<div class="page">
<div class="calc">
<div class="display-area">
<p class="sub">{{ subDisplay }}</p>
<p class="main">{{ displayText }}</p>
</div>
<div class="grid">
<button
v-for="(key, i) in flat"
:key="i"
:class="[btnClass(key), { zero: key === '0' }]"
@click="press(key)"
>
{{ key }}
</button>
</div>
</div>
</div>
</template>
<style scoped>
.page { min-height: 100vh; background: #0d1117; display: flex; align-items: center; justify-content: center; padding: 1.5rem; }
.calc { width: 280px; background: black; border-radius: 1.5rem; overflow: hidden; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.25); }
.display-area { padding: 1.5rem 1.25rem 0.75rem; text-align: right; }
.sub { color: #8b949e; font-size: 0.875rem; height: 1.25rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.main { color: white; font-size: 48px; font-weight: 300; line-height: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1px; background: #1c1c1c; padding: 1px; }
button {
height: 4rem;
border: none;
border-radius: 0;
font-size: 1.25rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.1s;
font-family: inherit;
}
button:active { opacity: 0.7; }
.num { background: #333333; color: white; }
.num:hover { background: #4a4a4a; }
.op { background: #f6901f; color: white; font-weight: 600; }
.op:hover { background: #e8830a; }
.func { background: #a5a5a5; color: black; font-weight: 600; }
.func:hover { background: #bfbfbf; }
.zero { grid-column: span 2; justify-content: flex-start; padding-left: 1.5rem; }
</style><script>
const BUTTONS = [
["C", "+/-", "%", "÷"],
["7", "8", "9", "×"],
["4", "5", "6", "−"],
["1", "2", "3", "+"],
["0", ".", "="],
];
let display = "0";
let prev = null;
let op = null;
let waitNext = false;
function press(key) {
if (key === "C") {
display = "0";
prev = null;
op = null;
waitNext = false;
return;
}
if (key === "+/-") {
display = String(-parseFloat(display));
return;
}
if (key === "%") {
display = String(parseFloat(display) / 100);
return;
}
if (["÷", "×", "−", "+"].includes(key)) {
prev = display;
op = key;
waitNext = true;
return;
}
if (key === "=") {
if (!op || !prev) return;
const a = parseFloat(prev);
const b = parseFloat(display);
let result = 0;
if (op === "÷") result = a / b;
else if (op === "×") result = a * b;
else if (op === "−") result = a - b;
else if (op === "+") result = a + b;
display = String(parseFloat(result.toPrecision(10)));
prev = null;
op = null;
waitNext = false;
return;
}
if (key === ".") {
if (display.includes(".") && !waitNext) return;
display = waitNext ? "0." : display + ".";
waitNext = false;
return;
}
display = waitNext ? key : display === "0" ? key : display + key;
waitNext = false;
}
function btnClass(key) {
if (["÷", "×", "−", "+", "="].includes(key)) return "op";
if (["C", "+/-", "%"].includes(key)) return "func";
return "num";
}
$: displayText = display.length > 9 ? parseFloat(display).toExponential(3) : display;
$: subDisplay = op ? `${prev} ${op}` : "";
const flat = BUTTONS.flat();
</script>
<style>
.page { min-height: 100vh; background: #0d1117; display: flex; align-items: center; justify-content: center; padding: 1.5rem; }
.calc { width: 280px; background: black; border-radius: 1.5rem; overflow: hidden; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.25); }
.display-area { padding: 1.5rem 1.25rem 0.75rem; text-align: right; }
.sub { color: #8b949e; font-size: 0.875rem; height: 1.25rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.main { color: white; font-size: 48px; font-weight: 300; line-height: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1px; background: #1c1c1c; padding: 1px; }
button {
height: 4rem;
border: none;
border-radius: 0;
font-size: 1.25rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.1s;
font-family: inherit;
}
button:active { opacity: 0.7; }
.num { background: #333333; color: white; }
.num:hover { background: #4a4a4a; }
.op { background: #f6901f; color: white; font-weight: 600; }
.op:hover { background: #e8830a; }
.func { background: #a5a5a5; color: black; font-weight: 600; }
.func:hover { background: #bfbfbf; }
.zero { grid-column: span 2; justify-content: flex-start; padding-left: 1.5rem; }
</style>
<div class="page">
<div class="calc">
<div class="display-area">
<p class="sub">{subDisplay}</p>
<p class="main">{displayText}</p>
</div>
<div class="grid">
{#each flat as key}
<button
class="{btnClass(key)} {key === '0' ? 'zero' : ''}"
on:click={() => press(key)}
>
{key}
</button>
{/each}
</div>
</div>
</div>Calculator
A modern calculator widget with a focus on usability and design. It handles basic arithmetic and features a history tape and keyboard input support.
Features
- Arithmetic operations: +, -, *, /
- Percentage calculation
- Positive/Negative toggle
- Precision handling for decimals
- Responsive grid-based button layout
- Dark theme optimized