Back

Form Validation Made Simple with htmx

Form Validation Made Simple with htmx

Form validation doesn’t have to mean writing mountains of JavaScript. If you’re tired of managing validation state, error messages, and DOM updates manually, htmx offers a refreshingly simple alternative. Let’s explore how to implement both client-side and server-side validation with minimal code and maximum user experience.

Key Takeaways

  • htmx enables form validation with minimal JavaScript using hx-validate="true" for HTML5 validation
  • Server-side inline validation provides real-time feedback without complex client-side state management
  • Proper htmx attribute placement and event handling are crucial for validation to work correctly
  • Combining client and server validation creates accessible, user-friendly forms with less code

Client-Side Validation with htmx

Using hx-validate for Instant Feedback

The simplest way to add client-side validation is with hx-validate="true" on your form. This tells htmx to check HTML5 validation attributes before sending any request:

<form hx-post="/submit" hx-validate="true">
  <input type="email" name="email" required>
  <input type="text" name="username" 
         pattern="[a-zA-Z0-9]{3,20}" 
         title="3-20 alphanumeric characters">
  <button type="submit">Submit</button>
</form>

This approach leverages the browser’s built-in validation, providing instant feedback without writing any JavaScript.

Advanced Rules with htmx-validation Extension

For more complex validation scenarios, the htmx-validation extension offers custom rules:

<script src="https://unpkg.com/htmx.org/dist/ext/validate.js"></script>

<form hx-post="/submit" hx-ext="validate">
  <input name="password" type="password" 
         data-validate='{"required": true, "minLength": 8}'>
  <input name="confirm" type="password" 
         data-validate='{"equalTo": "password"}'>
  <button type="submit">Submit</button>
</form>

This extension supports custom validation functions, making it perfect for business logic that HTML5 attributes can’t handle.

Server-Side Inline Validation

Real-Time Field Validation

The most powerful htmx form validation pattern combines server-side logic with inline updates. Here’s how to validate individual fields as users type:

<form hx-post="/register">
  <div>
    <label for="email">Email</label>
    <input name="email" id="email" 
           hx-post="/validate/email" 
           hx-trigger="blur, keyup delay:500ms"
           hx-target="next .error"
           hx-swap="innerHTML">
    <span class="error"></span>
  </div>
  <button type="submit">Register</button>
</form>

Your server endpoint returns validation feedback:

# Python/Flask example
@app.route('/validate/email', methods=['POST'])
def validate_email():
    email = request.form.get('email')
    if not email or '@' not in email:
        return '<span class="text-red">Invalid email format</span>'
    if email_exists_in_db(email):
        return '<span class="text-red">Email already taken</span>'
    return '<span class="text-green">✓ Available</span>'

Full Form Validation with Partial Responses

For complete form validation, return the entire form with error states:

<form id="contact-form" 
      hx-post="/contact" 
      hx-target="this" 
      hx-swap="outerHTML">
  <input name="email" type="email" required>
  <textarea name="message" required></textarea>
  <button type="submit">Send</button>
</form>

The server returns the form with error classes and messages when validation fails, or a success message when it passes.

Common Pitfalls and Solutions

Button vs Form Attributes

A frequent mistake is placing hx-post on the submit button instead of the form:

<!-- Wrong: validation won't trigger -->
<form hx-validate="true">
  <button hx-post="/submit">Submit</button>
</form>

<!-- Correct: validation works -->
<form hx-post="/submit" hx-validate="true">
  <button type="submit">Submit</button>
</form>

Handling Validation Events

htmx provides events for fine-grained control over validation flow:

// Intercept before validation
document.body.addEventListener('htmx:configRequest', function(evt) {
  if (evt.target.matches('[data-confirm]')) {
    evt.preventDefault()
    if (confirm(evt.target.dataset.confirm)) {
      evt.detail.issueRequest()
    }
  }
})

// Handle server validation errors
document.body.addEventListener('htmx:responseError', function(evt) {
  if (evt.detail.xhr.status === 422) {
    // Display validation errors from server
    const errors = JSON.parse(evt.detail.xhr.response)
    showValidationErrors(errors)
  }
})

Best Practices for Accessible Validation

Always include proper ARIA attributes for screen readers:

<input name="email" 
       aria-invalid="true" 
       aria-describedby="email-error">
<span id="email-error" role="alert">
  Please enter a valid email
</span>

Use hx-trigger="blur" instead of keyup for fields where constant validation might be annoying, like passwords. For search or username availability, keyup delay:500ms provides a good balance.

Conclusion

htmx form validation shines by keeping validation logic where it belongs—on the server—while providing the smooth, responsive experience users expect. Whether you choose client-side validation for instant feedback or server-side validation for complex rules, htmx reduces the JavaScript you write while improving the user experience. Start with hx-validate="true" for basic needs, then progressively add inline validation for a polished, production-ready form.

FAQs

Yes, htmx can use HTML5 validation attributes with hx-validate=true for basic client-side validation. However, htmx's real strength comes from server-side validation where you can implement complex business rules and database checks while maintaining a smooth user experience.

When using hx-validate=true, htmx automatically prevents submission if HTML5 validation fails. For server-side validation, return a non-200 status code or use htmx events like htmx:validation:failed to stop the submission and display error messages.

The blur trigger validates when users leave a field, reducing distractions during typing. The keyup trigger validates as users type, providing immediate feedback. Use blur for password fields and keyup with delay for username availability checks.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay