Relume Style Guide
Form-Validation
Form Styles
To use Forms on a dark background, simply use the add-on class is-alternate
Form to Copy
Success! We’ll be in touch soon.
Something went wrong while submitting.
How to use documentation
# Form Validation System Documentation
## Overview
This form validation system provides real-time validation with visual feedback for various input types. It supports text inputs, emails, phone numbers, textareas, select dropdowns, radio buttons, and checkboxes.
## Quick Start
### Basic Structure
```html
<div data-form-validate="" class="form-group w-form">
<form>
<div data-validate="" class="form-field-group">
<!-- Your input fields here -->
</div>
</form>
</div>
```
**Required Attributes:**
- `data-form-validate` - Container for the entire form
- `data-validate` - Each field group that needs validation
- `data-submit` - Submit button wrapper
---
## Field Types & Validation Rules
### 1. Text Input (Name, Generic Text)
**HTML Example:**
```html
<div data-validate="" class="form-field-group">
<label for="name" class="form-label">Name <span class="form-required">*</span></label>
<div class="form-field">
<input class="form-input w-input"
type="text"
id="name"
name="name"
min="1"
maxlength="256"
required="">
</div>
</div>
```
**Validation Rules:**
- **Minimum length**: Set via `min` attribute (e.g., `min="1"`)
- **Maximum length**: Set via `maxlength` attribute (e.g., `maxlength="256"`)
- **Required**: Add `required` attribute
**Behavior:**
- Validation starts when user types beyond min length
- Shows error if length is below min or above max
---
### 2. Email Input
**HTML Example:**
```html
<div data-validate="" class="form-field-group">
<label for="email" class="form-label">Email <span class="form-required">*</span></label>
<div class="form-field">
<input class="form-input w-input"
type="email"
id="email"
name="email"
maxlength="256"
required="">
</div>
</div>
```
**Validation Rules:**
- **Pattern**: Must match `[text]@[domain].[extension]`
- **Regex**: `/^[^\s@]+@[^\s@]+\.[^\s@]+$/`
- **Required**: Add `required` attribute
**Valid Examples:**
- `hello@osmo.supply`
- `user.name+tag@example.co.uk`
**Invalid Examples:**
- `user@domain` (missing TLD)
- `@domain.com` (missing local part)
- `user domain@test.com` (contains spaces)
**Behavior:**
- Validation starts automatically when valid email format is detected
- Also triggers on blur (when field loses focus)
---
### 3. Phone Input (Tel)
**HTML Example:**
```html
<div data-validate="" class="form-field-group">
<label for="phone" class="form-label">Phone <span class="form-required">*</span></label>
<div class="form-field">
<input class="form-input w-input"
type="tel"
id="phone"
name="phone"
placeholder="+1 234 567 8900"
required="">
</div>
</div>
```
**Validation Rules:**
- **Pattern**: `/^(\+?\d{7,15})$/` (after cleaning)
- **Minimum**: 7 digits (local short numbers)
- **Maximum**: 15 digits (international standard)
- **Optional +**: For international format
- **Character Filtering**: Users CANNOT type letters - only numbers and phone characters
**Allowed Input Characters:**
- Numbers: `0-9`
- Formatting: `+`, `-`, `(`, `)`, space
- Navigation keys and copy/paste shortcuts
**Valid Examples:**
- Local: `555-1234`, `(555) 123-4567`, `555.123.4567`
- International: `+1 555 123 4567`, `+44 20 1234 5678`, `+31 20 123 4567`
**Special Features:**
- **Letter Blocking**: Users cannot type letters (a-z) at all
- **Auto-Clean Paste**: If invalid characters are pasted, they're automatically removed
- **Format Flexibility**: All formatting (spaces, dashes, parentheses) is stripped before validation
**Behavior:**
- Validation starts automatically when valid phone format is detected
- Also triggers on blur
---
### 4. Textarea (Message, Long Text)
**HTML Example:**
```html
<div data-validate="" class="form-field-group">
<label for="message" class="form-label">Message <span class="form-required">*</span></label>
<div class="form-field">
<textarea class="form-input is--textarea w-input"
id="message"
name="message"
min="3"
maxlength="5000"
placeholder="Your message..."
required=""></textarea>
</div>
</div>
```
**Validation Rules:**
- **Minimum length**: Set via `min` attribute (e.g., `min="3"`)
- **Maximum length**: Set via `maxlength` attribute (e.g., `maxlength="5000"`)
- **Required**: Add `required` attribute
**Behavior:**
- Same as text input
- Allows Enter key for new lines (doesn't submit form)
---
### 5. Select Dropdown
**HTML Example:**
```html
<div data-validate="" class="form-field-group">
<label for="category" class="form-label">Category <span class="form-required">*</span></label>
<div class="form-field">
<select id="category"
name="category"
class="form-input w-select"
required="">
<option value="">Select option</option>
<option value="First">First choice</option>
<option value="Second">Second choice</option>
<option value="Third">Third choice</option>
</select>
</div>
</div>
```
**Validation Rules:**
- Must have a valid value selected
- **Invalid values** (auto-disabled on page load):
- Empty string: `value=""`
- `value="disabled"`
- `value="null"`
- `value="false"`
**Behavior:**
- Validation starts immediately on selection change
- First option (placeholder) should have `value=""` to force selection
---
### 6. Radio Buttons
**HTML Example:**
```html
<div data-validate="" class="form-field-group">
<label class="form-label">Mode <span class="form-required">*</span></label>
<div data-radiocheck-group="" class="radiocheck-group">
<label class="radiocheck-field w-radio">
<input type="radio" name="Mode" id="dark-mode" value="Dark mode" class="w-form-formradioinput radio-input w-radio-input">
<span class="radiocheck-label w-form-label">Dark mode</span>
</label>
<label class="radiocheck-field w-radio">
<input type="radio" name="Mode" id="light-mode" value="Light mode" class="w-form-formradioinput radio-input w-radio-input">
<span class="radiocheck-label w-form-label">Light mode</span>
</label>
</div>
</div>
```
**Required Attribute:**
- `data-radiocheck-group` on the container
**Validation Rules:**
- **Required (default)**: At least 1 option must be selected
- **Optional**: Add `min="0"` to make optional
**Attributes:**
- `min` - Minimum selections required (default: 1)
- `min="0"` - Optional (no selection required)
- `min="1"` - Required (at least 1 selection)
- No min attribute - Defaults to required (min="1")
**Behavior:**
- Validation starts when minimum requirement is met
- Also triggers on blur
---
### 7. Checkbox Group (Multiple Selection)
**HTML Example:**
```html
<div data-validate="" class="form-field-group">
<label class="form-label">Location <span class="form-required">*</span>
<span class="form-inactive-text">(select min. 2)</span>
</label>
<div data-radiocheck-group="" min="2" max="2" class="radiocheck-group">
<label class="w-checkbox radiocheck-field">
<input type="checkbox" name="Location-Netherlands" id="Location-Netherlands" class="w-checkbox-input checkbox-input">
<span class="radiocheck-label w-form-label">The Netherlands</span>
</label>
<label class="w-checkbox radiocheck-field">
<input type="checkbox" name="Location-Germany" id="Location-Germany" class="w-checkbox-input checkbox-input">
<span class="radiocheck-label w-form-label">Germany</span>
</label>
<label class="w-checkbox radiocheck-field">
<input type="checkbox" name="Location-Belgium" id="Location-Belgium" class="w-checkbox-input checkbox-input">
<span class="radiocheck-label w-form-label">Belgium</span>
</label>
</div>
</div>
```
**Required Attribute:**
- `data-radiocheck-group` on the container
**Validation Rules:**
- **Minimum**: Set via `min` attribute (default: 1)
- **Maximum**: Set via `max` attribute (default: number of checkboxes)
**Attributes:**
- `min="2"` - At least 2 selections required
- `max="2"` - Maximum 2 selections allowed
- `min="0"` - Optional (no selection required)
- No attributes - At least 1 required, all can be selected
**Common Patterns:**
- **Exact number**: `min="2" max="2"` - Must select exactly 2
- **At least**: `min="2"` - Select 2 or more
- **Optional multiple**: `min="0" max="3"` - Select up to 3, or none
- **Required single**: No attributes - At least 1 must be selected
**Behavior:**
- Validation starts when minimum requirement is met
- Also triggers on blur
---
### 8. Single Checkbox (Terms & Conditions)
**HTML Example:**
```html
<div data-validate="" class="form-field-group">
<div data-radiocheck-group="" class="radiocheck-group">
<label class="w-checkbox radiocheck-field">
<input type="checkbox"
name="Terms-Conditions"
id="Terms-Conditions"
required=""
class="w-checkbox-input checkbox-input">
<span class="radiocheck-label w-form-label">
Accept Terms & Conditions <span class="form-required">*</span>
</span>
</label>
</div>
</div>
```
**Validation Rules:**
- Must be checked to pass validation
- Automatically detected when there's only 1 checkbox in the group
**Behavior:**
- Validation starts immediately on check/uncheck
- Also triggers on blur
---
## Visual States & CSS Classes
### Dynamic State Classes
The system automatically adds/removes these classes based on field state:
#### 1. `is--filled`
- **Added when**: Field has content or option is selected
- **Removed when**: Field is empty
- **Use for**: Floating labels, placeholder animations, styling filled states
```css
.form-field-group.is--filled .form-label {
/* Style for filled state */
}
```
#### 2. `is--success`
- **Added when**: Field passes validation
- **Removed when**: Field becomes invalid
- **Use for**: Success icons, green borders, positive feedback
```css
.form-field-group.is--success .form-input {
border-color: green;
}
.form-field-group.is--success .form-field-icon.is--success {
display: block;
}
```
#### 3. `is--error`
- **Added when**: Field fails validation AND user has interacted with it
- **Removed when**: Field becomes valid
- **Use for**: Error icons, red borders, error messages
```css
.form-field-group.is--error .form-input {
border-color: red;
}
.form-field-group.is--error .form-field-icon.is--error {
display: block;
}
```
### State Combinations
| Field State | `is--filled` | `is--success` | `is--error` |
|-------------|--------------|---------------|-------------|
| Empty, untouched | ❌ | ❌ | ❌ |
| Empty, touched | ❌ | ❌ | ✅ |
| Invalid, untouched | ✅ | ❌ | ❌ |
| Invalid, touched | ✅ | ❌ | ✅ |
| Valid | ✅ | ✅ | ❌ |
---
## Validation Triggers
### When Validation Starts
Different field types have different validation triggers:
1. **Text/Textarea Inputs**
- When user types beyond `min` length
- When user meets `max` length
- On blur (field loses focus)
2. **Email/Phone Inputs**
- When valid format is detected
- On blur
3. **Select Dropdowns**
- Immediately on selection change
4. **Radio/Checkbox Groups**
- When minimum selection count is met
- On blur
5. **Form Submit**
- All fields are validated on submit attempt
- First invalid field receives focus
---
## Special Features
### 1. Spam Protection
- Form cannot be submitted within 5 seconds of page load
- Prevents bot submissions
- Shows alert: "Form submitted too quickly. Please try again."
```javascript
// Built-in, no configuration needed
// Timer starts on page load
```
### 2. Enter Key Handling
- Submits form on Enter key press (except in textareas)
- Validates before submission
- Prevents default form submission behavior
### 3. Disabled Options Auto-Detection
- Select options with invalid values are automatically disabled on page load
- Invalid values: `""`, `"disabled"`, `"null"`, `"false"`
### 4. Focus Management
- On submit attempt, first invalid field receives focus automatically
- Improves UX and accessibility
### 5. Phone Input Character Filtering
- **Prevents typing letters** - users cannot type a-z at all
- Allows: numbers, `+`, `-`, `(`, `)`, space
- Auto-cleans pasted content
- Preserves cursor position during cleaning
---
## Form Submission
### Submit Button Structure
```html
<div data-submit="" class="form-submit-btn">
<p class="form-submit-btn-p">Submit</p>
<input type="submit" class="form-submit w-button" value="Submit">
</div>
```
**Required:**
- `data-submit` attribute on wrapper
- Actual `<input type="submit">` inside
### Submission Flow
1. User clicks submit or presses Enter
2. All fields are validated
3. If invalid:
- Error states shown on all invalid fields
- First invalid field receives focus
- Submission prevented
4. If valid:
- Spam check performed (5 second minimum)
- Form submits
---
## Success/Error Messages
### Success Message
```html
<div class="form-notifcation w-form-done">
<div class="form-notification-p">Success! We'll be in touch soon.</div>
</div>
```
### Error Message
```html
<div class="form-notifcation is--error w-form-fail">
<div class="form-notification-p">Something went wrong while submitting.</div>
</div>
```
**Note:** These are handled by Webflow form submission. Custom handling may be needed for AJAX submissions.
---
## Initialization
The validation system auto-initializes on DOM ready:
```javascript
document.addEventListener('DOMContentLoaded', () => {
initAdvancedFormValidation();
});
```
**No manual initialization required** - just ensure proper HTML structure with data attributes.
---
## Attribute Reference
### Field Group Attributes
| Attribute | Required | Purpose |
|-----------|----------|---------|
| `data-form-validate` | Yes | Marks form container |
| `data-validate` | Yes | Marks field group for validation |
| `data-radiocheck-group` | For radio/checkbox | Groups radio/checkbox inputs |
| `data-submit` | Yes | Marks submit button wrapper |
### Input Attributes
| Attribute | Applies To | Purpose |
|-----------|------------|---------|
| `required` | All inputs | Field is required |
| `min` | Text, textarea, radio/checkbox groups | Minimum length or selections |
| `max` | Checkbox groups | Maximum selections |
| `maxlength` | Text, email, textarea | Maximum character length |
| `type` | All inputs | Input type (text, email, tel, etc.) |
### Special Values
| Attribute | Value | Effect |
|-----------|-------|--------|
| `min` | `"0"` | Makes radio/checkbox optional |
| `min` | `"1"` or absent | Makes radio/checkbox required |
| `min` | `"2"` | Requires at least 2 selections |
| `max` | `"2"` | Allows maximum 2 selections |
---
## Common Patterns
### Required Text Field with Min/Max Length
```html
<input type="text" name="username" min="3" maxlength="20" required="">
```
- Required
- Minimum 3 characters
- Maximum 20 characters
### Optional Radio Group
```html
<div data-radiocheck-group="" min="0">
<!-- radio buttons -->
</div>
```
- User can skip this field
### Exact Selection Count
```html
<div data-radiocheck-group="" min="2" max="2">
<!-- checkboxes -->
</div>
```
- Must select exactly 2 options
### International Phone
```html
<input type="tel" name="phone" placeholder="+1 234 567 8900">
```
- Accepts 7-15 digits
- Optional + for country code
- Letters blocked automatically
---
## Best Practices
1. **Always include labels** - Improves accessibility and UX
2. **Use placeholder text** - Show format examples (especially for phone/email)
3. **Mark required fields** - Use `<span class="form-required">*</span>`
4. **Provide hints** - Use `<span class="form-inactive-text">` for instructions
5. **Test keyboard navigation** - Ensure Tab order is logical
6. **Test with screen readers** - Use proper label associations
7. **Set realistic limits** - Phone: 7-15 digits, names: 1-256 chars
8. **Test paste behavior** - Especially for phone fields
9. **Provide clear error states** - Ensure success/error icons are visible
10. **Test on mobile** - Keyboard types should match input types
---
## Troubleshooting
### Validation Not Working
- ✅ Check `data-form-validate` on form container
- ✅ Check `data-validate` on field group
- ✅ Check `data-submit` on submit button wrapper
- ✅ Ensure JavaScript is loaded after DOM
### Radio/Checkbox Not Validating
- ✅ Check `data-radiocheck-group` on container
- ✅ Ensure all inputs share same `name` (radios only)
- ✅ Check `min`/`max` attributes are numbers
### Phone Field Allows Letters
- ✅ Ensure `type="tel"` on input
- ✅ Check JavaScript initialization
- ✅ Test in different browsers (some mobile keyboards differ)
### Submit Always Fails (Spam Alert)
- ✅ Wait 5+ seconds after page load before testing
- ✅ This is intentional spam protection
### Icons Not Showing
- ✅ Check CSS for `.is--success` and `.is--error` classes
- ✅ Ensure icon elements exist in HTML
- ✅ Check z-index and positioning
---
## Browser Compatibility
**Tested and working in:**
- Chrome 90+
- Firefox 88+
- Safari 14+
- Edge 90+
- Mobile Safari (iOS 14+)
- Chrome Mobile (Android 10+)
**Key Dependencies:**
- `e.key` for keyboard events (ES6+)
- `requestAnimationFrame` for performance
- `classList` API for class manipulation
---
## Files
- `validation.html` - Complete form example with validation
- `default.html` - Basic form template
- `osmo.html` - Osmo-specific form implementation
---
## Questions?
For implementation questions or issues, contact your development team.
**Last Updated:** December 2025
Form Validation styles
1<style>
2 /* Field: Error State */
3 [data-validate].is--error input,
4 [data-validate].is--error textarea,
5 [data-validate].is--error select {
6 border-color: var(--form-validation--error-border);
7 }
8
9 /* Field: Error Icon Visibility */
10 [data-validate].is--error .form-field-icon.is--error,
11 [data-validate].is--error .radiocheck-field-icon.is--error {
12 opacity: 1;
13 }
14
15 /* Field: Success */
16 [data-validate].is--success input,
17 [data-validate].is--success textarea,
18 [data-validate].is--success select {
19 border-color: var(--form-validation--success-border);
20 }
21
22 /* Field: Success Icon Visibility */
23 [data-validate].is--success .form-field-icon.is--success,
24 [data-validate].is--success .radiocheck-field-icon.is--success {
25 opacity: 1;
26 }
27
28 /* Field: Custom Radio or Checkbox Focused State */
29 [data-form-validate] .radiocheck-field input:focus-visible~.radiocheck-custom {
30 background-color:var(--form-validation--radio-or-checkbox--focused-background);
31 color: var(--form-validation--radio-or-checkbox--focused-color);
32 }
33
34 /* Field: Custom Radio or Checkbox */
35 [data-form-validate] .radiocheck-field input:focus-visible:checked~.radiocheck-custom,
36 [data-form-validate] .radiocheck-field input:checked~.radiocheck-custom {
37 background-color: var(--form-validation--radio-or-checkbox--checked-background); /* This is the checkbox tick / radio button bullet fill color */
38 color: var(--form-validation--radio-or-checkbox--checked-color) /* This is the checkbox tick / radio button bullet color */
39 }
40
41 [data-form-validate] .radiocheck-field .radiocheck-label.is--small {
42 margin-top: 0.125em;
43 }
44
45 /* Field: Custom Radio or Checkbox Error state */
46 [data-validate].is--error .radiocheck-custom {
47 border-color: var(--form-validation--error-border)
48 }
49
50 /* Field: Checkbox Error state but checked if there must be more then one selected*/
51 [data-validate].is--error input:checked~.radiocheck-custom {
52 border-color: var(--form-validation--error-border)
53 }
54
55 /* Field: Select before option is selected*/
56 [data-form-validate] select:has(option[value=""]:checked) {
57 color: rgba(19, 19, 19, 0.3);
58 }
59</style>Form validation script
1<script>
2 function initAdvancedFormValidation() {
3 const forms = document.querySelectorAll('[data-form-validate]');
4
5 forms.forEach((formContainer) => {
6 const startTime = new Date().getTime();
7
8 const form = formContainer.querySelector('form');
9 if (!form) return;
10
11 const validateFields = form.querySelectorAll('[data-validate]');
12 const dataSubmit = form.querySelector('[data-submit]');
13 if (!dataSubmit) return;
14
15 const realSubmitInput = dataSubmit.querySelector('input[type="submit"]');
16 if (!realSubmitInput) return;
17
18 function isSpam() {
19 const currentTime = new Date().getTime();
20 return currentTime - startTime < 5000;
21 }
22
23 // Disable select options with invalid values on page load
24 validateFields.forEach(function (fieldGroup) {
25 const select = fieldGroup.querySelector('select');
26 if (select) {
27 const options = select.querySelectorAll('option');
28 options.forEach(function (option) {
29 if (
30 option.value === '' ||
31 option.value === 'disabled' ||
32 option.value === 'null' ||
33 option.value === 'false'
34 ) {
35 option.setAttribute('disabled', 'disabled');
36 }
37 });
38 }
39 });
40
41 function validateAndStartLiveValidationForAll() {
42 let allValid = true;
43 let firstInvalidField = null;
44
45 validateFields.forEach(function (fieldGroup) {
46 const input = fieldGroup.querySelector('input, textarea, select');
47 const radioCheckGroup = fieldGroup.querySelector('[data-radiocheck-group]');
48 if (!input && !radioCheckGroup) return;
49
50 if (input) input.__validationStarted = true;
51 if (radioCheckGroup) {
52 radioCheckGroup.__validationStarted = true;
53 const inputs = radioCheckGroup.querySelectorAll('input[type="radio"], input[type="checkbox"]');
54 inputs.forEach(function (input) {
55 input.__validationStarted = true;
56 });
57 }
58
59 updateFieldStatus(fieldGroup);
60
61 if (!isValid(fieldGroup)) {
62 allValid = false;
63 if (!firstInvalidField) {
64 firstInvalidField = input || radioCheckGroup.querySelector('input');
65 }
66 }
67 });
68
69 if (!allValid && firstInvalidField) {
70 firstInvalidField.focus();
71 }
72
73 return allValid;
74 }
75
76 function isValid(fieldGroup) {
77 const radioCheckGroup = fieldGroup.querySelector('[data-radiocheck-group]');
78 if (radioCheckGroup) {
79 const inputs = radioCheckGroup.querySelectorAll('input[type="radio"], input[type="checkbox"]');
80 const checkedInputs = radioCheckGroup.querySelectorAll('input:checked');
81 const minAttr = radioCheckGroup.getAttribute('min');
82 const min = minAttr !== null ? parseInt(minAttr) : 1;
83 const max = parseInt(radioCheckGroup.getAttribute('max')) || inputs.length;
84 const checkedCount = checkedInputs.length;
85
86 if (inputs[0].type === 'radio') {
87 return checkedCount >= min;
88 } else {
89 if (inputs.length === 1) {
90 return inputs[0].checked;
91 } else {
92 return checkedCount >= min && checkedCount <= max;
93 }
94 }
95 } else {
96 const input = fieldGroup.querySelector('input, textarea, select');
97 if (!input) return false;
98
99 let valid = true;
100 const min = parseInt(input.getAttribute('min')) || 0;
101 const max = parseInt(input.getAttribute('max')) || Infinity;
102 const value = input.value.trim();
103 const length = value.length;
104
105 if (input.tagName.toLowerCase() === 'select') {
106 if (
107 value === '' ||
108 value === 'disabled' ||
109 value === 'null' ||
110 value === 'false'
111 ) {
112 valid = false;
113 }
114 } else if (input.type === 'email') {
115 const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
116 valid = emailPattern.test(value);
117 } else if (input.type === 'tel') {
118 // Phone validation - supports both international and local formats
119 // Remove all non-digit characters except leading +
120 const cleanPhone = value.replace(/[^\d\+]/g, '');
121
122 // Pattern: optional +, then 7-15 digits
123 // Local numbers: 7-10 digits (e.g., 5551234, 5551234567)
124 // International: + followed by country code and number (e.g., +1234567890)
125 const phonePattern = /^(\+?\d{7,15})$/;
126
127 valid = phonePattern.test(cleanPhone) && cleanPhone.length >= 7;
128 } else {
129 if (input.hasAttribute('min') && length < min) valid = false;
130 if (input.hasAttribute('max') && length > max) valid = false;
131 }
132
133 return valid;
134 }
135 }
136
137 function updateFieldStatus(fieldGroup) {
138 const radioCheckGroup = fieldGroup.querySelector('[data-radiocheck-group]');
139 if (radioCheckGroup) {
140 const inputs = radioCheckGroup.querySelectorAll('input[type="radio"], input[type="checkbox"]');
141 const checkedInputs = radioCheckGroup.querySelectorAll('input:checked');
142
143 if (checkedInputs.length > 0) {
144 fieldGroup.classList.add('is--filled');
145 } else {
146 fieldGroup.classList.remove('is--filled');
147 }
148
149 const valid = isValid(fieldGroup);
150
151 if (valid) {
152 fieldGroup.classList.add('is--success');
153 fieldGroup.classList.remove('is--error');
154 } else {
155 fieldGroup.classList.remove('is--success');
156 const anyInputValidationStarted = Array.from(inputs).some(input => input.__validationStarted);
157 if (anyInputValidationStarted) {
158 fieldGroup.classList.add('is--error');
159 } else {
160 fieldGroup.classList.remove('is--error');
161 }
162 }
163 } else {
164 const input = fieldGroup.querySelector('input, textarea, select');
165 if (!input) return;
166
167 const value = input.value.trim();
168
169 if (value) {
170 fieldGroup.classList.add('is--filled');
171 } else {
172 fieldGroup.classList.remove('is--filled');
173 }
174
175 const valid = isValid(fieldGroup);
176
177 if (valid) {
178 fieldGroup.classList.add('is--success');
179 fieldGroup.classList.remove('is--error');
180 } else {
181 fieldGroup.classList.remove('is--success');
182 if (input.__validationStarted) {
183 fieldGroup.classList.add('is--error');
184 } else {
185 fieldGroup.classList.remove('is--error');
186 }
187 }
188 }
189 }
190
191 validateFields.forEach(function (fieldGroup) {
192 const input = fieldGroup.querySelector('input, textarea, select');
193 const radioCheckGroup = fieldGroup.querySelector('[data-radiocheck-group]');
194
195 if (radioCheckGroup) {
196 const inputs = radioCheckGroup.querySelectorAll('input[type="radio"], input[type="checkbox"]');
197 inputs.forEach(function (input) {
198 input.__validationStarted = false;
199
200 input.addEventListener('change', function () {
201 requestAnimationFrame(function () {
202 if (!input.__validationStarted) {
203 const checkedCount = radioCheckGroup.querySelectorAll('input:checked').length;
204 const minAttr = radioCheckGroup.getAttribute('min');
205 const min = minAttr !== null ? parseInt(minAttr) : 1;
206
207 // Start validation when min requirement is met, or immediately if min=0 (optional)
208 if (checkedCount >= min || min === 0) {
209 input.__validationStarted = true;
210 }
211 }
212
213 if (input.__validationStarted) {
214 updateFieldStatus(fieldGroup);
215 }
216 });
217 });
218
219 input.addEventListener('blur', function () {
220 input.__validationStarted = true;
221 updateFieldStatus(fieldGroup);
222 });
223 });
224 } else if (input) {
225 input.__validationStarted = false;
226
227 // Prevent typing letters in phone inputs
228 if (input.type === 'tel') {
229 input.addEventListener('keydown', function (e) {
230 // Allow control keys
231 if (e.key === 'Backspace' ||
232 e.key === 'Delete' ||
233 e.key === 'Tab' ||
234 e.key === 'Escape' ||
235 e.key === 'Enter' ||
236 e.key === 'ArrowLeft' ||
237 e.key === 'ArrowRight' ||
238 e.key === 'ArrowUp' ||
239 e.key === 'ArrowDown' ||
240 e.key === 'Home' ||
241 e.key === 'End') {
242 return;
243 }
244
245 // Allow Ctrl/Cmd shortcuts (Copy, Paste, Cut, Select All)
246 if ((e.ctrlKey || e.metaKey) && (e.key === 'a' || e.key === 'c' || e.key === 'v' || e.key === 'x' || e.key === 'A' || e.key === 'C' || e.key === 'V' || e.key === 'X')) {
247 return;
248 }
249
250 // Allow numbers, +, -, (, ), and space
251 const allowedChars = /[0-9+\-() ]/;
252 if (!allowedChars.test(e.key)) {
253 e.preventDefault();
254 }
255 });
256
257 // Additional safeguard: remove non-allowed characters on input (for paste)
258 input.addEventListener('input', function (e) {
259 const cursorPos = input.selectionStart;
260 const oldValue = input.value;
261 const newValue = oldValue.replace(/[^0-9+\-() ]/g, '');
262 if (oldValue !== newValue) {
263 input.value = newValue;
264 // Restore cursor position
265 input.setSelectionRange(cursorPos - 1, cursorPos - 1);
266 }
267 });
268 }
269
270 if (input.tagName.toLowerCase() === 'select') {
271 input.addEventListener('change', function () {
272 input.__validationStarted = true;
273 updateFieldStatus(fieldGroup);
274 });
275 } else {
276 // Validation input listener (separate from tel character filter)
277 input.addEventListener('input', function () {
278 const value = input.value.trim();
279 const length = value.length;
280 const min = parseInt(input.getAttribute('min')) || 0;
281 const max = parseInt(input.getAttribute('max')) || Infinity;
282
283 if (!input.__validationStarted) {
284 if (input.type === 'email') {
285 if (isValid(fieldGroup)) input.__validationStarted = true;
286 } else if (input.type === 'tel') {
287 if (isValid(fieldGroup)) input.__validationStarted = true;
288 } else {
289 if (
290 (input.hasAttribute('min') && length >= min) ||
291 (input.hasAttribute('max') && length <= max)
292 ) {
293 input.__validationStarted = true;
294 }
295 }
296 }
297
298 if (input.__validationStarted) {
299 updateFieldStatus(fieldGroup);
300 }
301 }, { capture: false });
302
303 input.addEventListener('blur', function () {
304 input.__validationStarted = true;
305 updateFieldStatus(fieldGroup);
306 });
307 }
308 }
309 });
310
311 dataSubmit.addEventListener('click', function () {
312 if (validateAndStartLiveValidationForAll()) {
313 if (isSpam()) {
314 alert('Form submitted too quickly. Please try again.');
315 return;
316 }
317 realSubmitInput.click();
318 }
319 });
320
321 form.addEventListener('keydown', function (event) {
322 if (event.key === 'Enter' && event.target.tagName !== 'TEXTAREA') {
323 event.preventDefault();
324 if (validateAndStartLiveValidationForAll()) {
325 if (isSpam()) {
326 alert('Form submitted too quickly. Please try again.');
327 return;
328 }
329 realSubmitInput.click();
330 }
331 }
332 });
333 });
334 }
335
336 // Initialize Advanced Form Validation
337 document.addEventListener('DOMContentLoaded', () => {
338 initAdvancedFormValidation();
339 });
340</script>Modal
<style>
[data-whatsapp-modal-qr-canvas]:has(svg) {
background-color: transparent;
}
[data-whatsapp-modal-qr-canvas] svg rect {
fill: transparent;
}
.whatsapp-modal__card {
transition: all 0.6s cubic-bezier(0.625, 0.05, 0, 1);
transform: translateY(25%) rotate(0.001deg);
opacity: 0;
visibility: hidden;
}
[data-whatsapp-modal-status="active"] .whatsapp-modal__card {
transform: translateY(0%) rotate(0.001deg);
opacity: 1;
visibility: visible;
}
.whatsapp-modal__dark {
transition: all 0.6s cubic-bezier(0.625, 0.05, 0, 1);
opacity: 0;
visibility: hidden;
}
[data-whatsapp-modal-status="active"] .whatsapp-modal__dark {
opacity: 1;
visibility: visible;
}
/* Hide link on non-touch devices */
[data-whatsapp-modal-btn] [data-whatsapp-modal-link] {
display: none;
}
/* Hide modal on touch devices, and open link directly */
@media (hover: none) and (pointer: coarse) {
[data-whatsapp-modal] {
display: none;
}
/* Hide toggle button on touch devices */
[data-whatsapp-modal-trigger] [data-whatsapp-modal-toggle] {
display: none;
}
/* Show link on touch devices */
[data-whatsapp-modal-trigger] [data-whatsapp-modal-link] {
display: block;
}
}
</style><script>
function initWhatsAppModal() {
const modal = document.querySelector('[data-whatsapp-modal]');
// QR-code generation
const url = (modal.getAttribute('data-whatsapp-modal') || '').trim();
if (!url) return;
// Generate an SVG QR via kjua
const svg = kjua({
text: url,
render: 'svg',
crisp: true,
minVersion: 1,
ecLevel: 'M',
size: 540,
fill: '#000000',
back: '#FFFFFF',
rounded: 0
});
// Let CSS control sizing
svg.removeAttribute('width');
svg.removeAttribute('height');
svg.removeAttribute('style');
// Insert into canvas (or multiple if needed)
modal.querySelectorAll('[data-whatsapp-modal-qr-canvas]').forEach((placeholder, i) => {
const node = i === 0 ? svg : svg.cloneNode(true);
placeholder.appendChild(node);
});
// Add the link to all elements with [data-whatsapp-modal-link] attribute
document.querySelectorAll('[data-whatsapp-modal-link]').forEach(linkEl => {
linkEl.setAttribute('href', url);
linkEl.setAttribute('target', '_blank');
});
// Toggle open/close the modal
document.querySelectorAll('[data-whatsapp-modal-toggle]').forEach(btn => {
btn.addEventListener('click', () => {
if (!modal) return;
const isActive = modal.getAttribute('data-whatsapp-modal-status') === 'active';
modal.setAttribute('data-whatsapp-modal-status', isActive ? 'not-active' : 'active');
});
});
// Close on ESC key
document.addEventListener('keydown', event => {
if (event.key === 'Escape' || event.keyCode === 27) {
if (modal) {
modal.setAttribute('data-whatsapp-modal-status', 'not-active');
}
}
});
}
// Initialize WhatsApp Modal (Generate QR Code)
document.addEventListener('DOMContentLoaded', function() {
initWhatsAppModal();
});
</script>