Mobile-First Forms — Complete Guide
In this tutorial, you will learn about Mobile. We cover key concepts, practical examples, and best practices to help you master this topic.
Mobile-first forms use single-column layouts, large tap targets, native input types, floating labels, inline validation, and touch-friendly controls for all devices.
What You'll Learn
- Single-column form layout
- Tap target sizes and spacing
- Native HTML input types
- Floating labels vs placeholder-only
- Inline validation feedback
- Keyboard-friendly form flow
- Autocomplete and autofill
Why It Matters
- Forms are the primary conversion tool on the web
- Mobile form abandonment is 3x higher than desktop
- Small tap targets cause frustration and errors
- Poor keyboard handling breaks the user flow
Real-World Use
- A checkout form that works one-thumbed on a phone
- A login form that triggers the correct keyboard
- A signup form with real-time inline validation
- A contact form that autofills from saved data
flowchart LR A[Mobile-First Forms] --> B[Layout] A --> C[Inputs] A --> D[Validation] A --> E[Submission] B --> F[Single column] C --> G[Native types] D --> H[Inline feedback] E --> I[Loading state]
Single-Column Layout
Mobile forms must use a single column. Multiple columns force users to scroll left and right on narrow screens, causing a poor experience.
Code Example: Single-Column Form
<form class="mobile-form" novalidate>
<div class="form-group">
<label for="name">Full Name</label>
<input
type="text"
id="name"
name="name"
autocomplete="name"
required
inputmode="text"
>
</div>
<div class="form-group">
<label for="email">Email Address</label>
<input
type="email"
id="email"
name="email"
autocomplete="email"
required
inputmode="email"
>
</div>
<div class="form-group">
<label for="phone">Phone Number</label>
<input
type="tel"
id="phone"
name="phone"
autocomplete="tel"
inputmode="numeric"
pattern="[0-9\-\(\)\s]+"
>
</div>
<div class="form-group">
<label for="message">Message</label>
<textarea
id="message"
name="message"
rows="4"
autocomplete="off"
inputmode="text"
></textarea>
</div>
<button type="submit" class="submit-btn">Send Message</button>
</form>
<style>
.mobile-form {
max-width: 480px;
margin: 0 auto;
padding: 1rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.375rem;
font-weight: 600;
font-size: 0.9375rem;
color: #1f2937;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 0.75rem 0.875rem;
font-size: 1rem;
line-height: 1.5;
border: 2px solid #d1d5db;
border-radius: 8px;
background: #fff;
transition: border-color 0.2s ease;
-webkit-appearance: none;
appearance: none;
}
.form-group input:focus,
.form-group textarea:focus {
border-color: #3b82f6;
outline: none;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
.form-group input.error,
.form-group textarea.error {
border-color: #ef4444;
}
.submit-btn {
width: 100%;
padding: 1rem 1.5rem;
font-size: 1.125rem;
font-weight: 600;
color: #fff;
background: #3b82f6;
border: none;
border-radius: 8px;
cursor: pointer;
min-height: 48px;
transition: background 0.2s ease;
}
.submit-btn:active {
background: #2563eb;
}
.submit-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
</style>
Expected output: A single-column form that fills the screen width on mobile. Each field is stacked vertically with clear labels above the input. The submit button spans the full width and has a 48px minimum height for easy tapping.
Native Input Types and Keyboards
Using the correct input type tells mobile browsers to show the appropriate keyboard, autocomplete, and validation.
Code Example: Input Types and Keyboard Triggers
<form class="mobile-form">
<!-- Numeric keyboard -->
<div class="form-group">
<label for="age">Age</label>
<input
type="number"
id="age"
name="age"
inputmode="numeric"
pattern="[0-9]*"
autocomplete="off"
>
</div>
<!-- Telephone keyboard -->
<div class="form-group">
<label for="mobile">Mobile Number</label>
<input
type="tel"
id="mobile"
name="mobile"
inputmode="tel"
autocomplete="tel-national"
pattern="[0-9\+\-\(\)\s]{7,20}"
>
</div>
<!-- Email keyboard with @ and .com keys -->
<div class="form-group">
<label for="user-email">Email</label>
<input
type="email"
id="user-email"
name="user-email"
inputmode="email"
autocomplete="email"
required
>
</div>
<!-- URL keyboard with . and / keys -->
<div class="form-group">
<label for="website">Website</label>
<input
type="url"
id="website"
name="website"
inputmode="url"
autocomplete="url"
placeholder="https://example.com"
>
</div>
<!-- Search keyboard with search button -->
<div class="form-group">
<label for="search">Search</label>
<input
type="search"
id="search"
name="search"
inputmode="search"
autocomplete="off"
enterkeyhint="search"
>
</div>
<!-- Date picker (native) -->
<div class="form-group">
<label for="dob">Date of Birth</label>
<input
type="date"
id="dob"
name="dob"
autocomplete="bday"
>
</div>
</form>
Expected output: On mobile devices, each input triggers a different keyboard. The email field shows the @ and .com keys. The tel field shows a numeric keypad. The search field shows a search button on the keyboard.
Floating Labels
Floating labels stay visible in the input when the user is typing, solving the problem of labels disappearing behind the virtual keyboard.
Code Example: Floating Labels
<div class="floating-container">
<form class="floating-form">
<div class="floating-group">
<input
type="text"
id="fl-name"
class="floating-input"
placeholder="Full Name"
autocomplete="name"
required
>
<label for="fl-name" class="floating-label">Full Name</label>
</div>
<div class="floating-group">
<input
type="email"
id="fl-email"
class="floating-input"
placeholder="Email Address"
autocomplete="email"
required
>
<label for="fl-email" class="floating-label">Email Address</label>
</div>
<div class="floating-group">
<input
type="password"
id="fl-pass"
class="floating-input"
placeholder="Password"
autocomplete="new-password"
required
minlength="8"
>
<label for="fl-pass" class="floating-label">Password</label>
</div>
<button type="submit" class="submit-btn">Sign Up</button>
</form>
</div>
<style>
.floating-container {
max-width: 480px;
margin: 0 auto;
padding: 1rem;
}
.floating-group {
position: relative;
margin-bottom: 1.5rem;
}
.floating-input {
width: 100%;
padding: 1.25rem 0.875rem 0.5rem;
font-size: 1rem;
border: 2px solid #d1d5db;
border-radius: 8px;
background: #fff;
transition: border-color 0.2s ease;
-webkit-appearance: none;
appearance: none;
}
.floating-input:focus {
border-color: #3b82f6;
outline: none;
}
.floating-label {
position: absolute;
left: 0.875rem;
top: 50%;
transform: translateY(-50%);
font-size: 1rem;
color: #6b7280;
transition: all 0.2s ease;
pointer-events: none;
background: #fff;
padding: 0 0.25rem;
}
.floating-input:focus ~ .floating-label,
.floating-input:not(:placeholder-shown) ~ .floating-label {
top: 0.25rem;
transform: translateY(0);
font-size: 0.75rem;
color: #3b82f6;
}
.floating-input.error ~ .floating-label {
color: #ef4444;
}
</style>
Expected output: Labels float above the input when the user types or focuses the field. When empty and unfocused, the label sits inside the input as a placeholder. This saves vertical space while keeping labels visible.
Inline Validation
Validate inputs as the user types, providing immediate feedback without waiting for form submission.
Code Example: Inline Validation
<form class="mobile-form" id="validation-form">
<div class="form-group">
<label for="val-email">Email Address</label>
<input
type="email"
id="val-email"
name="val-email"
autocomplete="email"
required
>
<span class="feedback" id="email-feedback"></span>
</div>
<div class="form-group">
<label for="val-pass">Password</label>
<input
type="password"
id="val-pass"
name="val-pass"
autocomplete="new-password"
required
minlength="8"
>
<ul class="password-rules" id="pass-rules">
<li data-rule="length">At least 8 characters</li>
<li data-rule="uppercase">One uppercase letter</li>
<li data-rule="digit">One digit</li>
</ul>
</div>
</form>
<script>
const emailInput = document.getElementById('val-email');
const emailFeedback = document.getElementById('email-feedback');
const passInput = document.getElementById('val-pass');
const passRules = document.getElementById('pass-rules');
emailInput.addEventListener('input', function() {
if (this.validity.valid) {
this.classList.remove('error');
emailFeedback.textContent = 'Valid email';
emailFeedback.className = 'feedback success';
} else if (this.value.length > 0) {
this.classList.add('error');
emailFeedback.textContent = 'Please enter a valid email';
emailFeedback.className = 'feedback error';
} else {
this.classList.remove('error');
emailFeedback.textContent = '';
emailFeedback.className = 'feedback';
}
});
passInput.addEventListener('input', function() {
const value = this.value;
const rules = {
length: value.length >= 8,
uppercase: /[A-Z]/.test(value),
digit: /\d/.test(value)
};
passRules.querySelectorAll('li').forEach(li => {
const rule = li.dataset.rule;
if (rules[rule]) {
li.classList.add('valid');
li.classList.remove('invalid');
} else {
li.classList.remove('valid');
li.classList.add('invalid');
}
});
});
</script>
<style>
.feedback {
display: block;
margin-top: 0.25rem;
font-size: 0.8125rem;
min-height: 1.2em;
}
.feedback.success {
color: #16a34a;
}
.feedback.error {
color: #ef4444;
}
.password-rules {
list-style: none;
padding: 0.5rem 0 0;
margin: 0;
font-size: 0.8125rem;
}
.password-rules li {
padding: 0.125rem 0;
color: #6b7280;
}
.password-rules li.valid {
color: #16a34a;
}
.password-rules li.valid::before {
content: '✓ ';
}
.password-rules li.invalid::before {
content: '✗ ';
}
.password-rules li.invalid {
color: #ef4444;
}
</style>
Expected output: As the user types an email, it shows valid or invalid feedback in real time. Password rules check each condition and update the visual status. No submit needed for basic validation.
Keyboard-Friendly Navigation
Ensure the form flows naturally when the user tabs or taps through fields, especially with the virtual keyboard open.
Code Example: Keyboard-Aware Form
// Scroll the field into view when focused (iOS fix)
document.querySelectorAll('.form-group input, .form-group textarea, .form-group select').forEach(field => {
field.addEventListener('focus', function() {
setTimeout(() => {
this.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, 300);
});
});
// Prevent zoom on focus for iOS inputs
const style = document.createElement('style');
style.textContent = `
input, textarea, select {
font-size: 16px !important;
}
`;
document.head.appendChild(style);
// Handle keyboard open/close (adjust layout)
const form = document.querySelector('.mobile-form');
if ('visualViewport' in window) {
let initialHeight = window.visualViewport.height;
window.visualViewport.addEventListener('resize', () => {
const currentHeight = window.visualViewport.height;
const diff = initialHeight - currentHeight;
if (diff > 100) {
form.style.paddingBottom = diff + 'px';
} else {
form.style.paddingBottom = '1rem';
}
});
}
Expected output: When the user taps a field near the bottom, the page scrolls to show it above the keyboard. Inputs do not zoom on iOS. The form adds padding at the bottom to prevent the keyboard from covering the submit button.
Common Mistakes
- No autocomplete attribute — Users must type everything manually, increasing friction and error rates.
- Wrong input type — Using type="text" for emails means the keyboard lacks the @ key and autofill may not work.
- Placeholder-only labels — Once the user types, the label disappears and they cannot verify what the field is for.
- Tiny tap targets — Buttons and inputs smaller than 44x44px are hard to tap accurately on mobile.
- No inputmode — Even with the correct type, inputmode tells the browser exactly which keyboard layout to show.
- Form zoom on focus — iOS zooms into inputs with font-size smaller than 16px, breaking the layout.
- No loading state on submit — Users tap submit multiple times, causing duplicate submissions.
Practice Questions
- What input type triggers a numeric keypad on mobile? type="tel" or type="number" with inputmode="numeric".
- Why should labels never rely only on placeholders? Placeholders disappear when the user types, making it impossible to verify what the field expects.
- What is the minimum tap target size recommended for mobile forms? 44x44px (WCAG 2.1 Success Criterion 2.5.5).
- What CSS property prevents iOS zoom on input focus? Setting font-size to 16px or larger on input elements.
- What attribute helps browsers autofill form fields? autocomplete with values like "email", "name", "tel-national".
Challenge
Build a multi-step checkout form for an e-commerce site. Use a single-column layout, native input types for each field (email, tel, number for credit card, select for country), floating labels, inline validation for email and card number, a progress indicator showing steps (Shipping, Payment, Review), and a loading spinner on the final submit button.
FAQ
{{< faq "What inputmode values are most useful?" "inputmode=\"numeric\" for digits, \"tel\" for phone numbers, \"email\" for email addresses, \"url\" for URLs, \"decimal\" for decimal numbers, and \"search\" for search fields." >}}Mini Project
Build a complete mobile-first registration form for a SaaS product. Include: (1) single-column layout with max-width 480px, (2) 8 fields with correct input types (text, email, tel, password, date, select, checkbox for terms, file for avatar), (3) floating labels on all text inputs, (4) inline validation with real-time feedback, (5) autocomplete on all appropriate fields, (6) a show/hide password toggle, (7) a submit button with loading state and success animation, (8) keyboard-aware scroll behavior. Test on an actual phone with the keyboard open. Verify every field triggers the correct keyboard and autofill suggestions.
What's Next
Continue with Lesson 7: Mobile-First Tables to learn how to present tabular data on small screens.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro