Form Validation in the Hyva Theme: Wiring Client and Server Together Properly
AI generated
Hyvä
phtml
Hyva Theme, Alpine.js, Accessibility
Form Validation in the Hyva Theme
Client and server wired together properly, never just one side

Magento's old jQuery based validation.js does not fit a theme without jQuery, nor a strict Content Security Policy. Alpine based live validation closes that gap, but it never replaces server side checking, since every client side rule can be bypassed with JavaScript disabled or a direct POST request.

11 min read Form Validation Accessibility Alpine.js

1. Why Magento's jQuery validation.js no longer fits Hyva

Magento's Luma theme ships with mage/validation, a jQuery plugin that checks form fields based on data-validate attributes and automatically displays error messages on violation. Hyva deliberately skips jQuery and Knockout.js entirely, which means this plugin simply is not available, even if it could technically be loaded back in.

Even if jQuery were added back in, that would contradict Hyva's core idea of shipping as little JavaScript baggage as possible, and would additionally clash with the Content Security Policy, since mage/validation relies on dynamic DOM manipulation in several places that does not sit well with Hyva's CSP requirements. The consistent alternative is therefore a lean, Alpine based solution.

2. Alpine based live validation: the basic concept

The basic principle is simple: every field is bound to an Alpine data object through x-model, and a validation function checks on every change or on leaving the field whether the current value satisfies a defined rule. The result is a simple errors object holding either an error message or an empty string per field name, evaluated directly in the template.

The timing of the first check matters for perception: a field should not already be marked invalid when the page loads, but only after the user has left it once, triggered on blur. Only after that does immediate checking on input, with a light debounce, kick in, so validation does not run on every single keystroke.


function formValidation(rules) {
  return {
    values: {},
    errors: {},
    touched: {},
    validateField(name) {
      const rule = rules[name];
      if (!rule) return;
      this.errors[name] = rule.test(this.values[name]) ? '' : rule.message;
    },
    onBlur(name) {
      this.touched[name] = true;
      this.validateField(name);
    },
    onInput(name) {
      if (this.touched[name]) this.validateField(name);
    },
    isValid() {
      return Object.values(this.errors).every((error) => !error);
    },
  };
}

3. Practical example: live validation on a registration form

A registration form with email, password, and password confirmation demonstrates the pattern well, since it needs two rule types at once: simple format checking through a regular expression for the email address, and a field dependent rule for password confirmation that needs to know another field's value. Both rules fit into the same rules object without touching the validation function itself.

The submit button stays disabled until isValid() returns true, on top of the server side check that runs regardless after submission. This double safeguard prevents unnecessary form round trips for obvious mistakes, without leaving the actual, authoritative check up to the client.


<form x-data="formValidation({
        email: { test: v => /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(v || ''), message: 'Please enter a valid email address' },
        password_confirmation: { test: v => v === values.password, message: 'Passwords do not match' },
      })"
      method="post" action="/customer/account/createpost">
  <input type="email" name="email" x-model="values.email"
         @blur="onBlur('email')" @input="onInput('email')"
         :aria-invalid="errors.email ? 'true' : 'false'">
  <button type="submit" :disabled="!isValid()">Create account</button>
</form>

4. Accessible error output: aria-invalid, aria-describedby, role=alert

A red border alone is not enough for screen reader users, an error also needs to be linked to the field programmatically. The aria-invalid attribute signals the error state itself, while aria-describedby points to the ID of the error text, so a screen reader automatically reads out the field's error message when it receives focus.

The error text container itself additionally gets role="alert", so newly appearing error messages get announced even when focus is not on the affected field at all, for instance after a server side validation error following submission. Without this attribute, errors that appear later often go completely unnoticed by screen reader users.


<label for="email">Email address</label>
<input id="email" type="email" name="email" x-model="values.email"
       @blur="onBlur('email')"
       :aria-invalid="errors.email ? 'true' : 'false'"
       aria-describedby="email-error">
