HTMX Inline Validation
Inline validation checks a form field and shows feedback the moment the user finishes typing — before the entire form is submitted. This saves users the frustration of filling out a long form, hitting Submit, and only then discovering a typo on line one. With HTMX, inline validation requires no custom JavaScript. You attach a trigger to each input, point it at a server route, and the server returns the validation message as HTML.
Why Server-Side Validation Matters
Client-side validation (using HTML attributes like required or pattern) is fast but limited. It cannot check whether a username already exists in your database, whether a coupon code is valid, or whether an email address belongs to an active account. Server-side validation handles all of these cases. HTMX brings that server-side power to the inline, per-field level.
Basic Pattern
<label>Username</label> <input type="text" name="username" hx-post="/validate/username" hx-trigger="change delay:300ms" hx-target="#username-error" hx-swap="innerHTML"> <span id="username-error"></span>
Diagram:
User types "john_doe" into username field
|
| 300ms after last keystroke
v
HTMX POST /validate/username (body: username=john_doe)
|
v
Server checks database
|
┌─────┴──────┐
Available Already taken
| |
Returns "" Returns error HTML
| |
#username-error #username-error shows:
stays empty "Username already taken"
Server Response Examples
The server returns an empty string when the value is valid, and an error fragment when it is not:
Flask (Python)
@app.route('/validate/username', methods=['POST'])
def validate_username():
username = request.form.get('username', '').strip()
if len(username) < 3:
return '<span style="color:red">At least 3 characters required</span>'
if User.query.filter_by(username=username).first():
return '<span style="color:red">Username already taken</span>'
return '<span style="color:green">Username available</span>'
Node.js (Express)
app.post('/validate/username', (req, res) => {
const { username } = req.body;
if (!username || username.length < 3) {
return res.send('<span style="color:red">At least 3 characters required</span>');
}
const taken = db.users.find(u => u.username === username);
if (taken) {
return res.send('<span style="color:red">Username already taken</span>');
}
res.send('<span style="color:green">Username available ✓</span>');
});
Full Sign-Up Form With Inline Validation
<form hx-post="/register" hx-target="#form-result">
<div>
<label>Username</label>
<input type="text" name="username"
hx-post="/validate/username"
hx-trigger="change delay:300ms"
hx-target="#username-msg">
<span id="username-msg"></span>
</div>
<div>
<label>Email</label>
<input type="email" name="email"
hx-post="/validate/email"
hx-trigger="change delay:300ms"
hx-target="#email-msg">
<span id="email-msg"></span>
</div>
<div>
<label>Password</label>
<input type="password" name="password"
hx-post="/validate/password"
hx-trigger="keyup delay:500ms"
hx-target="#password-msg">
<span id="password-msg"></span>
</div>
<button type="submit">Create Account</button>
</form>
<div id="form-result"></div>
Visual flow: ┌──────────────────────────────────────────┐ │ Username: [john_doe____________] │ │ ✓ Username available │ ← green message │ │ │ Email: [john@example.com____] │ │ ✗ Email already registered │ ← red message │ │ │ Password: [••••••_________________] │ │ ✗ At least 8 characters │ ← red message │ │ │ [Create Account] │ └──────────────────────────────────────────┘
Preventing Form Submission When Errors Exist
HTMX inline validation shows messages but does not automatically block the final form submission. To prevent submission when errors exist, the server's final POST handler re-validates all fields before saving. If validation fails, it returns the form HTML with error messages already filled in. This keeps the server as the single source of truth and prevents bad data even if someone bypasses the browser.
Password Strength Indicator
<input type="password" name="password" hx-post="/check-strength" hx-trigger="keyup delay:200ms" hx-target="#strength-bar"> <div id="strength-bar"></div>
Server response for a weak password:
<div> <span style="color:red">Weak — add numbers and symbols</span> <progress value="25" max="100"></progress> </div>
Server response for a strong password:
<div> <span style="color:green">Strong password ✓</span> <progress value="100" max="100"></progress> </div>
Including Other Fields in the Validation Request
Sometimes validation of one field depends on the value of another — for example, a "confirm password" field needs to compare against the "password" field. Use hx-include to send additional input values along with the request:
<input type="password" name="password" id="password"> <input type="password" name="confirm_password" hx-post="/validate/confirm-password" hx-trigger="keyup delay:300ms" hx-target="#confirm-msg" hx-include="#password"> <span id="confirm-msg"></span>
HTMX sends both confirm_password and password in the POST body. The server compares the two and returns a match or mismatch message.
Key Takeaway
HTMX inline validation triggers a server request when a user leaves a field or stops typing. The server checks the value against real business rules — database lookups, format checks, cross-field comparisons — and returns an HTML fragment with the result. That fragment appears instantly next to the field. Users see clear feedback on every field before they ever click Submit. The server always re-validates on final submission, so the system is secure regardless of what happens in the browser.
