Building a robust payment form

Fundamentally, all payment forms aim to do three things: be secure, be accessible, and be easy to use. A great way to achieve all three is to build your form using standard HTML elements and attributes, which lets you tap into built-in browser features instead of reinventing them. This codelab walks through the process of turning a basic payment form into a best-practice implementation.

Thinking in semantic HTML

The core of a good form is in its markup. Use elements designed for the job whenever possible instead of relying on custom

<div> structures or JavaScript-heavy widgets. The semantic building blocks here are:

  • <form>
  • <section>
  • <label>
  • <input>, <select>, <textarea>
  • <button>

In a standard payment form, this means you have <input> elements for card number, name, expiry date, and security code. Each should be wrapped in its own <section> and paired with a <label>. The submit action should be a <button>. This structure gives you the accessibility tree and the default behavior you need without writing a single line of JavaScript for interactions, navigation, or announcements.

Designing for form usability

Default browser styling rarely makes for a good user experience, especially on smaller screens. Once you have the basic structure, you need to set explicit sizes and spacing for the form fields. A few high-impact changes to css/main.css include:

  • Adding padding and margin to the inputs to make them comfortable tap targets.
  • Using display: block; on labels so they sit on their own line.
  • Adjusting font-size and spacing values at different viewport widths.

This approach should be mobile-first: start with styles for small viewports (under 400px wide) and then use media queries to override for larger widths (scaling up at 400px and again at 500px). The goal is that the form works on a small phone, a large mobile device, and a desktop without glitches. It's also worth noting that the :invalid selector in CSS can indicate when an input has an invalid value, which your JS can complement but does not need to replace.

Testing across devices and viewport sizes is non-negotiable for forms. You can do this via Chrome DevTools device mode, sending the URL to a real phone, or using a cross-browser testing service.

Adding autofill and input attributes

Perfection of your CSS is secondary to enabling the browser's built-in autofill and validation. Add the appropriate attributes to your HTML to let the browser handle these tasks. The critical ones for a payment form are the autocomplete values for the payment card:

  • cc-number
  • cc-name
  • cc-exp
  • cc-csc

When you do this, the browser can retrieve stored payment methods, and upon selection, automatically populate the fields. Many browsers will even validate the card number length and checksum. This is a primary upgrade: a wrong or missing single value (such as for the expiry date) forces the user to pull out their physical card, which may cause you to lose the sale.

Also on mobile, the presence of the pattern attribute and inputmode="numeric" is essential. Using inputmode="numeric" on the card number field brings up a numeric keyboard with no letters, which significantly speeds up entry and prevents confusion. The pattern attribute triggers browser-level validation built into the Constraint Validation API—no JavaScript needed. If a field is left empty or has a letter in a numeric-only pattern, the browser will nudge the user without page reloads.

Preventing duplicate submissions

Even with excellent UX, users double-tap submit buttons. But users don't just double-tap: many click several times, even when the button seems to work. For a payment action, this can cause duplication of transaction processing and extra server load—a costly glitch you should handle at the client level.

In js/main.js, attach an event listener to the form that disables the submit button upon the first attempt. As soon as the user submits, the button becomes disabled, which blocks further taps or clicks.

For a more complete solution, you can integrate JavaScript validation alongside the native HTML constraints. The web platform offers a widely supported API that allows you to access the browser's built-in UI for setting focus and displaying custom prompts, using functions like someField.setCustomValidity(message) or checking someField.validity against your regex.

Remaining best practices

Beyond code specifics, keep two things top of mind for any production form.

First, start with your terms. Communicate clearly with links to your privacy policy and Terms of Service. This reassures users how you intend to safeguard their data and is a core component of high-converting, trustworthy checkouts.

Second, you need analytics and Real User Monitoring (RUM) to improve the form. You can't optimise what you can't measure. Test the performance and usability continuously against real user data rather than just relying on optimal lab conditions.

This final list should help you audit any payment form you build or encounter:

  • Do the Terms of Service and privacy links match our branding and sites?
  • Are the visual styles aligned with the entire purchase flow?
  • Have we tried filling out this form without needing to look at our physical card?
  • What happens when the submission is slow or fails—do we block user input appropriately?