|$ curl https://forge-ai.dev/api/markdown?path=docs/html/validation
$cat docs/html-form-validation.md
updated This week·35 min read·published

HTML Form Validation

HTMLFormsValidationIntermediate
Introduction

HTML5 provides built-in form validation through attributes and the Constraint Validation API. Before HTML5, developers relied entirely on JavaScript for validation. Today, you can enforce required fields, format constraints, and range limits using only HTML attributes — with the browser handling the user interface and error messages natively.

Client-side validation improves user experience by providing instant feedback without a server round-trip. However, it is never a substitute for server-side validation — client-side checks are easily bypassed. Always validate on both sides of the network.

HTML5 Built-in Validation

HTML5 validation attributes let you specify constraints declaratively, without JavaScript. The browser automatically checks these constraints on form submission and displays native error tooltips. Each attribute targets a specific type of validation.

AttributeApplies ToValidation Rule
requiredMost input types, textarea, selectField must have a non-empty value
minlengthtext, email, url, tel, search, password, textareaValue must be at least N characters
maxlengthtext, email, url, tel, search, password, textareaValue must not exceed N characters
minnumber, date, range, datetime-local, month, week, timeValue must be at least N (numeric or date)
maxnumber, date, range, datetime-local, month, week, timeValue must not exceed N (numeric or date)
patterntext, tel, email, url, search, passwordValue must match the regex pattern
stepnumber, date, range, datetime-local, month, week, timeValue must be a multiple of step from min
typeemail, url, numberValue must match the type's format (e.g., valid email)
multipleemail, fileComma-separated email values or multiple files
built-in-validation.html
HTML
1<!-- Required fields -->
2<form>
3 <label for="name">Name <span class="required">*</span></label>
4 <input type="text" id="name" name="name" required />
5
6 <label for="email">Email <span class="required">*</span></label>
7 <input type="email" id="email" name="email" required />
8
9 <button type="submit">Submit</button>
10</form>
11
12<!-- Character length constraints -->
13<input
14 type="text"
15 id="username"
16 name="username"
17 minlength="3"
18 maxlength="20"
19 required
20 placeholder="3-20 characters"
21/>
22
23<!-- Numeric range constraints -->
24<label for="age">Age</label>
25<input
26 type="number"
27 id="age"
28 name="age"
29 min="18"
30 max="120"
31 step="1"
32 required
33/>
34
35<!-- Date range constraints -->
36<label for="appointment">Appointment Date</label>
37<input
38 type="date"
39 id="appointment"
40 name="appointment"
41 min="2026-07-01"
42 max="2026-12-31"
43 required
44/>
45
46<!-- Pattern validation -->
47<label for="postcode">Postal Code (UK format)</label>
48<input
49 type="text"
50 id="postcode"
51 name="postcode"
52 pattern="[A-Z]{1,2}[0-9R][0-9A-Z]? [0-9][ABD-HJLNP-UW-Z]{2}"
53 title="UK postcode format e.g. SW1A 1AA"
54 placeholder="SW1A 1AA"
55/>
56
57<!-- Type-based validation -->
58<label for="website">Website</label>
59<input
60 type="url"
61 id="website"
62 name="website"
63 placeholder="https://example.com"
64 required
65/>
66
67<!-- Multiple email addresses -->
68<label for="team-emails">Team Emails (comma-separated)</label>
69<input
70 type="email"
71 id="team-emails"
72 name="team-emails"
73 multiple
74 placeholder="alice@example.com, bob@example.com"
75/>

best practice

Always include the title attribute when using pattern. The title text is shown in the browser's validation error message and helps users understand the required format. For example, title="5-digit ZIP code (e.g., 12345)" tells the user exactly what to enter.
Constraint Validation API

The Constraint Validation API gives JavaScript access to the browser's validation engine. Every form element exposes a validity object with boolean properties for each validation constraint. The checkValidity() method triggers validation, and setCustomValidity() lets you set custom error messages.

