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.forms module which provides blueprint-driven form building.

Quick Start

Simple Form

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.

Tutorial
Enter your first and last name
Must be at least 8 characters

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

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

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

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

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

Validation States
Looks good!
This field is required
Are you sure?
<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

Input Groups
@
.00
<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

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-invalid for 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

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>