Promo Code Input — Apply Coupon HTML CSS JS
Promo Code Input · Forms · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="pci-card">
<h3>Order summary</h3>
<div class="pci-line"><span>Subtotal</span><span id="pciSubtotal">$240.00</span></div>
<div class="pci-line pci-discount" id="pciDiscountLine" hidden><span id="pciDiscountLabel">Discount</span><span id="pciDiscount">−$0.00</span></div>
<div class="pci-line"><span>Shipping</span><span>$8.00</span></div>
<div class="pci-promo">
<label for="pciInput">Promo code</label>
<div class="pci-applied" id="pciApplied" hidden>
<span class="pci-applied-tag"><b id="pciAppliedCode"></b> applied</span>
<button type="button" id="pciRemove" aria-label="Remove code">Remove</button>
</div>
<form class="pci-form" id="pciForm" novalidate>
<input type="text" id="pciInput" placeholder="Enter code" autocomplete="off" spellcheck="false">
<button type="submit" id="pciApply">Apply</button>
</form>
<p class="pci-msg" id="pciMsg" hidden></p>
</div>
<div class="pci-line pci-total"><span>Total</span><span id="pciTotal">$248.00</span></div>
<p class="pci-hint">Try <b>SAVE20</b>, <b>FREESHIP</b>, or <b>HALFOFF</b>.</p>
</div>*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.pci-card{background:#fff;border-radius:16px;padding:22px;width:100%;max-width:380px;box-shadow:0 18px 44px rgba(15,23,42,.1)}
.pci-card h3{font-size:16px;font-weight:800;color:#0f172a;margin-bottom:16px}
.pci-line{display:flex;justify-content:space-between;font-size:13.5px;color:#475569;margin-bottom:10px;font-variant-numeric:tabular-nums}
.pci-line[hidden]{display:none}
.pci-discount{color:#16a34a;font-weight:700}
.pci-total{border-top:1.5px solid #e2e8f0;padding-top:13px;margin-top:3px;font-size:16px;font-weight:800;color:#0f172a}
.pci-promo{border-top:1px dashed #e2e8f0;border-bottom:1px dashed #e2e8f0;padding:15px 0;margin:14px 0}
.pci-promo label{display:block;font-size:11.5px;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.03em;margin-bottom:9px}
.pci-form{display:flex;gap:8px}
.pci-form[hidden]{display:none}
.pci-form input{flex:1;border:1.5px solid #e2e8f0;border-radius:9px;padding:10px 12px;font-size:13.5px;font-family:inherit;color:#0f172a;text-transform:uppercase;letter-spacing:.04em;transition:border-color .15s,box-shadow .15s}
.pci-form input:focus{outline:none;border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.15)}
.pci-form input.invalid{border-color:#ef4444;box-shadow:0 0 0 3px rgba(239,68,68,.12)}
.pci-form button{background:#0f172a;color:#fff;border:none;border-radius:9px;padding:0 18px;font-size:13px;font-weight:700;cursor:pointer;transition:background .15s,opacity .15s}
.pci-form button:hover{background:#1e293b}
.pci-form button:disabled{opacity:.6;cursor:default}
.pci-applied{display:flex;align-items:center;justify-content:space-between;background:#ecfdf5;border:1px solid #a7f3d0;border-radius:9px;padding:9px 12px}
.pci-applied[hidden]{display:none}
.pci-applied-tag{font-size:12.5px;color:#047857;font-weight:600;display:flex;align-items:center;gap:5px}
.pci-applied-tag::before{content:'✓';font-weight:800}
.pci-applied-tag b{font-weight:800}
.pci-applied button{background:none;border:none;color:#059669;font-size:12px;font-weight:700;cursor:pointer;text-decoration:underline}
.pci-msg{font-size:12px;font-weight:600;margin-top:8px}
.pci-msg.err{color:#dc2626}
.pci-msg.ok{color:#16a34a}
.pci-hint{font-size:11.5px;color:#94a3b8;margin-top:14px;text-align:center}
.pci-hint b{color:#64748b;font-weight:700}var SUBTOTAL = 240;
var SHIPPING = 8;
var CODES = {
SAVE20: { type: 'percent', value: 20, label: 'SAVE20 (20% off)' },
HALFOFF: { type: 'percent', value: 50, label: 'HALFOFF (50% off)' },
FREESHIP: { type: 'shipping', value: SHIPPING, label: 'FREESHIP (free shipping)' },
TENOFF: { type: 'fixed', value: 10, label: 'TENOFF ($10 off)' },
};
var applied = null;
var form = document.getElementById('pciForm');
var input = document.getElementById('pciInput');
var applyBtn = document.getElementById('pciApply');
var msg = document.getElementById('pciMsg');
function money(n) { return '$' + n.toFixed(2); }
function discountAmount(code) {
var c = CODES[code];
if (c.type === 'percent') return SUBTOTAL * (c.value / 100);
if (c.type === 'fixed') return Math.min(c.value, SUBTOTAL);
if (c.type === 'shipping') return c.value; // offsets shipping
return 0;
}
function recalc() {
var discount = applied ? discountAmount(applied) : 0;
var line = document.getElementById('pciDiscountLine');
if (applied) {
line.hidden = false;
document.getElementById('pciDiscountLabel').textContent = CODES[applied].type === 'shipping' ? 'Free shipping' : 'Discount (' + applied + ')';
document.getElementById('pciDiscount').textContent = '−' + money(discount);
} else {
line.hidden = true;
}
var total = SUBTOTAL + SHIPPING - discount;
document.getElementById('pciTotal').textContent = money(Math.max(0, total));
}
function showMsg(text, ok) {
msg.textContent = text;
msg.className = 'pci-msg ' + (ok ? 'ok' : 'err');
msg.hidden = false;
}
form.addEventListener('submit', function (e) {
e.preventDefault();
var code = input.value.trim().toUpperCase();
input.classList.remove('invalid');
if (!code) { input.classList.add('invalid'); showMsg('Enter a promo code first.', false); return; }
if (applied === code) { showMsg('That code is already applied.', false); return; }
if (!CODES[code]) { input.classList.add('invalid'); showMsg('"' + code + '" is not a valid code.', false); return; }
applied = code;
recalc();
document.getElementById('pciForm').hidden = true;
document.getElementById('pciMsg').hidden = true;
document.getElementById('pciAppliedCode').textContent = code;
document.getElementById('pciApplied').hidden = false;
showMsg(CODES[code].label + ' applied!', true);
});
document.getElementById('pciRemove').addEventListener('click', function () {
applied = null;
recalc();
document.getElementById('pciApplied').hidden = true;
document.getElementById('pciForm').hidden = false;
input.value = '';
msg.hidden = true;
input.focus();
});
document.getElementById('pciSubtotal').textContent = money(SUBTOTAL);
recalc();Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Promo Code Input — Coupon Validation, Discount Types & Live Order Total

Almost every checkout has a "promo code" or "discount code" field, and it's deceptively easy to get wrong: codes need validating, different coupon types calculate differently, the order total must update without a page reload, and the user needs clear feedback whether a code worked or not. This snippet builds a complete, correct promo-code experience in plain HTML, CSS, and vanilla JavaScript — supporting percentage, fixed-amount, and free-shipping coupons, with an applied state, a remove control, and a live-recalculating total.
Three coupon types, one calculation function
Real stores run more than one kind of discount, so discountAmount() handles three: percent (20% off the subtotal), fixed (a flat $10 off, clamped so it never exceeds the subtotal and produces a negative price), and shipping (offsets the shipping line for free delivery). Each code in the CODES map declares its type and value, so adding a new coupon is a one-line data change, not new logic. The total always recomputes as subtotal + shipping − discount, floored at zero.
Validation that explains itself
Submitting a code runs through ordered checks, each with a specific message rather than a generic failure: an empty field ("Enter a promo code first"), a code that's already applied ("That code is already applied"), and an unrecognised code ("'BOGUS' is not a valid code"). Invalid input also flashes a red border on the field. This specificity matters at checkout — a shopper who typed a code from an email needs to know whether they fat-fingered it or it simply expired, not just that "something went wrong."
Case- and whitespace-insensitive matching
Codes are normalised with trim().toUpperCase() before lookup, and the input is visually uppercased via CSS, so "save20", " SAVE20 ", and "Save20" all resolve to the same coupon. This removes the single most common reason a valid code "doesn't work" — invisible whitespace or a lowercase letter — without the user ever having to think about it.
Applied and remove states
A successfully applied code hides the input and shows a green confirmation chip ("✓ SAVE20 applied") with a Remove link. Removing it restores the input, clears the discount line, recomputes the total, and refocuses the field — so a shopper can swap one code for a better one without confusion. The discount line itself only appears when a code is active, and its label adapts ("Discount (SAVE20)" vs "Free shipping") to match the coupon type.
Where the real validation lives
Client-side code-matching is for instant feedback only — it must never be the source of truth, because anyone can read the CODES map in the page source. In production the field still gives immediate UX, but the authoritative check (does this code exist, is it expired, is it limited to this customer, does it stack) happens server-side when the order is priced and again when it's placed. The FAQs cover wiring the front end to that real validation endpoint while keeping this snippet's instant-feedback feel.
Step by step
How to Use
- 1Paste HTML, CSS, and JSAn order summary renders with a subtotal, shipping, total, and a promo-code field. Try SAVE20, FREESHIP, or HALFOFF.
- 2Apply a valid codeEnter SAVE20 and click Apply — a discount line appears, the total drops, and a green "applied" chip replaces the field.
- 3See validation errorsEnter a made-up code or leave it blank — a specific red message explains exactly what's wrong, and the field flags invalid.
- 4Remove an applied codeClick Remove on the applied chip — the discount clears, the total restores, and the input comes back focused for a new code.
- 5Add your own couponsAdd entries to the CODES map with a type (percent / fixed / shipping) and value — no new logic needed.
- 6Validate codes server-sideReplace the client-side CODES lookup with a fetch to your coupon-validation endpoint, keeping the same applied/error UI states.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Replace the client-side CODES lookup in the submit handler with a fetch to your coupon-validation endpoint, passing the entered code and the cart contents; the server returns whether it's valid plus the discount type and amount. Show a "Checking…" state during the request, then the applied chip on success or the inline error on failure — the rest of the UI stays the same.
Anyone can read the CODES map in your page source, so client-side matching is purely for instant feedback. The authoritative check — does the code exist, is it expired, is it usage-limited or customer-specific, can it stack with other offers — must run server-side when the order is priced and again when it's placed, or the discount can be forged. Keep the client check for UX, never for enforcement.
Track applied as an array instead of a single value, render one chip per code, and sum their discounts in recalc() — but enforce your stacking rules (which combinations are allowed, caps on total discount) server-side. Most stores deliberately disallow stacking, which the single-code model here already enforces.
Give each code optional conditions (minSubtotal, eligibleProductIds) in its data, and check them before applying — if the cart doesn't qualify, show a specific message ("SAVE20 requires a $100 minimum") rather than silently applying a $0 discount. The same conditions must be re-validated server-side at checkout.
In React, hold the applied code and message in useState and derive the total with useMemo; in Vue, use ref()/computed(); in Angular, use a component field with a getter. The discount-type calculation is plain JavaScript, and the apply/remove handlers map directly to event handlers in each framework — swap the CODES lookup for an async validation call.