PropertyTypeDescription
validity.valueMissingbooleanElement has required attribute but no value
validity.typeMismatchbooleanValue does not match the type (email, url)
validity.patternMismatchbooleanValue does not match the pattern attribute
validity.tooLongbooleanValue exceeds maxlength (only if user edits)
validity.tooShortbooleanValue is shorter than minlength
validity.rangeUnderflowbooleanValue is less than min
validity.rangeOverflowbooleanValue is greater than max
validity.stepMismatchbooleanValue does not match the step increment
validity.badInputbooleanBrowser cannot parse the value (e.g., "abc" in number)
validity.customErrorbooleanCustom validity message has been set
validity.validbooleanAll constraints pass (no errors)
checkValidity()methodReturns true if valid, triggers validation UI if invalid
reportValidity()methodLike checkValidity() but also shows the browser's error bubble
setCustomValidity(msg)methodSets a custom error message (empty string = no error)
validationMessagepropertyReturns the current validation message string
willValidatepropertyTrue if the element will be validated on submit
constraint-validation-api.html
HTML
1<form id="signupForm" novalidate>
2 <label for="fullname">Full Name</label>
3 <input
4 type="text"
5 id="fullname"
6 name="fullname"
7 required
8 minlength="2"
9 maxlength="100"
10 pattern="[A-Za-z\s]+"
11 title="Only letters and spaces"
12 />
13
14 <label for="age">Age</label>
15 <input
16 type="number"
17 id="age"
18 name="age"
19 required
20 min="18"
21 max="120"
22 step="1"
23 />
24
25 <label for="email">Email</label>
26 <input
27 type="email"
28 id="email"
29 name="email"
30 required
31 />
32
33 <button type="submit">Submit</button>
34</form>
35
36<script>
37 const form = document.getElementById('signupForm');
38
39 form.addEventListener('submit', (e) => {
40 e.preventDefault();
41
42 // Check each field using the ValidityState API
43 const name = document.getElementById('fullname');
44 const age = document.getElementById('age');
45 const email = document.getElementById('email');
46
47 let isValid = true;
48
49 // Validate name manually
50 const nameState = name.validity;
51 if (nameState.valueMissing) {
52 name.setCustomValidity('Please enter your full name');
53 isValid = false;
54 } else if (nameState.patternMismatch) {
55 name.setCustomValidity('Only letters and spaces allowed');
56 isValid = false;
57 } else if (nameState.tooShort) {
58 name.setCustomValidity('Name must be at least 2 characters');
59 isValid = false;
60 } else {
61 name.setCustomValidity('');
62 }
63
64 // Validate age
65 const ageState = age.validity;
66 if (ageState.valueMissing) {
67 age.setCustomValidity('Please enter your age');
68 isValid = false;
69 } else if (ageState.rangeUnderflow) {
70 age.setCustomValidity('You must be at least 18 years old');
71 isValid = false;
72 } else if (ageState.rangeOverflow) {
73 age.setCustomValidity('Age cannot exceed 120');
74 isValid = false;
75 } else {
76 age.setCustomValidity('');
77 }
78
79 // Validate email
80 const emailState = email.validity;
81 if (emailState.valueMissing) {
82 email.setCustomValidity('Please enter your email');
83 isValid = false;
84 } else if (emailState.typeMismatch) {
85 email.setCustomValidity('Please enter a valid email address');
86 isValid = false;
87 } else {
88 email.setCustomValidity('');
89 }
90
91 if (isValid) {
92 // Report any remaining validity issues
93 if (form.checkValidity()) {
94 alert('Form is valid! Submitting...');
95 // form.submit();
96 }
97 } else {
98 // Show the first error
99 form.reportValidity();
100 }
101 });
102
103 // Clear custom validity on input
104 form.querySelectorAll('input').forEach((input) => {
105 input.addEventListener('input', () => {
106 input.setCustomValidity('');
107 });
108 });
109</script>
🔥

pro tip

Always call setCustomValidity('') to reset the custom error before rechecking validity. An empty string means no custom error — a non-empty string marks the field as invalid with that message. The reportValidity() method triggers the browser's native error tooltip, so you don't need to build custom error UI for simple cases.
Custom Validation Messages

The browser's default validation messages are generic and vary by locale. Use the Constraint Validation API's setCustomValidity() method to provide user-friendly, context-specific messages. You can also use the validationMessage property to read the current message or checkValidity() with oninvalid handler for inline customization.