<p id="email-error" role="alert" x-show="errors.email" x-text="errors.email"
   class="text-sm text-red-600"></p>

5. Why server side validation remains mandatory

Every client side rule is ultimately just a convenience feature, not a security mechanism. An attacker can disable JavaScript, tamper with the Alpine code, or simply send a POST request straight to the endpoint without the form ever being rendered in a browser at all. Magento's data and form validators on the server therefore need to check every rule independently, regardless of the client.

In practice that means maintaining the same business rule twice: once as an Alpine expression for immediate UI feedback, and once in a \Magento\Framework\Validator or a data patch based input filter in server code. Redundancy here is not a design flaw, it is the only way to guarantee good UX and real security at the same time.


<?php

declare(strict_types=1);

namespace Mironsoft\Core\Model\Validator;

use Magento\Framework\Validator\AbstractValidator;

/**
 * Server side check for password confirmation, independent of the client.
 */
class PasswordConfirmationValidator extends AbstractValidator
{
    /**
     * Checks whether password and confirmation match.
     *
     * @param mixed $value
     * @return bool
     */
    public function isValid($value): bool
    {
        $isValid = $value['password'] === $value['password_confirmation'];
        if (!$isValid) {
            $this->_addMessages(['Passwords do not match.']);
        }
        return $isValid;
    }
}

6. Sending server errors back: session messages and the Ajax pattern

With a classic, full form submit without Ajax, the controller processes the input, attaches a session message on error, and redirects back to the form, and the Hyva template reads the message through the familiar messages block and displays it above the form. This pattern also works with no JavaScript at all and is the most robust fallback layer.

With an Ajax based form, the controller instead returns a structured JSON response with an errors object per field name, which the Alpine component merges directly into its existing errors object. That way server side errors land in exactly the same UI display as client side errors, without maintaining two separate error displays in the same form.


async function submitForm() {
  const response = await fetch(this.$el.action, { method: 'POST', body: new FormData(this.$el) });
  const data = await response.json();
  if (!response.ok && data.errors) {
    this.errors = { ...this.errors, ...data.errors };
    return;
  }
  window.location.href = data.redirectUrl;
}

7. Practical example: checkout address form with country dependent validation

Checkout address forms are a good example of rules that change based on the selected country, such as different postcode formats or a region field that is mandatory in some countries and does not exist at all in others. The Alpine component holds the selected country as a reactive value and picks the matching postcode pattern from a rule map based on it.

The same principle applies here too: Magento's directory module already delivers the same country and region logic server side, so the client side rule map ideally gets generated from that same configuration data, instead of creating a second, independently maintained source that can drift out of sync over time.


const postcodePatterns = {
  DE: /^\d{5}$/,
  AT: /^\d{4}$/,
  NL: /^\d{4}\s?[A-Z]{2}$/,
};

function addressValidation(countryId) {
  return {
    country: countryId,
    validatePostcode(value) {
      const pattern = postcodePatterns[this.country];
      return pattern ? pattern.test(value) : true;
    },
  };
}

8. Reusable validation logic across forms

Once several forms share similar rules, such as email format used identically in registration, newsletter signup, and a contact form, a central rule library as its own JavaScript module pays off, with every form component importing the rules it needs, instead of maintaining slightly different versions of the same regular expression in three places.

This central library can additionally be registered as its own Alpine store for shared validation functions when several components need to access the same rules at once. What matters is that this reuse applies only at the client layer, server side checking stays entirely separate and independent of it.

9. Testing forms: keyboard, screen reader, automated

A manual, keyboard only test with no mouse quickly surfaces the most common accessibility problems: does focus land on the first invalid field after an error, is every field reachable through Tab, and does an error message actually get read out once it appears. A short test with a real screen reader like NVDA or VoiceOver confirms whether aria-describedby and role="alert" actually work as expected.

For automated testing, end to end tests that deliberately enter invalid values and check both that the client side error display appears and that a direct POST request with no JavaScript gets correctly rejected by the server work well. Only both paths together confirm that client and server validation actually stay in sync.

Layer Purpose Bypassable UX benefit Mandatory
Alpine live validation Immediate feedback while typing Yes, with JavaScript disabled Very high, errors become visible instantly No, pure convenience feature
HTML5 native validation Basic format checking in the browser Yes, via disabled validation or a direct POST Medium, display varies by browser No, an extra safeguard
Magento server validation Authoritative, business level checking No, runs server side Low directly, but prevents bad data Yes, always mandatory
Database constraints Final technical safeguard No, database level No direct UX relevance Yes, as a last line of defense
Accessible error output Making errors perceivable for all user groups No, concerns display not security Very high for screen reader users Yes, for legal and ethical reasons

Mironsoft

Hyvä theme development and Luma migration

Still running Luma, or a Hyvä theme that just doesn't feel right?

We build Hyvä themes for Magento from scratch or migrate existing Luma shops cleanly, with Tailwind CSS, Alpine.js, and none of the unnecessary JavaScript baggage.

Luma-to-Hyvä Migration

Move an existing shop to Hyvä in a structured way, without losing functionality.

Custom Theme Development

Build a custom Hyvä theme from scratch based on your design.

Performance Optimization

Improve Core Web Vitals and load times in the Hyvä frontend with purpose.

10. Summary

Form Validation in Hyva

Alpine instead of legacy jQuery

Live validation through x-model and simple rule functions replaces Magento's mage/validation without jQuery and without CSP conflicts.

Accessibility built in from the start

aria-invalid, aria-describedby, and role=alert make sure errors are perceivable for screen reader users too.

The server stays the only authority

Every client side rule needs an independent server side re-check, since client validation can be bypassed at any time.

Keep error formats in sync

Server side Ajax error responses land in the same errors object as client side errors, avoiding two separate displays.

11. FAQ: Form Validation in Hyva

1Why doesn't Magento's mage/validation just work in the Hyva theme?
Hyva deliberately skips jQuery, which mage/validation relies on as a foundation. Even if added back in, the plugin would introduce extra JavaScript baggage and clash with Hyva's strict Content Security Policy.
2Does Alpine based live validation replace server side checking?
No, never. Client validation is a pure UX convenience feature and can be bypassed with JavaScript disabled or a direct POST request. Server side Magento validation remains mandatory in every case.
3When should a field first be marked as invalid?
Only after the user has left the field once, triggered by the blur event. Marking a field invalid immediately on page load feels intrusive and confuses users who have not entered anything yet.
4How do I make error messages perceivable for screen readers?
Through aria-invalid on the field itself, aria-describedby pointing to the ID of the error text, and role=alert on the error text container, so that errors appearing later also get read out automatically.
5How do I return server side validation errors in an Ajax form?
The controller returns a structured JSON response with an errors object per field name, which the Alpine component merges directly into its existing errors object, so server and client errors display identically.
6What happens if a user has JavaScript disabled?
The form should still work as a classic form submit. The controller processes the input server side, attaches a session message on error, and redirects back, with no dependency on Alpine at all.
7How do I avoid maintaining validation rules twice between client and server?
The two layers cannot be fully merged since they run in different languages. A central rule source, such as country data from Magento's directory module, does reduce redundancy and drift between the two sides though.
8Should the submit button be disabled while the form is invalid?
That is a common UX pattern and reduces unnecessary server round trips for obvious mistakes. It is important to still keep the button semantically correct and not treat it as the sole safeguard against invalid input.
9How do I practically test a form's accessibility?
A purely keyboard driven test with no mouse surfaces the most common problems, complemented by a short test with a real screen reader like NVDA or VoiceOver to verify that error messages actually get read out.
10Is a central validation library worth it across several forms?
Yes, once several forms share similar rules like email format. A central, importable rule library or a shared Alpine store avoids maintaining slightly different versions of the same logic in multiple places.