<form post="/api/register"
success="#registerSuccess"
error="#registerError"
loading="#registerLoading"
validate>
<input type="text" name="name" required minlength="2" />
<input type="email" name="email" required />
<input type="password" name="password" required minlength="8"
pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}" />
<button type="submit">Register</button> <!-- auto-disabled when invalid -->
</form>No.JS automatically detects native HTML5 validation attributes — no validate attribute needed for standard rules:
<!-- All native HTML5 validation works automatically -->
<input required />
<input minlength="3" maxlength="50" />
<input type="email" />
<input type="url" />
<input type="number" min="1" max="100" step="5" />
<input pattern="[0-9]{3}-[0-9]{4}" />The validate attribute is only needed for No.JS-specific validators that go beyond what HTML5 offers:
<input validate="custom:validateUsername" /> <!-- Custom function -->Use error-{rule} attributes to set a custom message for a specific validation rule, or error for a generic fallback:
<input type="email" name="email" required
error-required="Email is required"
error-email="Please enter a valid email"
error="This field is invalid" />When multiple rules fail, errors are resolved by priority (required → type-based → length → pattern → range).
Point an error attribute to a <template> using a # prefix to render rich error UI:
<input type="email" name="email" required
error="#emailError" />
<template id="emailError">
<span class="field-error" bind="$error"></span>
</template>Inside the template, $error contains the error message and $rule contains the failing rule name. The template is rendered after the <template> element and automatically removed when the field becomes valid.
Per-rule templates work too:
<input name="user" required validate="custom:checkAvailability"
error-required="#reqTpl"
error-custom="#customTpl" />Use error-class on the form or on individual fields to automatically toggle a CSS class when a field is invalid and touched:
<!-- Form-level: applies to all fields -->
<form validate error-class="is-invalid">
<input name="email" required /> <!-- gets .is-invalid when invalid + touched -->
</form>
<!-- Per-field override -->
<input name="name" required error-class="field-error" />The class is added only after the field has been touched (focused and blurred), so users don't see errors before interacting with the form.
Inside any <form> with the validate attribute, $form provides:
| Property | Type | Description |
|---|---|---|
$form.valid |
boolean |
true if all fields pass validation |
$form.dirty |
boolean |
true if any field has been modified |
$form.touched |
boolean |
true if any field has been focused and blurred |
$form.submitting |
boolean |
true while the request is in flight |
$form.pending |
boolean |
true while async validators are running |
$form.errors |
object |
Map of field names → error messages |
$form.values |
object |
Current form values |
$form.firstError |
string|null |
Error message of the first invalid field (DOM order) |
$form.errorCount |
number |
Number of fields currently failing validation |
$form.fields |
object |
Per-field state (see below) |
$form.reset() |
function |
Reset form to initial values, clear errors and classes |
<form post="/api/contact" validate>
<input type="text" name="name" required />
<input type="email" name="email" required />
<textarea name="message" required minlength="10"></textarea>
<p show="$form.errors.email" class="error" bind="$form.errors.email"></p>
<!-- Show first error as a summary -->
<p show="$form.firstError" class="error-summary" bind="$form.firstError"></p>
<p show="$form.errorCount" bind="$form.errorCount + ' field(s) need attention'"></p>
<button type="submit"
bind-disabled="!$form.valid || $form.submitting">
<span hide="$form.submitting">Send</span>
<span show="$form.submitting">Sending...</span>
</button>
</form>$form.fields exposes individual field state keyed by field name:
| Property | Type | Description |
|---|---|---|
$form.fields.{name}.valid |
boolean |
Whether this field passes validation |
$form.fields.{name}.error |
string|null |
Error message for this field |
$form.fields.{name}.dirty |
boolean |
Whether this field has been modified |
$form.fields.{name}.touched |
boolean |
Whether this field has been focused and blurred |
<form validate>
<input type="email" name="email" required />
<p show="$form.fields.email.touched && !$form.fields.email.valid"
bind="$form.fields.email.error"
class="error"></p>
</form>Use the as attribute to expose a field's state under a custom name in the context:
<form validate>
<input type="email" name="email" required as="emailField" />
<!-- Access via $form.fields.email OR via the alias -->
<p show="!emailField.valid && emailField.touched"
bind="emailField.error"></p>
</form>By default, validation runs on input and focusout. Use validate-on to change when visual feedback appears:
<!-- Form-level: validate on blur only -->
<form validate validate-on="blur">
<input name="email" required />
</form>
<!-- Per-field override -->
<form validate>
<input name="email" required validate-on="blur" />
<input name="name" required /> <!-- uses default: input + focusout -->
</form>Note: Internally,
$formdata is always kept up-to-date regardless ofvalidate-on. The trigger only controls when visual feedback (error messages, error classes) is shown.
Skip validation for a field based on a condition:
<form validate>
<input type="checkbox" on:change="hasCompany = $event.target.checked" />
<label>I have a company</label>
<input name="company" required
validate-if="hasCompany"
placeholder="Company name" />
</form>When validate-if evaluates to false, the field is treated as valid and excluded from $form.errors.
Submit buttons (<button type="submit">, <input type="submit">, and <button> without a type) are automatically disabled when the form is invalid. No bind-disabled needed:
<form validate>
<input name="email" required />
<button type="submit">Send</button> <!-- auto-disabled when invalid -->
</form>Buttons with type="button" are not affected. If you set a custom bind-disabled expression, the auto-disable is skipped for that button.
<script>
NoJS.validator('strongPassword', (value) => {
if (value.length < 8) return 'Must be at least 8 characters';
if (!/[A-Z]/.test(value)) return 'Must contain uppercase';
if (!/[0-9]/.test(value)) return 'Must contain a number';
return true;
});
</script>
<input type="password" validate="strongPassword" />Next: Routing →