custom-validation.html
HTML
1<!-- Approach 1: oninvalid handler (declarative) -->
2<input
3 type="text"
4 id="zip"
5 name="zip"
6 pattern="[0-9]{5}"
7 required
8 oninvalid="this.setCustomValidity('Please enter a 5-digit ZIP code (e.g., 12345)')"
9 oninput="this.setCustomValidity('')"
10 placeholder="12345"
11/>
12
13<!-- Approach 2: JavaScript with setCustomValidity -->
14<form id="contactForm" novalidate>
15 <label for="phone">Phone Number</label>
16 <input
17 type="tel"
18 id="phone"
19 name="phone"
20 pattern="[+][0-9]{1,3}[0-9]{3,14}"
21 placeholder="+1-555-0123"
22 required
23 />
24 <span id="phone-error" class="error-message"></span>
25
26 <button type="submit">Submit</button>
27</form>
28
29<script>
30 const contactForm = document.getElementById('contactForm');
31 const phoneInput = document.getElementById('phone');
32 const phoneError = document.getElementById('phone-error');
33
34 contactForm.addEventListener('submit', (e) => {
35 e.preventDefault();
36
37 // Clear previous custom validity
38 phoneInput.setCustomValidity('');
39
40 if (phoneInput.validity.valueMissing) {
41 phoneInput.setCustomValidity('Phone number is required');
42 } else if (phoneInput.validity.patternMismatch) {
43 phoneInput.setCustomValidity(
44 'Please enter a valid number with country code, e.g. +1-555-0123'
45 );
46 }
47
48 if (!phoneInput.checkValidity()) {
49 phoneError.textContent = phoneInput.validationMessage;
50 phoneInput.focus();
51 } else {
52 phoneError.textContent = '';
53 alert('Phone number is valid!');
54 }
55 });
56
57 // Clear error on input
58 phoneInput.addEventListener('input', () => {
59 phoneInput.setCustomValidity('');
60 phoneError.textContent = '';
61 });
62</script>
63
64<!-- Approach 3: Custom validation on blur -->
65<input
66 type="password"
67 id="password"
68 name="password"
69 required
70 minlength="8"
71/>
72
73<script>
74 const passwordInput = document.getElementById('password');
75
76 passwordInput.addEventListener('blur', () => {
77 const value = passwordInput.value;
78
79 if (value.length === 0) {
80 passwordInput.setCustomValidity('Password is required');
81 } else if (value.length < 8) {
82 passwordInput.setCustomValidity(
83 'Password must be at least 8 characters'
84 );
85 } else if (!/[A-Z]/.test(value)) {
86 passwordInput.setCustomValidity(
87 'Password must contain at least one uppercase letter'
88 );
89 } else if (!/[0-9]/.test(value)) {
90 passwordInput.setCustomValidity(
91 'Password must contain at least one number'
92 );
93 } else {
94 passwordInput.setCustomValidity('');
95 }
96
97 passwordInput.reportValidity();
98 });
99</script>

info

Use the oninvalid attribute for simple custom messages without writing JavaScript. It sets the custom validity message when validation fails. Always pair it with oninput="this.setCustomValidity('')" to clear the message when the user starts fixing the error. This pattern keeps custom messages lightweight and declarative.
Styling Valid / Invalid States

CSS pseudo-classes let you style form controls based on their validation state without JavaScript. The browser applies these classes automatically as validation state changes — on keystroke, on blur, and on submit. The newer :user-valid and :user-invalid pseudo-classes only apply after the user has interacted with the field, preventing premature error indicators.

