Forms
Comprehensive form controls and layouts with validation styling - CSS-only components
Overview
Forms provide a comprehensive set of form controls and layouts. Includes inputs, textareas, selects, checkboxes, radios, switches, and validation styling. All components work with native HTML form elements enhanced with Domma's theme-aware styling.
Key Features
- Native HTML - Works with standard form elements
- Validation States - Success, error, and warning styling
- Input Groups - Addons, icons, and button combinations
- Layouts - Vertical, horizontal, and inline forms
Basic Usage
<div class="form-group">
<label>Email</label>
<input type="email" class="form-input" placeholder="you@example.com">
</div>
Getting Started
Add styled form controls to your page in two simple steps
Step 1: Include Domma CSS
Add Domma CSS to your page:
<!-- CSS -->
<link rel="stylesheet" href="dist/domma.css">
<link rel="stylesheet" href="dist/elements.css">
Step 2: Add Form HTML
Use form classes with standard HTML elements:
<form>
<!-- Text input -->
<div class="form-group">
<label>Email</label>
<input type="email" class="form-input" placeholder="you@example.com">
</div>
<!-- Password input -->
<div class="form-group">
<label>Password</label>
<input type="password" class="form-input">
</div>
<!-- Checkbox -->
<div class="form-group">
<label class="checkbox">
<input type="checkbox">
<span>Remember me</span>
</label>
</div>
<!-- Submit button -->
<button type="submit" class="btn btn-primary">Sign In</button>
</form>
Note: Domma forms are CSS-only and work with native HTML form elements (no JavaScript required). Simply add the appropriate classes to your HTML. For advanced form generation and validation, see the
Domma.formsmodule which provides blueprint-driven form building.
Quick Start
Simple Form
<form class="forms-max-w-400-center">
<div class="form-group">
<label>Email</label>
<input type="email" class="form-input" placeholder="you@example.com">
</div>
<div class="form-group">
<label>Password</label>
<input type="password" class="form-input" placeholder="Enter password">
</div>
<button type="submit" class="btn btn-primary w-full">Sign In</button>
</form><form>
<div class="form-group">
<label>Email</label>
<input type="email" class="form-input" placeholder="you@example.com">
</div>
<div class="form-group">
<label>Password</label>
<input type="password" class="form-input">
</div>
<button type="submit" class="btn btn-primary">Sign In</button>
</form>
Tutorial
Build a registration form with validation.
Fill out the form and submit
<form id="tutorial-form" class="forms-max-w-500-center">
<div class="form-group">
<label>Full Name</label>
<input type="text" id="name" class="form-input" placeholder="John Doe" required="">
<small class="form-text">Enter your first and last name</small>
</div>
<div class="form-group">
<label>Email Address</label>
<input type="email" id="email" class="form-input" placeholder="john@example.com" required="">
</div>
<div class="form-group">
<label>Password</label>
<input type="password" id="password" class="form-input" placeholder="Min 8 characters" required="">
<small class="form-text">Must be at least 8 characters</small>
</div>
<div class="form-group">
<label>Account Type</label>
<select id="account-type" class="form-select">
<option value="">Select type...</option>
<option value="personal">Personal</option>
<option value="business">Business</option>
<option value="enterprise">Enterprise</option>
</select>
</div>
<div class="form-group">
<label class="form-checkbox">
<input type="checkbox" id="terms" required="">
<span>I agree to the terms and conditions</span>
</label>
</div>
<button type="submit" class="btn btn-primary w-full">Create Account</button>
</form>
<div id="tutorial-result" class="mt-4 p-3 bg-surface rounded text-center">
<p class="text-muted text-sm">Fill out the form and submit</p>
</div>/**
* Tutorial - from public/showcase/elements/forms.
*/
export function mount(root) {
$(root).find('#tutorial-form').on('submit', (e) => {
e.preventDefault();
// Clear previous validation
$('.form-group').removeClass('has-error has-success');
let isValid = true;
// Validate name
const $name = $(root).find('#name');
if ($name.val().length < 2) {
$name.closest('.form-group').addClass('has-error');
isValid = false;
} else {
$name.closest('.form-group').addClass('has-success');
}
// Validate email
const $email = $(root).find('#email');
if (!$email.val().includes('@')) {
$email.closest('.form-group').addClass('has-error');
isValid = false;
} else {
$email.closest('.form-group').addClass('has-success');
}
// Validate password
const $password = $(root).find('#password');
if ($password.val().length < 8) {
$password.closest('.form-group').addClass('has-error');
isValid = false;
} else {
$password.closest('.form-group').addClass('has-success');
}
// Validate account type
const $accountType = $(root).find('#account-type');
if (!$accountType.val()) {
$accountType.closest('.form-group').addClass('has-error');
isValid = false;
} else {
$accountType.closest('.form-group').addClass('has-success');
}
// Validate terms
const $terms = $(root).find('#terms');
if (!$terms.prop('checked')) {
$terms.closest('.form-group').addClass('has-error');
isValid = false;
} else {
$terms.closest('.form-group').addClass('has-success');
}
// Show result
if (isValid) {
$(root).find('#tutorial-result').html('<p class="text-success">✓ Form submitted successfully!</p>');
} else {
$(root).find('#tutorial-result').html('<p class="text-danger">✗ Please fix the errors above</p>');
}
});
}$('#form').on('submit', (e) => {
e.preventDefault();
// Clear previous validation
$('.form-group').forEach(group => {
group.classList.remove('has-error', 'has-success');
});
// Validate name
const name = $('#name')[0];
if (name.value.length < 2) {
name.closest('.form-group').classList.add('has-error');
return;
}
name.closest('.form-group').classList.add('has-success');
// Validate email
const email = $('#email')[0];
if (!email.value.includes('@')) {
email.closest('.form-group').classList.add('has-error');
return;
}
email.closest('.form-group').classList.add('has-success');
// Form is valid
console.log('Form submitted successfully!');
});
Examples
Input Types
<div class="forms-max-w-500-center">
<div class="form-group">
<label>Text Input</label>
<input type="text" class="form-input" placeholder="Enter text">
</div>
<div class="form-group">
<label>Email Input</label>
<input type="email" class="form-input" placeholder="email@example.com">
</div>
<div class="form-group">
<label>Number Input</label>
<input type="number" class="form-input" placeholder="123">
</div>
<div class="form-group">
<label>Date Input</label>
<input type="date" class="form-input">
</div>
<div class="form-group">
<label>Textarea</label>
<textarea class="form-textarea" rows="3" placeholder="Enter long text..."></textarea>
</div>
</div>Select Menus
<div class="forms-max-w-500-center">
<div class="form-group">
<label>Single Select</label>
<select class="form-select">
<option>Choose option...</option>
<option>Option 1</option>
<option>Option 2</option>
<option>Option 3</option>
</select>
</div>
<div class="form-group">
<label>Multiple Select</label>
<select class="form-select" multiple="" size="4">
<option>Option 1</option>
<option>Option 2</option>
<option>Option 3</option>
<option>Option 4</option>
</select>
</div>
</div>Checkboxes and Radios
<div class="forms-max-w-500-center">
<div class="form-group">
<label>Checkboxes</label>
<label class="form-checkbox">
<input type="checkbox">
<span>Option 1</span>
</label>
<label class="form-checkbox">
<input type="checkbox" checked="">
<span>Option 2 (checked)</span>
</label>
<label class="form-checkbox">
<input type="checkbox" disabled="">
<span>Option 3 (disabled)</span>
</label>
</div>
<div class="form-group">
<label>Radio Buttons</label>
<label class="form-radio">
<input type="radio" name="radio-demo" checked="">
<span>Choice 1</span>
</label>
<label class="form-radio">
<input type="radio" name="radio-demo">
<span>Choice 2</span>
</label>
<label class="form-radio">
<input type="radio" name="radio-demo" disabled="">
<span>Choice 3 (disabled)</span>
</label>
</div>
</div>Input Sizes
<div class="forms-max-w-500-center">
<div class="form-group">
<label>Small</label>
<input type="text" class="form-input form-input-sm" placeholder="Small input">
</div>
<div class="form-group">
<label>Default</label>
<input type="text" class="form-input" placeholder="Default input">
</div>
<div class="form-group">
<label>Large</label>
<input type="text" class="form-input form-input-lg" placeholder="Large input">
</div>
</div>Validation States
<div class="forms-max-w-500-center">
<div class="form-group has-success">
<label>Success</label>
<input type="text" class="form-input" value="Valid input">
<small class="form-text text-success">Looks good!</small>
</div>
<div class="form-group has-error">
<label>Error</label>
<input type="text" class="form-input" value="Invalid input">
<small class="form-text text-danger">This field is required</small>
</div>
<div class="form-group has-warning">
<label>Warning</label>
<input type="text" class="form-input" value="Unusual input">
<small class="form-text text-warning">Are you sure?</small>
</div>
</div>Input Groups
<div class="forms-max-w-500-center">
<div class="form-group">
<label>With Prefix</label>
<div class="input-group">
<span class="input-group-text">@</span>
<input type="text" class="form-input" placeholder="Username">
</div>
</div>
<div class="form-group">
<label>With Suffix</label>
<div class="input-group">
<input type="text" class="form-input" placeholder="Amount">
<span class="input-group-text">.00</span>
</div>
</div>
<div class="form-group">
<label>With Button</label>
<div class="input-group">
<input type="text" class="form-input" placeholder="Search...">
<button type="button" class="btn btn-primary">Go</button>
</div>
</div>
</div>Inline Forms
<form class="form-inline">
<input type="email" class="form-input" placeholder="Email">
<input type="password" class="form-input" placeholder="Password">
<button type="submit" class="btn btn-primary">Sign In</button>
</form>Best Practices
Accessibility
- Labels - Always provide `` elements for inputs
- ARIA Attributes - Use
aria-required,aria-invalidfor validation - Error Messages - Link errors to inputs using
aria-describedby - Keyboard Navigation - Ensure all form controls are keyboard accessible
Validation
- Client-Side First - Validate on submit before sending to server
- Clear Feedback - Use validation states and helper text
- Real-Time Validation - Consider validating on blur for better UX
- Server-Side Required - Always validate on backend for security
UX Guidelines
- Placeholder Text - Use for examples, not instructions
- Help Text - Provide context with
.form-text - Logical Grouping - Group related fields together
- Required Fields - Mark clearly with asterisk or label
- Button States - Disable submit while processing
CSS Classes
| Class | Description |
|---|---|
.form-group |
Form field container |
.form-input |
Text input styling |
.form-textarea |
Textarea styling |
.form-select |
Select dropdown styling |
.form-checkbox |
Checkbox wrapper |
.form-radio |
Radio button wrapper |
.form-text |
Helper text |
.form-input-sm |
Small input size |
.form-input-lg |
Large input size |
.has-success |
Success validation state |
.has-error |
Error validation state |
.has-warning |
Warning validation state |
.input-group |
Input group container |
.input-group-text |
Input group prefix/suffix |
.form-inline |
Inline form layout |
Hybrid CSS + JavaScript
Forms work perfectly with pure CSS styling, but you can enhance them with JavaScript for validation, auto-save, dynamic fields, and more using Domma's declarative configuration.
Form Validation with $.setup()
$.setup({
'#registration-form': {
initial: {
data: { touched: {}, errors: {} }
},
events: {
submit: (e, $form) => {
e.preventDefault();
// Collect form data
const formData = {
username: $form.find('[name="username"]').val(),
email: $form.find('[name="email"]').val(),
password: $form.find('[name="password"]').val()
};
// Validate with utils
const errors = {};
if (_.isEmpty(_.trim(formData.username))) {
errors.username = 'Username required';
}
if (!formData.email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
errors.email = 'Invalid email';
}
if (formData.password.length < 8) {
errors.password = 'Password must be 8+ characters';
}
$form.data('errors', errors);
if (_.isEmpty(errors)) {
// Submit
Domma.http.post('/api/register', formData)
.then(() => {
Domma.elements.toast.success('Registered successfully!');
window.location = '/dashboard';
})
.catch(err => {
Domma.elements.toast.error(err.message);
});
} else {
// Show errors
_.forEach(errors, (msg, field) => {
$form.find(`[name="${field}"]`)
.closest('.form-group')
.addClass('has-error')
.find('.error-message').text(msg);
});
}
},
'blur .form-input': _.debounce((e, $form) => {
const $input = $(e.target);
const field = $input.attr('name');
const value = $input.val();
const $group = $input.closest('.form-group');
// Mark as touched
const touched = $form.data('touched') || {};
touched[field] = true;
$form.data('touched', touched);
// Validate
let isValid = true;
if (field === 'email') {
isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
} else if (field === 'password') {
isValid = value.length >= 8;
} else {
isValid = !_.isEmpty(_.trim(value));
}
$group.toggleClass('has-error', !isValid);
$group.toggleClass('has-success', isValid);
}, 300)
}
}
});
Auto-Save Draft
$.setup({
'#article-form': {
initial: {
data: { saveTimer: null, lastSaved: null }
},
events: {
'input .form-input, change select': _.debounce((e, $form) => {
// Collect current state
const data = {
title: $form.find('[name="title"]').val(),
content: $form.find('[name="content"]').val(),
category: $form.find('[name="category"]').val()
};
// Save draft
Domma.http.post('/api/drafts/auto-save', data)
.then(() => {
$form.data('lastSaved', new Date());
$('#save-status').text('Draft saved ' + D().format('HH:mm:ss'));
});
}, 2000)
}
}
});
Dynamic Form Fields
$.setup({
'#contact-form': {
events: {
'click #add-phone': (e, $form) => {
const phoneCount = $form.find('.phone-input').length;
const html = `
<div class="form-group">
<input type="tel" name="phone_${phoneCount}"
class="form-input phone-input"
placeholder="Phone ${phoneCount + 1}">
</div>
`;
$form.find('#phone-inputs').append(html);
},
'change [name="contact-method"]': (e, $form) => {
const method = $(e.target).val();
// Show/hide relevant fields
$form.find('.email-field').toggle(method === 'email');
$form.find('.phone-field').toggle(method === 'phone');
$form.find('.address-field').toggle(method === 'mail');
}
}
}
});
Multi-Step Form
$.setup({
'#wizard-form': {
initial: {
data: { currentStep: 1, totalSteps: 3, formData: {} }
},
events: {
'click .next-step': (e, $form) => {
const data = $form.data();
if (data.currentStep < data.totalSteps) {
// Collect current step data
$form.find(`.step-${data.currentStep} .form-input`).each(function() {
const $input = $(this);
data.formData[$input.attr('name')] = $input.val();
});
// Move to next step
data.currentStep++;
$form.data(data);
// Update UI
$form.find('.step').hide();
$form.find(`.step-${data.currentStep}`).fadeIn();
$('#step-indicator').text(`Step ${data.currentStep} of ${data.totalSteps}`);
}
},
'click .prev-step': (e, $form) => {
const data = $form.data();
if (data.currentStep > 1) {
data.currentStep--;
$form.data(data);
$form.find('.step').hide();
$form.find(`.step-${data.currentStep}`).fadeIn();
$('#step-indicator').text(`Step ${data.currentStep} of ${data.totalSteps}`);
}
}
}
}
});
Benefits of Hybrid Approach
- Progressive Enhancement - Forms work without JS, enhanced with JS
- Better UX - Real-time validation, auto-save, dynamic fields
- Maintainability - Centralized logic with $.setup()
- Accessibility - CSS provides base styling, JS adds interactivity
CSS-Only vs CSS+JS Hybrid
CSS-Only (Default)
<form action="/submit" method="POST">
<div class="form-group">
<label>Email</label>
<input type="email"
class="form-input"
name="email"
required>
</div>
<button type="submit"
class="btn btn-primary">
Submit
</button>
</form>
<!-- Works with native validation -->
CSS+JS Hybrid (Enhanced)
<form id="enhanced-form">
<div class="form-group">
<label>Email</label>
<input type="email"
class="form-input"
name="email">
<span class="error-message"></span>
</div>
<button type="submit"
class="btn btn-primary">
Submit
</button>
</form>
<script>
$.setup({
'#enhanced-form': {
events: {
submit: validateAndSubmit,
'blur .form-input': validateField
}
}
});
</script>
CSS Customisation
Override these CSS variables to customise Form elements appearance and match your design system.
| Variable | Default | Controls |
|---|---|---|
--dm-input-bg |
var(--dm-surface) |
Input background colour |
--dm-input-border |
var(--dm-border-dark) |
Input border colour |
--dm-input-text |
var(--dm-text) |
Input text colour |
--dm-input-focus-border |
var(--dm-primary) |
Focus state border colour |
--dm-focus-ring |
rgba(59, 130, 246, 0.25) |
Focus ring (box-shadow) |
Example Override
:root {
/* Custom focus colour */
--dm-input-focus-border: #16a34a; /* Green focus */
--dm-focus-ring: rgba(22, 163, 74, 0.25);
/* Subtle borders */
--dm-input-border: #e5e7eb;
}
Full CSS Customisation Cheat-Sheet →
Related Elements
<div class="grid grid-cols-1 gap-4 grid-auto-fit-250"><a href="/showcase/elements/button-group" class="card card-hover">
<div class="card-body">
<h3 class="card-title">Button Group</h3>
<p class="card-text">For radio-style button selections</p>
</div>
</a>
<a href="/showcase/elements/modal" class="card card-hover">
<div class="card-body">
<h3 class="card-title">Modal</h3>
<p class="card-text">Display forms in modal dialogs</p>
</div>
</a>
<a href="/showcase/elements/tooltip" class="card card-hover">
<div class="card-body">
<h3 class="card-title">Tooltip</h3>
<p class="card-text">Add helpful hints to form fields</p>
</div>
</a>
</div>