Search for a command to run...
Last updated June 9, 2026
ctrovalidate-browser provides a Ctrovalidate controller class that bridges ctrovalidate-core validation logic to the DOM. It discovers fields via data attributes, manages event listeners, displays errors, and handles ARIA accessibility — all without a framework.
npm install ctrovalidate-browser ctrovalidate-corectrovalidate-core is a peer dependency and must be installed separately.
<form id="signup-form" novalidate>
<div class="field-group">
<label for="email">Email</label>
<input
type="email"
id="email"
name="email"
data-ctrovalidate-rules="required|email"
/>
<div class="error-message"></div>
</div>
<div class="field-group">
<label for="password">Password</label>
<input
type="password"
id="password"
name="password"
data-ctrovalidate-rules="required|minLength:8|strongPassword"
/>
<div class="error-message"></div>
</div>
<button type="submit">Sign Up</button>
</form>import { Ctrovalidate } from 'ctrovalidate-browser';
const form = document.querySelector('#signup-form');
const validator = new Ctrovalidate(form, {
realTime: true,
errorClass: 'is-invalid',
errorMessageClass: 'error-message',
pendingClass: 'is-validating',
});
form.addEventListener('submit', async (e) => {
e.preventDefault();
const isValid = await validator.validate();
if (isValid) {
// Submit form
}
});data-ctrovalidate-rulesPipe-delimited rule string. Parsed via parseRules() from ctrovalidate-core.
<input data-ctrovalidate-rules="required|email" />
<input data-ctrovalidate-rules="required|minLength:8|between:1,100" />See the Rules Catalog for all 22 built-in rules.
data-ctrovalidate-ifConditional validation — only validates when the dependency is met.
Syntax: fieldName:type or fieldName:type=value
| Type | Example | Behavior |
|---|---|---|
checked | agree:checked | Controller checkbox must be checked |
present | email:present | Controller must have a truthy value |
value=X | country:value=USA | Controller value must equal X |
<!-- Validate only if checkbox is checked -->
<input type="checkbox" name="agreeTerms" />
<input
name="signature"
data-ctrovalidate-if="agreeTerms:checked"
data-ctrovalidate-rules="required"
/>
<!-- Validate only if select has a specific value -->
<select name="country">
<option value="">Select</option>
<option value="USA">USA</option>
</select>
<input
name="state"
data-ctrovalidate-if="country:value=USA"
data-ctrovalidate-rules="required"
/>
<!-- Validate only if another field has a value -->
<input name="email" />
<input
name="confirmEmail"
data-ctrovalidate-if="email:present"
data-ctrovalidate-rules="required|email"
/>When the dependency is not met, the field is skipped (no errors displayed, returns valid). When the dependency becomes met, the dependent field is immediately re-validated.
data-ctrovalidate-messageCatch-all error message override for the field:
<input
name="email"
data-ctrovalidate-rules="required|email"
data-ctrovalidate-message="Please fix this field."
/>data-ctrovalidate-{ruleName}-messageRule-specific override. The attribute name is case-insensitive (HTML attributes are lowercase):
<input
name="password"
data-ctrovalidate-rules="required|minLength:8"
data-ctrovalidate-required-message="Password is required."
data-ctrovalidate-minlength-message="Must be at least 8 characters."
/>Rules can come from two sources that are merged together:
data-ctrovalidate-rules attribute on the elementschema option passed to the constructorHTML rules execute first, then schema rules. Fields discovered via the schema (no data attribute) are also validated.
const validator = new Ctrovalidate(form, {
schema: {
email: 'email',
username: 'required|minLength:3|alphaDash',
},
});If a field has both data-ctrovalidate-rules="required" and schema: { email: 'email' }, the combined rules are required|email.
When a rule fails, the error message is resolved in this order:
data-ctrovalidate-{ruleName}-message attributedata-ctrovalidate-message attributeCtrovalidate.setCustomMessages()'Invalid input.'Messages support {0}, {1} parameter substitution for rule params:
Ctrovalidate.setCustomMessages({
minLength: 'Must be at least {0} characters.',
});The constructor automatically:
HTMLFormElementnovalidate on the form to disable browser-native validationdata-ctrovalidate-rules (and schema fields)realTime: false)| Event | When Validation Runs |
|---|---|
blur | Always — validates and marks field as dirty |
input | Only if field is already dirty (prevents premature errors) |
Controller input | Dependent fields re-validate when controller changes |
await validator.validate() — validates every discovered field in parallel via Promise.all. Returns true only if all fields pass.
pendingClass is added to the field, existing errors clearedAbortController is created for the async ruleAbortController.abort() is calledAbortError caught silently — returns valid (no error displayed)The controller composes three internal classes:
| Class | Responsibility |
|---|---|
FormController | Field discovery, event listeners, state management |
RuleEngine | Executes rules against field values, resolves messages |
UIManager | DOM manipulation, error display/clear, ARIA attributes |
The UIManager searches for the error message container using a 3-level parent traversal:
errorMessageClass in the field's parent<div> with role="status" and aria-live="polite" placed immediately after the fieldResults are cached via WeakMap to avoid repeated DOM queries.
All class options (errorClass, errorMessageClass, pendingClass) support space-separated strings for Tailwind CSS compatibility:
const validator = new Ctrovalidate(form, {
errorClass: 'border-red-500 bg-red-50',
errorMessageClass: 'text-red-500 text-sm mt-1',
pendingClass: 'opacity-50 pointer-events-none',
});The controller automatically manages these attributes:
aria-invalid="true" — Added on validation failure, removed on successaria-describedby — Links the field to its error container by IDrole="status" — Set on auto-generated fallback containersaria-live="polite" — Set on auto-generated fallback containers<!-- After validation failure -->
<input
name="email"
aria-invalid="true"
aria-describedby="ctrovalidate-error-email"
/>
<div id="ctrovalidate-error-email" class="error-message"
role="status" aria-live="polite">
Please enter a valid email address.
</div>Generated error container IDs follow the pattern ctrovalidate-error-{fieldName} (sanitized). If the field has no name, a numeric counter is used.
| Feature | ctrovalidate-browser | ctrovalidate-core |
|---|---|---|
| DOM interaction | Full (form, events, ARIA) | None |
| Rule definition | Data attributes + schema | Schema object only |
| Message source | HTML attributes + global registry | options.messages + translator |
| Conditional validation | data-ctrovalidate-if | Not supported |
| Static rule registration | Ctrovalidate.addRule() | Not supported |
| Instance lifecycle | Constructor + destroy() | Pure functions |
| Async support | AbortController per field | AbortSignal per call |