Pseudo-classApplied WhenUse Case
:validField passes all constraintsShow green border or check icon
:invalidField fails any constraintShow red border on submit (applies on page load for required empty fields)
:user-validUser has interacted + validShow success styling after user types
:user-invalidUser has interacted + invalidShow error styling only after user types
:requiredField has required attributeStyle required field indicators
:optionalField lacks required attributeSubtle styling for optional fields
:in-rangeValue within min/max boundsRange slider inside valid range
:out-of-rangeValue outside min/max boundsRange slider outside valid range
:focus-withinElement or descendant has focusHighlight entire field group on focus
:placeholder-shownPlaceholder is currently visibleHide validation styles until user types
validation-styling.css
CSS
1/* Base input styles */
2input {
3 border: 1px solid #d1d5db;
4 padding: 10px 12px;
5 border-radius: 6px;
6 outline: none;
7 transition: border-color 0.2s, box-shadow 0.2s;
8 width: 100%;
9 font-size: 14px;
10}
11
12/* Valid state */
13input:valid {
14 border-color: #22c55e;
15}
16
17/* Invalid state (applies immediately on page load for required) */
18input:invalid {
19 border-color: #d1d5db; /* Neutral until user interaction */
20}
21
22/* User-interacted invalid — show error */
23input:user-invalid {
24 border-color: #ef4444;
25 box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.15);
26}
27
28/* User-interacted valid — show success */
29input:user-valid {
30 border-color: #22c55e;
31 box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.15);
32}
33
34/* Focus state overrides */
35input:focus {
36 border-color: #3b82f6;
37 box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
38}
39
40input:focus:user-invalid {
41 border-color: #ef4444;
42 box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.2);
43}
44
45input:focus:user-valid {
46 border-color: #22c55e;
47 box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.2);
48}
49
50/* Disabled */
51input:disabled {
52 opacity: 0.5;
53 cursor: not-allowed;
54 background: #f9fafb;
55}
56
57/* Required field label indicator */
58label:has(+ input:required)::after,
59label:has(+ input:required)::after {
60 content: " *";
61 color: #ef4444;
62}
63
64/* Range slider states */
65input[type="range"]:in-range {
66 accent-color: #22c55e;
67}
68
69input[type="range"]:out-of-range {
70 accent-color: #ef4444;
71}
72
73/* Error message styling */
74.error-message {
75 color: #ef4444;
76 font-size: 12px;
77 margin-top: 4px;
78 display: none;
79}
80
81input:user-invalid ~ .error-message {
82 display: block;
83}
84
85/* Success message */
86.success-message {
87 color: #22c55e;
88 font-size: 12px;
89 margin-top: 4px;
90 display: none;
91}
92
93input:user-valid ~ .success-message {
94 display: block;
95}

best practice

Use :user-valid and :user-invalid instead of :valid/:invalid for user-facing validation styling. The basic pseudo-classes apply on page load (an empty required field is immediately invalid), which causes a poor UX with red borders before the user has typed anything. The :user-* variants only apply after the user has interacted with the field.
Real-time vs Submit Validation

Validation can happen at different times: on keystroke (real-time), on blur (when the field loses focus), on form submit, or a combination. Each approach has different UX implications. Real-time validation catches errors early but can be annoying if messages appear before the user finishes typing. Submit validation is less intrusive but delays feedback.

StrategyTriggerProsCons
Submit validationForm submissionSimple, no premature errorsUser fills entire form before seeing any error
Blur validationField loses focusValidates after user finishes each fieldMay not see error if moving quickly
Real-time validationOn each keystrokeInstant feedbackCan be distracting; premature errors while typing
HybridBlur + submitBest UX — validate on blur, re-validate on submitSlightly more code
Debounced real-timeKeystroke with delayReal-time feel without constant checkingImplementation complexity
validation-strategies.html
HTML
1<form id="hybridForm" novalidate>
2 <div class="field">
3 <label for="h-name">Full Name</label>
4 <input
5 type="text"
6 id="h-name"
7 name="name"
8 required
9 minlength="2"
10 pattern="[A-Za-z\s]+"
11 />
12 <span class="error-msg" id="h-name-error"></span>
13 </div>
14
15 <div class="field">
16 <label for="h-email">Email</label>
17 <input
18 type="email"
19 id="h-email"
20 name="email"
21 required
22 />
23 <span class="error-msg" id="h-email-error"></span>
24 </div>
25
26 <button type="submit">Submit</button>
27</form>
28
29<script>
30 const hybridForm = document.getElementById('hybridForm');
31 const fields = [
32 { input: document.getElementById('h-name'), error: document.getElementById('h-name-error') },
33 { input: document.getElementById('h-email'), error: document.getElementById('h-email-error') },
34 ];
35
36 // Real-time validation: show/hide error messages on input
37 // but only after the field has been blurred at least once
38 fields.forEach(({ input, error }) => {
39 let hasBeenBlurred = false;
40
41 input.addEventListener('blur', () => {
42 hasBeenBlurred = true;
43 validateField(input, error);
44 });
45
46 input.addEventListener('input', () => {
47 if (hasBeenBlurred) {
48 validateField(input, error);
49 }
50 });
51 });
52
53 function validateField(input, errorEl) {
54 input.setCustomValidity('');
55
56 // Check validity and set custom messages
57 if (input.validity.valueMissing) {
58 input.setCustomValidity('This field is required');
59 } else if (input.validity.typeMismatch) {
60 input.setCustomValidity('Please enter a valid value');
61 } else if (input.validity.patternMismatch) {
62 input.setCustomValidity('Format is incorrect');
63 } else if (input.validity.tooShort) {
64 input.setCustomValidity('Value is too short');
65 }
66
67 if (!input.checkValidity()) {
68 errorEl.textContent = input.validationMessage;
69 input.setAttribute('aria-invalid', 'true');
70 } else {
71 errorEl.textContent = '';
72 input.removeAttribute('aria-invalid');
73 }
74 }
75
76 // Submit validation: check all fields again
77 hybridForm.addEventListener('submit', (e) => {
78 e.preventDefault();
79 let hasError = false;
80
81 fields.forEach(({ input, error }) => {
82 validateField(input, error);
83 if (!input.checkValidity()) {
84 hasError = true;
85 }
86 });
87
88 if (!hasError) {
89 alert('Form submitted successfully!');
90 }
91 });
92</script>
🔥

