Banking — Amount Keypad
A trust-first currency amount entry screen for fintech send-money and top-up flows. A large tabular-figure display formats digits live with thousands separators, a tappable currency pill cycles EUR, USD and GBP, and a backspace key trims one character at a time. Quick-amount chips, a Send-max shortcut, and a live balance hint speed real input, while invalid taps shake the display and over-balance amounts disable the call to action. Built with on-screen and physical keyboard support, vanilla JS only.
MCP
Code
:root {
--navy: #0e1b3a;
--navy-2: #16264d;
--ink: #0e1726;
--ink-2: #3a4660;
--muted: #697089;
--accent: #3b6ef6;
--accent-d: #2a55cc;
--accent-50: #eaf0ff;
--teal: #0fb5a6;
--violet: #7c5cff;
--bg: #f5f7fb;
--surface: #ffffff;
--line: rgba(14, 27, 58, 0.10);
--line-2: rgba(14, 27, 58, 0.18);
--ok: #1f9d62;
--warn: #d9982b;
--danger: #d4493e;
--credit: #1f9d62;
--debit: #0e1726;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--shadow-1: 0 1px 2px rgba(14, 27, 58, 0.06), 0 4px 14px rgba(14, 27, 58, 0.06);
--shadow-2: 0 10px 30px rgba(14, 27, 58, 0.12), 0 2px 8px rgba(14, 27, 58, 0.08);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(1200px 560px at 50% -10%, rgba(59, 110, 246, 0.16), transparent 60%),
var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
}
.tabnum { font-variant-numeric: tabular-nums; }
.screen {
min-height: 100vh;
display: grid;
place-items: center;
padding: 28px 16px;
}
/* Card shell */
.pay {
width: 100%;
max-width: 408px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--shadow-2);
padding: 18px 18px 20px;
display: flex;
flex-direction: column;
gap: 16px;
}
/* Header */
.pay__head {
display: flex;
align-items: center;
gap: 10px;
}
.pay__title-wrap { flex: 1; min-width: 0; }
.pay__head h1 {
margin: 0;
font-size: 1.05rem;
font-weight: 700;
letter-spacing: -0.01em;
}
.pay__secure {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 0.72rem;
font-weight: 600;
color: var(--muted);
}
.pay__secure svg { width: 13px; height: 13px; color: var(--ok); }
.icon-btn {
display: grid;
place-items: center;
width: 38px;
height: 38px;
border-radius: 50%;
border: 1px solid var(--line);
background: var(--surface);
color: var(--ink);
cursor: pointer;
transition: background 0.15s, transform 0.1s;
}
.icon-btn svg { width: 20px; height: 20px; }
.icon-btn:hover { background: var(--bg); }
.icon-btn:active { transform: scale(0.94); }
.pill {
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.02em;
padding: 4px 9px;
border-radius: 999px;
text-transform: uppercase;
}
.pill--ok { background: rgba(31, 157, 98, 0.12); color: var(--ok); }
/* Recipient */
.recipient {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
text-align: left;
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 10px 12px;
cursor: pointer;
transition: border-color 0.15s, background 0.15s;
}
.recipient:hover { border-color: var(--line-2); background: #eef2fa; }
.recipient__avatar {
display: grid;
place-items: center;
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, var(--violet), var(--accent));
color: #fff;
font-weight: 700;
font-size: 0.82rem;
flex-shrink: 0;
}
.recipient__meta { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.recipient__name {
display: inline-flex;
align-items: center;
gap: 5px;
font-weight: 600;
font-size: 0.92rem;
}
.recipient__verify { width: 15px; height: 15px; flex-shrink: 0; }
.recipient__sub {
font-size: 0.76rem;
color: var(--muted);
font-variant-numeric: tabular-nums;
}
.recipient__chev { width: 20px; height: 20px; color: var(--muted); flex-shrink: 0; }
/* Amount display */
.amount {
display: flex;
align-items: baseline;
justify-content: center;
gap: 4px;
padding: 14px 6px 4px;
user-select: none;
}
.amount.is-invalid { animation: shake 0.42s cubic-bezier(.36,.07,.19,.97); }
.amount.is-invalid .amount__value,
.amount.is-invalid .amount__cents { color: var(--danger); }
.amount.is-invalid .amount__cur { border-color: var(--danger); color: var(--danger); }
.amount__cur {
align-self: center;
display: inline-flex;
align-items: center;
gap: 1px;
font-size: 1.6rem;
font-weight: 700;
color: var(--ink-2);
background: var(--bg);
border: 1px solid var(--line);
border-radius: 999px;
padding: 4px 8px 4px 12px;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
.amount__cur:hover { background: var(--accent-50); border-color: var(--accent); }
.amount__cur svg { width: 16px; height: 16px; }
.amount__value {
font-size: clamp(2.8rem, 13vw, 3.8rem);
font-weight: 800;
letter-spacing: -0.03em;
line-height: 1;
font-variant-numeric: tabular-nums;
color: var(--ink);
transition: color 0.2s;
}
.amount__value.is-zero { color: var(--muted); }
.amount__cents {
font-size: clamp(1.5rem, 7vw, 2rem);
font-weight: 700;
letter-spacing: -0.02em;
font-variant-numeric: tabular-nums;
color: var(--ink-2);
}
/* Balance hint */
.balance {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
margin: 0;
font-size: 0.82rem;
color: var(--muted);
font-variant-numeric: tabular-nums;
}
.balance svg { width: 16px; height: 16px; color: var(--ink-2); }
.balance strong { color: var(--ink); font-weight: 700; }
.balance.is-over strong { color: var(--danger); }
.link {
background: none;
border: none;
padding: 0;
font: inherit;
font-weight: 700;
color: var(--accent);
cursor: pointer;
}
.link:hover { color: var(--accent-d); text-decoration: underline; }
/* Chips */
.chips {
display: flex;
gap: 8px;
overflow-x: auto;
padding: 2px;
scrollbar-width: none;
}
.chips::-webkit-scrollbar { display: none; }
.chip {
flex: 1 0 auto;
white-space: nowrap;
font: inherit;
font-weight: 600;
font-size: 0.86rem;
font-variant-numeric: tabular-nums;
color: var(--ink-2);
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: 999px;
padding: 8px 14px;
cursor: pointer;
transition: background 0.14s, color 0.14s, border-color 0.14s, transform 0.1s;
}
.chip:hover { border-color: var(--accent); color: var(--accent-d); }
.chip:active { transform: scale(0.95); }
.chip.is-active {
background: var(--accent);
border-color: var(--accent);
color: #fff;
}
/* Keypad */
.keypad {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.key {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0;
height: 56px;
font: inherit;
font-size: 1.5rem;
font-weight: 600;
color: var(--ink);
background: var(--bg);
border: 1px solid transparent;
border-radius: var(--r-md);
cursor: pointer;
overflow: hidden;
transition: background 0.12s, transform 0.06s;
-webkit-tap-highlight-color: transparent;
}
.key small {
font-size: 0.55rem;
font-weight: 600;
letter-spacing: 0.12em;
color: var(--muted);
margin-top: 1px;
}
.key:hover { background: #e9eef8; }
.key:active, .key.is-press { background: var(--accent-50); transform: scale(0.96); }
.key--alt { background: transparent; color: var(--ink-2); }
.key--alt:hover { background: var(--bg); }
.key svg { width: 26px; height: 26px; }
/* CTA */
.cta {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
height: 54px;
font: inherit;
font-weight: 700;
font-size: 1rem;
color: #fff;
background: linear-gradient(180deg, var(--accent), var(--accent-d));
border: none;
border-radius: var(--r-md);
cursor: pointer;
box-shadow: 0 8px 20px rgba(59, 110, 246, 0.32);
transition: filter 0.15s, transform 0.08s, box-shadow 0.15s;
}
.cta svg { width: 20px; height: 20px; }
.cta:hover { filter: brightness(1.04); }
.cta:active { transform: translateY(1px); box-shadow: 0 4px 12px rgba(59, 110, 246, 0.3); }
.cta:disabled {
background: var(--line-2);
color: var(--muted);
cursor: not-allowed;
box-shadow: none;
}
.cta:disabled svg { display: none; }
/* Focus visibility */
:focus-visible {
outline: 3px solid rgba(59, 110, 246, 0.45);
outline-offset: 2px;
}
/* Toast */
.toast {
position: fixed;
left: 50%;
bottom: 24px;
transform: translate(-50%, 14px);
background: var(--navy);
color: #fff;
font-size: 0.85rem;
font-weight: 600;
padding: 11px 16px;
border-radius: 999px;
box-shadow: var(--shadow-2);
opacity: 0;
pointer-events: none;
transition: opacity 0.22s, transform 0.22s;
z-index: 50;
max-width: calc(100% - 32px);
}
.toast.is-on { opacity: 1; transform: translate(-50%, 0); }
@keyframes shake {
10%, 90% { transform: translateX(-1px); }
20%, 80% { transform: translateX(2px); }
30%, 50%, 70% { transform: translateX(-5px); }
40%, 60% { transform: translateX(5px); }
}
@media (max-width: 520px) {
.screen { padding: 0; }
.pay {
max-width: none;
min-height: 100vh;
border-radius: 0;
border: none;
box-shadow: none;
justify-content: flex-start;
}
.key { height: 52px; }
.amount__value { font-size: clamp(2.6rem, 16vw, 3.4rem); }
}
@media (prefers-reduced-motion: reduce) {
*, .amount.is-invalid { animation: none !important; transition: none !important; }
}(function () {
"use strict";
// --- Config / state ---------------------------------------------------
var CURRENCIES = [
{ code: "EUR", sym: "€" },
{ code: "USD", sym: "$" },
{ code: "GBP", sym: "£" }
];
var BALANCE = 4820.0; // available balance in current currency units
var MAX_INTEGER_DIGITS = 7;
var raw = ""; // user-entered string, e.g. "12" or "12." or "12.5"
var curIdx = 0; // index into CURRENCIES
// --- Elements ---------------------------------------------------------
var el = {
amount: document.getElementById("amount"),
value: document.getElementById("amountValue"),
cents: document.getElementById("amountCents"),
curSym: document.getElementById("curSym"),
curBtn: document.getElementById("curBtn"),
balanceHint: document.getElementById("balanceHint"),
balanceVal: document.getElementById("balanceVal"),
maxBtn: document.getElementById("maxBtn"),
chips: Array.prototype.slice.call(document.querySelectorAll(".chip")),
keys: Array.prototype.slice.call(document.querySelectorAll(".key")),
cta: document.getElementById("sendBtn"),
ctaLabel: document.getElementById("ctaLabel"),
toast: document.getElementById("toast")
};
// --- Helpers ----------------------------------------------------------
var toastTimer;
function toast(msg) {
el.toast.textContent = msg;
el.toast.classList.add("is-on");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
el.toast.classList.remove("is-on");
}, 2400);
}
function sym() { return CURRENCIES[curIdx].sym; }
function code() { return CURRENCIES[curIdx].code; }
// Numeric value of the current input
function numericValue() {
var n = parseFloat(raw);
return isNaN(n) ? 0 : n;
}
// Group the integer part with thousands separators
function groupInt(intStr) {
return intStr.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
// Format a number as a full currency string for hints
function fmt(n) {
return sym() + groupInt(Math.floor(n).toString()) + "." +
(Math.round((n - Math.floor(n)) * 100)).toString().padStart(2, "0");
}
// --- Render -----------------------------------------------------------
function render() {
var hasDot = raw.indexOf(".") !== -1;
var intPart = hasDot ? raw.slice(0, raw.indexOf(".")) : raw;
var decPart = hasDot ? raw.slice(raw.indexOf(".") + 1) : "";
if (intPart === "" || intPart === "0") {
// show 0 but keep entered leading zero if any
el.value.textContent = intPart === "" ? "0" : "0";
} else {
el.value.textContent = groupInt(intPart);
}
var isZero = raw === "" || numericValue() === 0;
el.value.classList.toggle("is-zero", isZero && !hasDot);
if (hasDot) {
el.cents.textContent = "." + decPart;
} else {
el.cents.textContent = "";
}
el.value.setAttribute("aria-label", sym() + " " + (raw || "0"));
// currency symbol
el.curSym.textContent = sym();
// balance over-limit state
var over = numericValue() > BALANCE;
el.balanceHint.classList.toggle("is-over", over);
// CTA state
var v = numericValue();
if (v <= 0) {
el.cta.disabled = true;
el.ctaLabel.textContent = "Enter an amount";
} else if (over) {
el.cta.disabled = true;
el.ctaLabel.textContent = "Amount exceeds balance";
} else {
el.cta.disabled = false;
el.ctaLabel.textContent = "Send " + fmt(v);
}
// chip active sync
el.chips.forEach(function (chip) {
var match = parseFloat(chip.getAttribute("data-amount")) === v && v > 0;
chip.classList.toggle("is-active", match);
});
}
function flashInvalid() {
el.amount.classList.remove("is-invalid");
// force reflow so the animation can replay
void el.amount.offsetWidth;
el.amount.classList.add("is-invalid");
}
el.amount.addEventListener("animationend", function () {
el.amount.classList.remove("is-invalid");
});
// --- Input handling ---------------------------------------------------
function press(k) {
if (k === "back") {
if (raw.length === 0) { return; }
raw = raw.slice(0, -1);
render();
return;
}
if (k === ".") {
if (raw.indexOf(".") !== -1) { flashInvalid(); return; }
raw = raw === "" ? "0." : raw + ".";
render();
return;
}
// digit
var dotPos = raw.indexOf(".");
if (dotPos !== -1) {
// already 2 decimals -> reject
if (raw.length - dotPos - 1 >= 2) { flashInvalid(); return; }
} else {
// integer length cap (ignore a single leading 0)
var intLen = raw === "0" ? 0 : raw.length;
if (intLen >= MAX_INTEGER_DIGITS) { flashInvalid(); return; }
// prevent multiple leading zeros
if (raw === "0") { raw = ""; }
}
raw += k;
render();
}
// Keypad clicks + press feedback
el.keys.forEach(function (btn) {
btn.addEventListener("click", function () {
press(btn.getAttribute("data-key"));
});
});
// --- Quick chips ------------------------------------------------------
el.chips.forEach(function (chip) {
chip.addEventListener("click", function () {
raw = parseFloat(chip.getAttribute("data-amount")).toString();
render();
});
});
// --- Max / balance ----------------------------------------------------
el.maxBtn.addEventListener("click", function () {
raw = BALANCE.toFixed(2);
render();
toast("Filled with full balance");
});
// --- Currency switch --------------------------------------------------
el.curBtn.addEventListener("click", function () {
curIdx = (curIdx + 1) % CURRENCIES.length;
el.balanceVal.textContent = fmt(BALANCE);
render();
el.curBtn.setAttribute("aria-expanded", "false");
toast("Currency set to " + code());
});
// --- Send -------------------------------------------------------------
el.cta.addEventListener("click", function () {
var v = numericValue();
if (v <= 0 || v > BALANCE) { flashInvalid(); return; }
el.ctaLabel.textContent = "Sending…";
el.cta.disabled = true;
setTimeout(function () {
toast("Sent " + fmt(v) + " to Mara Reyes · pending");
raw = "";
render();
}, 650);
});
// --- Physical keyboard ------------------------------------------------
document.addEventListener("keydown", function (e) {
var k = null;
if (e.key >= "0" && e.key <= "9") { k = e.key; }
else if (e.key === "." || e.key === ",") { k = "."; }
else if (e.key === "Backspace") { k = "back"; }
else if (e.key === "Enter") { el.cta.click(); return; }
if (k === null) { return; }
e.preventDefault();
press(k);
// visual feedback on the matching key
var btn = document.querySelector('.key[data-key="' + k + '"]');
if (btn) {
btn.classList.add("is-press");
setTimeout(function () { btn.classList.remove("is-press"); }, 120);
}
});
// --- Init -------------------------------------------------------------
el.balanceVal.textContent = fmt(BALANCE);
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Banking — Amount Keypad</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="screen" role="main">
<section class="pay" aria-labelledby="pay-title">
<header class="pay__head">
<button class="icon-btn" type="button" aria-label="Go back">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15 5l-7 7 7 7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="pay__title-wrap">
<h1 id="pay-title">Send money</h1>
<span class="pay__secure"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 10V8a6 6 0 1112 0v2m-13 0h14v9a2 2 0 01-2 2H7a2 2 0 01-2-2z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>Secured · 2FA</span>
</div>
<span class="pill pill--ok" aria-hidden="true">Verified</span>
</header>
<!-- Recipient -->
<button class="recipient" type="button" aria-label="Change recipient">
<span class="recipient__avatar" aria-hidden="true">MR</span>
<span class="recipient__meta">
<span class="recipient__name">Mara Reyes <svg class="recipient__verify" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2l2.4 1.6 2.9-.2 1 2.7 2.3 1.7-.9 2.8.9 2.8-2.3 1.7-1 2.7-2.9-.2L12 22l-2.4-1.6-2.9.2-1-2.7L3.4 16.5l.9-2.8-.9-2.8 2.3-1.7 1-2.7 2.9.2z" fill="var(--accent)"/><path d="M8.5 12.2l2.3 2.3 4.7-4.8" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
<span class="recipient__sub">Revolat · IBAN •••• 8841</span>
</span>
<svg class="recipient__chev" viewBox="0 0 24 24" aria-hidden="true"><path d="M9 6l6 6-6 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<!-- Amount display -->
<div class="amount" id="amount" aria-live="polite">
<button class="amount__cur" id="curBtn" type="button" aria-haspopup="true" aria-expanded="false" aria-label="Change currency">
<span id="curSym">€</span>
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 9l6 6 6-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<span class="amount__value" id="amountValue" aria-label="Amount to send">0</span>
<span class="amount__cents" id="amountCents"></span>
</div>
<p class="balance" id="balanceHint">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 7h18v10a2 2 0 01-2 2H5a2 2 0 01-2-2z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 10h18" stroke="currentColor" stroke-width="2"/></svg>
Balance <strong id="balanceVal">€4,820.00</strong> ·
<button class="link" id="maxBtn" type="button">Send max</button>
</p>
<!-- Quick chips -->
<div class="chips" role="group" aria-label="Quick amounts">
<button class="chip" type="button" data-amount="25">€25</button>
<button class="chip" type="button" data-amount="50">€50</button>
<button class="chip" type="button" data-amount="100">€100</button>
<button class="chip" type="button" data-amount="250">€250</button>
<button class="chip" type="button" data-amount="500">€500</button>
</div>
<!-- Keypad -->
<div class="keypad" role="group" aria-label="Numeric keypad">
<button class="key" type="button" data-key="1">1</button>
<button class="key" type="button" data-key="2">2<small>ABC</small></button>
<button class="key" type="button" data-key="3">3<small>DEF</small></button>
<button class="key" type="button" data-key="4">4<small>GHI</small></button>
<button class="key" type="button" data-key="5">5<small>JKL</small></button>
<button class="key" type="button" data-key="6">6<small>MNO</small></button>
<button class="key" type="button" data-key="7">7<small>PQRS</small></button>
<button class="key" type="button" data-key="8">8<small>TUV</small></button>
<button class="key" type="button" data-key="9">9<small>WXYZ</small></button>
<button class="key key--alt" type="button" data-key=".">.</button>
<button class="key" type="button" data-key="0">0</button>
<button class="key key--alt" type="button" data-key="back" aria-label="Backspace">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M21 5H8.5a2 2 0 00-1.6.8l-4 5.2a1.5 1.5 0 000 1.9l4 5.3a2 2 0 001.6.8H21a2 2 0 002-2V7a2 2 0 00-2-2z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="M12 9.5l5 5m0-5l-5 5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
</button>
</div>
<button class="cta" id="sendBtn" type="button">
<span id="ctaLabel">Enter an amount</span>
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14m-6-6l6 6-6 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</section>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Amount Keypad
A focused send-money screen built around a single task: entering an amount with confidence. The hero display renders the figure in heavy tabular numerals so digits never shift width, grouping thousands as you type and floating a smaller cents span once you tap the decimal. A pill at the left cycles the currency symbol across EUR, USD and GBP, and a verified recipient header with a 2FA secure cue sets the trust-first tone.
The on-screen keypad mirrors a real banking app: phone-style letter sublabels, an alternate decimal and backspace row, and pressed-state feedback on every tap. Quick-amount chips (€25–€500) fill the field in one tap and highlight when they match, while a Send-max shortcut drops in the full balance. The keypad enforces sane limits — one decimal point, two cents digits, no runaway integers — and any rejected tap triggers a short shake on the display.
Validation is woven into the call to action rather than bolted on. The balance hint turns red and the Send button disables when the amount exceeds available funds, and reads the formatted total back when valid. Everything is keyboard-usable: number keys, comma or period, Backspace and Enter all drive the same logic with matching visual feedback, a toast() helper confirms each action, and the layout collapses to a full-height mobile sheet down to 360px.
Illustrative UI only — not real banking software or financial advice.