Forms Skill
Production-grade HTML5 forms with Constraint Validation API and accessible patterns.
🎯 Purpose
Provide atomic, single-responsibility operations for:
- Form structure and layout
- Input types and validation
- Constraint Validation API usage
- Accessible form patterns
- Error handling and display
- Multi-step form logic
📥 Input Schema
interface FormsInput {
operation: 'create' | 'validate' | 'pattern' | 'convert';
form_type: FormType;
fields?: FieldDefinition[];
options?: {
validation_mode: 'html5' | 'javascript' | 'hybrid';
accessibility_level: 'A' | 'AA' | 'AAA';
autocomplete: boolean;
novalidate: boolean;
};
}
type FormType =
| 'contact' // Name, email, message
| 'login' // Email/username, password
| 'registration' // Full user signup
| 'search' // Search input
| 'checkout' // Payment form
| 'survey' // Questions, ratings
| 'multi-step'; // Wizard-style form
interface FieldDefinition {
name: string;
type: InputType;
label: string;
required?: boolean;
pattern?: string;
validation?: ValidationRule[];
}
📤 Output Schema
interface FormsOutput {
success: boolean;
markup: string;
validation_script?: string;
accessibility_score: number;
issues: FormIssue[];
}
🛠️ Core Patterns
1. Input Types Reference
| Type |
Purpose |
Validation |
Keyboard |
text |
Generic text |
pattern |
text |
email |
Email address |
Built-in email |
email |
password |
Passwords |
pattern |
text |
tel |
Phone numbers |
pattern |
tel |
url |
URLs |
Built-in URL |
url |
number |
Numeric values |
min/max/step |
numeric |
date |
Date picker |
min/max |
date |
time |
Time picker |
min/max/step |
time |
datetime-local |
Date + time |
min/max |
datetime |
search |
Search queries |
None |
search |
color |
Color picker |
None |
- |
range |
Slider |
min/max/step |
- |
file |
File upload |
accept |
- |
2. Complete Form Template
<form id="contact-form"
action="/api/contact"
method="POST"
novalidate
aria-labelledby="form-title">
<h2 id="form-title">Contact Us</h2>
<!-- Error Summary (initially hidden) -->
<div id="error-summary"
role="alert"
aria-live="polite"
hidden>
<h3>Please fix the following errors:</h3>
<ul id="error-list"></ul>
</div>
<!-- Name Field -->
<div class="field">
<label for="name">
Full Name
<span class="required" aria-hidden="true">*</span>
</label>
<input type="text"
id="name"
name="name"
required
aria-required="true"
autocomplete="name"
aria-describedby="name-hint">
<p id="name-hint" class="hint">Enter your full name</p>
<p id="name-error" class="error" aria-live="polite"></p>
</div>
<!-- Email Field -->
<div class="field">
<label for="email">
Email
<span class="required" aria-hidden="true">*</span>
</label>
<input type="email"
id="email"
name="email"
required
aria-required="true"
autocomplete="email"
aria-describedby="email-hint email-error">
<p id="email-hint" class="hint">We'll never share your email</p>
<p id="email-error" class="error" aria-live="polite"></p>
</div>
<!-- Message Field -->
<div class="field">
<label for="message">
Message
<span class="required" aria-hidden="true">*</span>
</label>
<textarea id="message"
name="message"
rows="5"
required
aria-required="true"
minlength="10"
maxlength="1000"
aria-describedby="message-hint message-count"></textarea>
<p id="message-hint" class="hint">10-1000 characters</p>
<p id="message-count" class="hint" aria-live="polite">0/1000</p>
<p id="message-error" class="error" aria-live="polite"></p>
</div>
<button type="submit">Send Message</button>
</form>
3. Constraint Validation API
const form = document.getElementById('contact-form');
const inputs = form.querySelectorAll('input, textarea, select');
// Disable browser default validation UI
form.setAttribute('novalidate', '');
// Validate on submit
form.addEventListener('submit', (e) => {
if (!validateForm()) {
e.preventDefault();
showErrorSummary();
focusFirstError();
}
});
// Validate on blur for immediate feedback
inputs.forEach(input => {
input.addEventListener('blur', () => validateField(input));
input.addEventListener('input', () => {
if (input.classList.contains('invalid')) {
validateField(input);
}
});
});
function validateForm() {
let isValid = true;
inputs.forEach(input => {
if (!validateField(input)) {
isValid = false;
}
});
return isValid;
}
function validateField(input) {
const errorEl = document.getElementById(`${input.id}-error`);
// Check validity using Constraint Validation API
if (!input.checkValidity()) {
const message = getErrorMessage(input);
showError(input, errorEl, message);
return false;
}
clearError(input, errorEl);
return true;
}
function getErrorMessage(input) {
const validity = input.validity;
if (validity.valueMissing) {
return `${input.labels[0].textContent} is required`;
}
if (validity.typeMismatch) {
return `Please enter a valid ${input.type}`;
}
if (validity.patternMismatch) {
return input.dataset.patternError || 'Please match the requested format';
}
if (validity.tooShort) {
return `Must be at least ${input.minLength} characters`;
}
if (validity.tooLong) {
return `Must be no more than ${input.maxLength} characters`;
}
if (validity.rangeUnderflow) {
return `Must be at least ${input.min}`;
}
if (validity.rangeOverflow) {
return `Must be no more than ${input.max}`;
}
return input.validationMessage;
}
function showError(input, errorEl, message) {
input.classList.add('invalid');
input.setAttribute('aria-invalid', 'true');
input.setAttribute('aria-errormessage', errorEl.id);
errorEl.textContent = message;
errorEl.hidden = false;
}
function clearError(input, errorEl) {
input.classList.remove('invalid');
input.removeAttribute('aria-invalid');
input.removeAttribute('aria-errormessage');
errorEl.textContent = '';
errorEl.hidden = true;
}
function focusFirstError() {
const firstError = form.querySelector('.invalid');
if (firstError) {
firstError.focus();
}
}
⚠️ Error Handling
Error Codes
| Code |
Description |
Recovery |
FORM001 |
Missing form label |
Add aria-labelledby or aria-label |
FORM002 |
Input without label |
Add <label> element |
FORM003 |
Invalid pattern regex |
Fix regex syntax |
FORM004 |
Missing required indicator |
Add visual + aria indicator |
FORM005 |
No error message element |
Add aria-live region |
FORM006 |
Autocomplete missing |
Add autocomplete attribute |
FORM007 |
Submit without action |
Add form action |
FORM008 |
Fieldset without legend |
Add legend element |
ValidityState Properties
| Property |
True When |
valid |
All constraints satisfied |
valueMissing |
Required field is empty |
typeMismatch |
Value doesn't match type (email, url) |
patternMismatch |
Value doesn't match pattern |
tooLong |
Value exceeds maxlength |
tooShort |
Value below minlength |
rangeUnderflow |
Value below min |
rangeOverflow |
Value above max |
stepMismatch |
Value doesn't match step |
badInput |
Browser can't convert input |
customError |
setCustomValidity() called |
🔍 Troubleshooting
Problem: Form not validating
Debug Checklist:
□ novalidate attribute set?
□ JavaScript calling checkValidity()?
□ Required attribute on mandatory fields?
□ Pattern regex valid?
□ Type attribute correct?
□ Min/max values valid?
Problem: Errors not announced
Debug Checklist:
□ Error container has role="alert"?
□ aria-live="polite" or "assertive" set?
□ Error linked via aria-errormessage?
□ aria-invalid="true" on field?
□ Error content actually changing?
Problem: Autocomplete not working
Debug Checklist:
□ autocomplete attribute present?
□ Correct autocomplete value?
□ Input has name attribute?
□ Form has action?
□ Browser autocomplete enabled?
Autocomplete Values
| Value |
Purpose |
name |
Full name |
given-name |
First name |
family-name |
Last name |
email |
Email address |
tel |
Phone number |
street-address |
Street address |
postal-code |
ZIP/Postal code |
country |
Country |
cc-number |
Credit card number |
cc-exp |
Card expiration |
cc-csc |
Security code |
username |
Username |
current-password |
Current password |
new-password |
New password |
📊 Form Types
Login Form
<form action="/login" method="POST" aria-labelledby="login-title">
<h2 id="login-title">Sign In</h2>
<div class="field">
<label for="username">Email or Username</label>
<input type="text"
id="username"
name="username"
required
autocomplete="username"
autofocus>
</div>
<div class="field">
<label for="password">Password</label>
<input type="password"
id="password"
name="password"
required
autocomplete="current-password"
minlength="8">
<a href="/forgot-password">Forgot password?</a>
</div>
<label class="checkbox">
<input type="checkbox" name="remember" value="1">
Remember me
</label>
<button type="submit">Sign In</button>
<p>Don't have an account? <a href="/register">Sign up</a></p>
</form>
Search Form
<form role="search"
action="/search"
method="GET"
aria-label="Site search">
<label for="search-input" class="visually-hidden">
Search
</label>
<input type="search"
id="search-input"
name="q"
placeholder="Search..."
autocomplete="off"
aria-describedby="search-hint">
<button type="submit" aria-label="Submit search">
<svg aria-hidden="true">...</svg>
</button>
<p id="search-hint" class="visually-hidden">
Enter keywords to search
</p>
</form>
Multi-Step Form
<form id="wizard" aria-labelledby="wizard-title">
<h2 id="wizard-title">Registration</h2>
<!-- Progress indicator -->
<nav aria-label="Registration progress">
<ol>
<li aria-current="step">
<span class="step-number">1</span>
<span class="step-label">Account</span>
</li>
<li>
<span class="step-number">2</span>
<span class="step-label">Profile</span>
</li>
<li>
<span class="step-number">3</span>
<span class="step-label">Confirm</span>
</li>
</ol>
</nav>
<!-- Step 1: Account -->
<fieldset id="step-1" class="step active">
<legend>Account Information</legend>
<div class="field">
<label for="email">Email</label>
<input type="email" id="email" name="email" required>
</div>
<div class="field">
<label for="password">Password</label>
<input type="password"
id="password"
name="password"
required
minlength="8"
autocomplete="new-password">
</div>
<button type="button" onclick="nextStep(1)">Next</button>
</fieldset>
<!-- Step 2: Profile (hidden initially) -->
<fieldset id="step-2" class="step" hidden>
<legend>Profile Information</legend>
<!-- fields -->
<button type="button" onclick="prevStep(2)">Back</button>
<button type="button" onclick="nextStep(2)">Next</button>
</fieldset>
<!-- Step 3: Confirm (hidden initially) -->
<fieldset id="step-3" class="step" hidden>
<legend>Confirm Details</legend>
<!-- summary -->
<button type="button" onclick="prevStep(3)">Back</button>
<button type="submit">Create Account</button>
</fieldset>
</form>
📋 Usage Examples
# Create contact form
skill: forms
operation: create
form_type: contact
options:
validation_mode: hybrid
accessibility_level: "AA"
autocomplete: true
# Validate form markup
skill: forms
operation: validate
markup: "<form>...</form>"
# Get login form pattern
skill: forms
operation: pattern
form_type: login
🔗 References