pro tip

The hybrid approach (validate on blur, re-validate on submit) provides the best user experience. Validate each field when the user leaves it (blur), then re-validate all fields on form submission. Use a hasBeenBlurred flag to prevent showing errors before the user has had a chance to interact with the field. For password strength and other inline feedback, use real-time validation with debouncing.

Live preview of an interactive form with hybrid validation — errors appear on blur and are cleared on valid input:

preview
Accessibility

Accessible form validation ensures screen reader users and keyboard-only users receive clear, timely feedback about errors. The key techniques are: linking error messages to inputs with aria-describedby, marking invalid fields with aria-invalid, and using role="alert" on error containers for immediate announcement.

TechniqueImplementationImpact
aria-invalidaria-invalid="true"Screen reader announces field as invalid
aria-describedbyaria-describedby="error-id"Links input to its error message
role="alert"role="alert" on error divImmediately announces dynamic error content
aria-livearia-live="polite" or "assertive"Region that announces content changes
aria-errormessagearia-errormessage="error-id"Explicitly identifies the element containing the error
Focus managementinput.focus() on errorMoves focus to first invalid field
Summaryaria-live region with error countProvides overview of all errors
validation-accessibility.html
HTML
1<!-- Accessible validation form -->
2<form id="accessibleForm" novalidate>
3 <div class="form-group">
4 <label for="fullname">Full Name</label>
5 <input
6 type="text"
7 id="fullname"
8 name="fullname"
9 required
10 minlength="2"
11 aria-required="true"
12 aria-describedby="fullname-error"
13 />
14 <span
15 id="fullname-error"
16 class="error-message"
17 role="alert"
18 aria-live="assertive"
19 ></span>
20 </div>
21
22 <div class="form-group">
23 <label for="email">Email Address</label>
24 <input
25 type="email"
26 id="email"
27 name="email"
28 required
29 aria-required="true"
30 aria-describedby="email-error"
31 />
32 <span
33 id="email-error"
34 class="error-message"
35 role="alert"
36 aria-live="polite"
37 ></span>
38 </div>
39
40 <button type="submit">Submit</button>
41</form>
42
43<!-- Error summary region -->
44<div
45 id="error-summary"
46 class="error-summary"
47 role="alert"
48 aria-live="polite"
49 tabindex="-1"
50 hidden
51>
52 <h3>Please fix the following errors:</h3>
53 <ul id="error-list"></ul>
54</div>
55
56<script>
57 const aForm = document.getElementById('accessibleForm');
58 const errorSummary = document.getElementById('error-summary');
59 const errorList = document.getElementById('error-list');
60
61 aForm.addEventListener('submit', (e) => {
62 e.preventDefault();
63 const errors = [];
64
65 const fields = [
66 { input: document.getElementById('fullname'), name: 'Full Name' },
67 { input: document.getElementById('email'), name: 'Email' },
68 ];
69
70 fields.forEach(({ input, name }) => {
71 const errorEl = document.getElementById(input.id + '-error');
72 input.setCustomValidity('');
73
74 if (input.validity.valueMissing) {
75 const msg = name + ' is required';
76 input.setCustomValidity(msg);
77 errors.push(msg);
78 } else if (input.validity.typeMismatch) {
79 const msg = 'Please enter a valid ' + name.toLowerCase();
80 input.setCustomValidity(msg);
81 errors.push(msg);
82 } else if (input.validity.tooShort) {
83 const msg = name + ' is too short';
84 input.setCustomValidity(msg);
85 errors.push(msg);
86 }
87
88 if (!input.checkValidity()) {
89 errorEl.textContent = input.validationMessage;
90 input.setAttribute('aria-invalid', 'true');
91 } else {
92 errorEl.textContent = '';
93 input.removeAttribute('aria-invalid');
94 }
95 });
96
97 // Update error summary
98 if (errors.length > 0) {
99 errorList.innerHTML = errors
100 .map((e) => '<li>' + e + '</li>')
101 .join('');
102 errorSummary.hidden = false;
103 errorSummary.focus();
104 } else {
105 errorSummary.hidden = true;
106 alert('Form submitted successfully!');
107 }
108 });
109
110 // Clear individual errors on input
111 aForm.querySelectorAll('input').forEach((input) => {
112 input.addEventListener('input', () => {
113 input.setCustomValidity('');
114 const errorEl = document.getElementById(input.id + '-error');
115 errorEl.textContent = '';
116 input.removeAttribute('aria-invalid');
117 });
118 });
119</script>

