Forms
Accessible, consistent form controls built with modern CSS. Every control meets WCAG 2.2 touch targets (44×44px minimum) and works across all themes.
Text Input
Use .velin-input on any <input> element. Supports all HTML5 types including text, email, password, number, search, and url.
<input type="text" class="velin-input" placeholder="Default text input"> <input type="email" class="velin-input" placeholder="Email input"> <input type="password" class="velin-input" placeholder="Password input"> <input type="text" class="velin-input" placeholder="Disabled input" disabled>
Select
Apply .velin-select for a styled native dropdown with a custom chevron indicator.
<select class="velin-select"> <option value="" disabled selected>Choose an option…</option> <option value="1">Option One</option> <option value="2">Option Two</option> <option value="3">Option Three</option> </select> <select class="velin-select" disabled> <option>Disabled select</option> </select>
Textarea
Use .velin-textarea for multi-line text fields. The textarea is vertically resizable by default with a minimum height of 6rem.
<textarea class="velin-textarea" placeholder="Write something…"></textarea> <textarea class="velin-textarea" placeholder="Disabled textarea" disabled></textarea>
Checkbox
Wrap a checkbox in .velin-check with __input and __label elements. The label is the clickable wrapper via the <label> element.
<label class="velin-check"> <input type="checkbox" class="velin-check__input"> <span class="velin-check__label">Accept terms and conditions</span> </label> <label class="velin-check"> <input type="checkbox" class="velin-check__input" checked> <span class="velin-check__label">Checked by default</span> </label> <label class="velin-check"> <input type="checkbox" class="velin-check__input" disabled> <span class="velin-check__label">Disabled checkbox</span> </label>
Radio
Radio groups use .velin-radio with the same BEM structure as checkboxes. Group them with a shared name attribute.
<fieldset style="border:none;padding:0;margin:0">
<legend class="velin-label">Select a plan</legend>
<label class="velin-radio">
<input type="radio" name="plan" class="velin-radio__input" value="free" checked>
<span class="velin-radio__label">Free</span>
</label>
<label class="velin-radio">
<input type="radio" name="plan" class="velin-radio__input" value="pro">
<span class="velin-radio__label">Pro</span>
</label>
<label class="velin-radio">
<input type="radio" name="plan" class="velin-radio__input" value="enterprise" disabled>
<span class="velin-radio__label">Enterprise (disabled)</span>
</label>
</fieldset>
Switch / Toggle
The .velin-switch uses a hidden checkbox with a custom track and thumb. Available in three sizes: --sm, default, and --lg.
Sizes
<!-- Default switch --> <label class="velin-switch"> <input type="checkbox" class="velin-switch__input" role="switch"> <span class="velin-switch__track"></span> <span class="velin-switch__label">Notifications</span> </label> <!-- Checked --> <label class="velin-switch"> <input type="checkbox" class="velin-switch__input" role="switch" checked> <span class="velin-switch__track"></span> <span class="velin-switch__label">Dark mode (on)</span> </label> <!-- Disabled --> <label class="velin-switch"> <input type="checkbox" class="velin-switch__input" role="switch" disabled> <span class="velin-switch__track"></span> <span class="velin-switch__label">Disabled</span> </label> <!-- Sizes --> <label class="velin-switch velin-switch--sm">…</label> <label class="velin-switch">…</label> <label class="velin-switch velin-switch--lg">…</label>
Range
Apply .velin-range to a range input for a custom-styled slider with accessible focus indicators.
<div class="velin-field"> <label class="velin-label" for="volume">Volume</label> <input type="range" id="volume" class="velin-range" min="0" max="100" value="60"> </div> <!-- Disabled --> <input type="range" class="velin-range" min="0" max="100" value="40" disabled>
File Upload
The .velin-file creates a drag-and-drop styled upload zone. Use __input, __icon, __label, and __hint for the inner elements. A compact variant is also available.
Compact variant
<!-- Full dropzone -->
<div class="velin-file">
<input type="file" class="velin-file__input" aria-label="Upload file">
<svg class="velin-file__icon" width="32" height="32" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/>
<line x1="12" y1="3" x2="12" y2="15"/>
</svg>
<span class="velin-file__label">Drop files here or click to upload</span>
<span class="velin-file__hint">PNG, JPG, or PDF up to 10MB</span>
</div>
<!-- Compact variant -->
<div class="velin-file velin-file--compact">
<input type="file" class="velin-file__input" aria-label="Upload file">
<svg class="velin-file__icon" …>…</svg>
<span class="velin-file__label">Choose file</span>
<span class="velin-file__hint">Max 10MB</span>
</div>
Labels
Use .velin-label on <label> elements. Add .velin-label--required to automatically append a red asterisk.
<label class="velin-label" for="username">Username</label> <input type="text" id="username" class="velin-input"> <label class="velin-label velin-label--required" for="email">Email</label> <input type="email" id="email" class="velin-input" required>
Help Text
Add .velin-field-help below an input to provide contextual guidance. Link it with aria-describedby for accessibility.
Must be at least 8 characters with one number.
<div class="velin-field">
<label class="velin-label" for="password">Password</label>
<input type="password" id="password" class="velin-input"
aria-describedby="password-hint">
<p class="velin-field-help" id="password-hint">
Must be at least 8 characters with one number.
</p>
</div>
Error States
Add .velin-input--error (or .velin-select--error, .velin-textarea--error) to mark invalid fields. Pair with .velin-field-error for the error message and aria-invalid="true" for screen readers.
Please enter a valid email address.
Please select a country.
Bio is required.
Success state
<!-- Error state -->
<div class="velin-field">
<label class="velin-label velin-label--required" for="email">Email</label>
<input type="email" id="email"
class="velin-input velin-input--error"
aria-invalid="true"
aria-describedby="email-error">
<p class="velin-field-error" id="email-error">
Please enter a valid email address.
</p>
</div>
<!-- Success state -->
<input class="velin-input velin-input--success" value="johndoe">
<!-- Also available for select & textarea -->
<select class="velin-select velin-select--error">…</select>
<textarea class="velin-textarea velin-textarea--error">…</textarea>
Validation CSS (:user-invalid)
Add .velin-form--validated on the <form>. Browsers that support :user-invalid only show error borders after the user has interacted, avoiding an all-red form on first paint. Pair with aria-invalid and aria-describedby for screen readers.
Use .velin-field-hint (alias of help styling) or .velin-field-help, .velin-field-error, and optionally .velin-field-error--icon for a leading accent. Dynamic messages: role="status" + aria-live="polite" or role="alert" for urgent errors.
<form class="velin-form--validated" novalidate> <input class="velin-input" required aria-describedby="hint-id"> <p class="velin-field-hint" id="hint-id">…</p> <div role="status" aria-live="polite" id="live"></div> </form>
Field Wrapper
Use .velin-field to group a label, input, and help/error text. It provides consistent vertical spacing between form rows.
As it appears on your ID.
This email is already in use.
Brief description (max 200 characters).
<div class="velin-field">
<label class="velin-label velin-label--required" for="name">Full name</label>
<input type="text" id="name" class="velin-input" required>
<p class="velin-field-help">As it appears on your ID.</p>
</div>
<div class="velin-field">
<label class="velin-label velin-label--required" for="email">Email</label>
<input type="email" id="email"
class="velin-input velin-input--error"
aria-invalid="true"
aria-describedby="email-err"
required>
<p class="velin-field-error" id="email-err">
This email is already in use.
</p>
</div>
Complete Contact Form
A full example combining all form controls with proper labels, accessibility attributes, and validation styling.
<form novalidate>
<div class="velin-field">
<label class="velin-label velin-label--required" for="name">Full Name</label>
<input type="text" id="name" class="velin-input" required
aria-describedby="name-help">
<p class="velin-field-help" id="name-help">First and last name.</p>
</div>
<div class="velin-field">
<label class="velin-label velin-label--required" for="email">Email</label>
<input type="email" id="email"
class="velin-input velin-input--error"
aria-invalid="true"
aria-describedby="email-err" required>
<p class="velin-field-error" id="email-err">
Please enter a valid email address.
</p>
</div>
<div class="velin-field">
<label class="velin-label" for="phone">Phone</label>
<input type="tel" id="phone" class="velin-input">
</div>
<div class="velin-field">
<label class="velin-label velin-label--required" for="subject">Subject</label>
<select id="subject" class="velin-select" required>
<option value="" disabled selected>Choose a topic…</option>
<option value="general">General Inquiry</option>
<option value="support">Technical Support</option>
</select>
</div>
<div class="velin-field">
<label class="velin-label velin-label--required" for="message">Message</label>
<textarea id="message" class="velin-textarea" required
aria-describedby="msg-help"></textarea>
<p class="velin-field-help" id="msg-help">Minimum 20 characters.</p>
</div>
<div class="velin-field">
<label class="velin-label" for="budget">Budget Range</label>
<input type="range" id="budget" class="velin-range"
min="0" max="10000" step="500" value="2500">
</div>
<div class="velin-field">
<label class="velin-label">Attachment</label>
<div class="velin-file velin-file--compact">
<input type="file" class="velin-file__input" aria-label="Upload">
<svg class="velin-file__icon" …>…</svg>
<span class="velin-file__label">Attach a file</span>
<span class="velin-file__hint">Max 10MB</span>
</div>
</div>
<fieldset class="velin-field">
<legend class="velin-label">Preferred contact method</legend>
<label class="velin-radio">
<input type="radio" name="contact" class="velin-radio__input"
value="email" checked>
<span class="velin-radio__label">Email</span>
</label>
<label class="velin-radio">
<input type="radio" name="contact" class="velin-radio__input"
value="phone">
<span class="velin-radio__label">Phone</span>
</label>
</fieldset>
<div class="velin-field">
<label class="velin-switch">
<input type="checkbox" class="velin-switch__input" role="switch">
<span class="velin-switch__track"></span>
<span class="velin-switch__label">Subscribe to newsletter</span>
</label>
</div>
<div class="velin-field">
<label class="velin-check">
<input type="checkbox" class="velin-check__input" required>
<span class="velin-check__label">I agree to the terms</span>
</label>
</div>
<button type="submit" class="velin-btn velin-btn--primary">
Send Message
</button>
</form>