best practice

Use aria-describedby to link each input to its error message element. Screen readers announce the described-by content when the input receives focus, so the user hears the error message immediately. The role="alert" on the error span ensures the message is announced as soon as it appears. Move focus to the first invalid field or an error summary region after form submission.
Best Practices

Validation Design Principles

Always validate on the server — client-side validation is a UX convenience, not a security measure. Malicious users can bypass browser validation and submit arbitrary data
Use the hybrid validation strategy: validate on blur (when the user leaves a field) and re-validate all fields on form submission. This gives immediate feedback without interrupting the user while typing
Write clear, specific error messages that tell the user what went wrong and how to fix it. Avoid generic messages like 'Invalid input' — instead use 'Password must be at least 8 characters with one uppercase letter'
Place error messages immediately below the relevant field, not grouped at the top of the form. The proximity helps users associate the error with the correct input
Use the Constraint Validation API's validity object to provide tailored messages for each failure type (valueMissing, patternMismatch, tooShort, rangeUnderflow, etc.) instead of a single generic message
Apply :user-valid and :user-invalid pseudo-classes for validation styling — they only activate after user interaction, preventing premature red borders on empty required fields
Set aria-invalid='true' on invalid fields and aria-describedby to link the error message. Use role='alert' on error containers for immediate screen reader announcements
Use inputmode to show the appropriate mobile keyboard (numeric, email, url, tel) alongside the correct input type — this improves the mobile experience significantly
Include a novalidate attribute on the form if you want to handle all validation with JavaScript. This suppresses the browser's native validation tooltips
Test your validation with screen readers (VoiceOver on macOS, NVDA on Windows) to ensure error messages are announced correctly and focus management works as expected

Common Pitfalls

PitfallWhy It HurtsSolution
Client-only validationEasily bypassed — no real securityAlways re-validate on the server
Unclear error messagesUsers don't know how to fix the errorBe specific about what's required
Real-time validation on empty fieldsRed borders before user typesUse :user-invalid or validate on blur
No focus managementScreen reader users don't know about errorsFocus first invalid field or summary
Missing aria attributesErrors invisible to screen readersUse aria-invalid and aria-describedby
Password requirements hiddenUsers guess invalid passwordsShow requirements before/during typing

warning

Never trust client-side validation. Always validate and sanitize all user input on the server. Client-side validation is a UX feature that improves the user experience by providing instant feedback — it is not a security boundary. A user can disable JavaScript, use browser DevTools to remove constraints, or send HTTP requests directly to your server with arbitrary data.

Live preview of a complete validation demo with all best practices applied — accessible, user-friendly, with proper styling:

preview
$Blueprint — Engineering Documentation·Section ID: HTML-21·Revision: 1.0