Mobile-First Buttons — 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 buttons use adequate sizing, touch-friendly spacing, clear states, thumb-zone placement, and accessible interactions for reliable tapping on all devices.
What You'll Learn
- Minimum tap target sizes
- Touch-friendly spacing and grouping
- Visual states (default, hover, active, disabled)
- Thumb zone placement
- Button loading and feedback states
- Touch event optimization
- Accessible button markup
Why It Matters
- Small buttons cause mistaps and user frustration
- Poor button placement makes one-handed use impossible
- Missing feedback leads to double-taps
- WCAG requires minimum 44x44px touch targets
Real-World Use
- A "Buy Now" button at thumb-reach on a shopping app
- A form submit button with loading spinner feedback
- A floating action button for primary actions
- A button group that adapts spacing for touch
flowchart LR A[Mobile-First Buttons] --> B[Sizing] A --> C[Spacing] A --> D[States] A --> E[Placement] B --> F[44x44px min] C --> G[8px+ gap] D --> H[Hover/Active/Disabled] E --> I[Thumb zones]
Minimum Tap Target Size
Every tappable element must be at least 44x44px with adequate spacing from adjacent targets.
Code Example: Proper Sizing
<!-- Minimum viable button -->
<button class="btn btn-primary" style="min-width: 44px; min-height: 44px;">
Submit
</button>
<!-- Icon button with proper size -->
<button class="icon-btn" aria-label="Delete item">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/>
</svg>
</button>
<!-- Link styled as button -->
<a href="/checkout" class="btn btn-primary" role="button">
Proceed to Checkout
</a>
<style>
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 44px;
min-height: 44px;
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: 600;
border: none;
border-radius: 8px;
cursor: pointer;
text-decoration: none;
font-family: inherit;
line-height: 1.2;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
.btn-primary {
background: #3b82f6;
color: #fff;
}
.btn-primary:active {
background: #2563eb;
}
.btn-secondary {
background: #e5e7eb;
color: #374151;
}
.btn-secondary:active {
background: #d1d5db;
}
.icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
padding: 0;
border: none;
border-radius: 8px;
background: transparent;
cursor: pointer;
color: #6b7280;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
.icon-btn:active {
background: #f3f4f6;
color: #374151;
}
</style>
Expected output: All buttons have at least 44x44px touch targets. Icon-only buttons include an aria-label for Accessibility. The link styled as a button maintains proper focus and click behavior.
Button Spacing and Grouping
Adjacent buttons must have enough space between them to prevent mistaps.
Code Example: Button Groups
<!-- Button row with adequate spacing -->
<div class="btn-row">
<button class="btn btn-secondary">Cancel</button>
<button class="btn btn-primary">Save Changes</button>
</div>
<!-- Stacked buttons on mobile -->
<div class="btn-stack">
<button class="btn btn-primary">Primary Action</button>
<button class="btn btn-secondary">Secondary Action</button>
<a href="/learn-more" class="btn btn-text">Learn More</a>
</div>
<!-- Segmented control (button group) -->
<div class="segmented-control" role="group" aria-label="View mode">
<button class="segmented-btn active" aria-pressed="true">Day</button>
<button class="segmented-btn" aria-pressed="false">Week</button>
<button class="segmented-btn" aria-pressed="false">Month</button>
</div>
<style>
.btn-row {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.btn-row .btn {
flex: 1;
min-width: 120px;
}
.btn-stack {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.btn-stack .btn {
width: 100%;
}
.btn-text {
background: transparent;
color: #3b82f6;
padding: 0.75rem 0;
}
.btn-text:active {
color: #2563eb;
background: transparent;
}
.segmented-control {
display: flex;
border: 2px solid #d1d5db;
border-radius: 8px;
overflow: hidden;
}
.segmented-btn {
flex: 1;
padding: 0.75rem 1rem;
font-size: 0.875rem;
font-weight: 500;
border: none;
background: #fff;
color: #6b7280;
cursor: pointer;
min-height: 44px;
transition: background 0.2s;
-webkit-tap-highlight-color: transparent;
}
.segmented-btn.active {
background: #3b82f6;
color: #fff;
}
.segmented-btn:not(.active):active {
background: #f3f4f6;
}
</style>
Expected output: Buttons in a row have 12px gap minimum. Stacked buttons fill full width on narrow screens. The segmented control has clear active state and adequate tap targets.
Button States
Every button needs clear visual feedback for default, hover, active, focused, and disabled states.
Code Example: All Button States
<button class="stateful-btn" id="stateful-btn">
<span class="btn-label">Click Me</span>
<span class="btn-loader" hidden>
<span class="spinner"></span> Processing...
</span>
</button>
<button class="stateful-btn" disabled>
Disabled Action
</button>
<style>
.stateful-btn {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
min-width: 140px;
min-height: 48px;
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: 600;
color: #fff;
background: #3b82f6;
border: none;
border-radius: 8px;
cursor: pointer;
overflow: hidden;
transition: background 0.2s, transform 0.1s, box-shadow 0.2s;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
/* Hover (desktop only) */
@media (hover: hover) {
.stateful-btn:hover:not(:disabled) {
background: #2563eb;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
}
}
/* Active (pressed) */
.stateful-btn:active:not(:disabled) {
transform: scale(0.97);
background: #1d4ed8;
}
/* Focus visible */
.stateful-btn:focus-visible {
outline: 3px solid #93c5fd;
outline-offset: 2px;
}
/* Disabled */
.stateful-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
background: #93c5fd;
}
/* Loading state */
.stateful-btn.loading .btn-label {
visibility: hidden;
}
.stateful-btn.loading .btn-loader {
visibility: visible;
}
.spinner {
display: inline-block;
width: 18px;
height: 18px;
border: 2px solid rgba(255,255,255,0.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
<script>
const btn = document.getElementById('stateful-btn');
btn.addEventListener('click', function() {
this.classList.add('loading');
this.disabled = true;
setTimeout(() => {
this.classList.remove('loading');
this.disabled = false;
}, 2000);
});
</script>
Expected output: The button shows hover state on desktop (darker blue, shadow), scales down slightly on press, shows a loading spinner when clicked, and appears faded when disabled.
Thumb Zone Placement
Place primary buttons where the thumb naturally rests on mobile screens.
Code Example: Thumb-Optimized Layout
<div class="thumb-zone-layout">
<header class="app-header">
<button class="icon-btn" aria-label="Menu">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 12h18M3 6h18M3 18h18"/>
</svg>
</button>
<h1>Dashboard</h1>
<button class="icon-btn" aria-label="Notifications">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/>
</svg>
</button>
</header>
<main class="app-content">
<p>Main content here. The primary action button is placed at the bottom where thumbs reach easily.</p>
</main>
<!-- Bottom action area (easy thumb reach) -->
<div class="bottom-actions">
<button class="btn btn-secondary">Cancel</button>
<button class="btn btn-primary">Confirm Order</button>
</div>
<!-- Floating action button (bottom right, thumb zone) -->
<button class="fab" aria-label="Add new item">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 5v14M5 12h14"/>
</svg>
</button>
</div>
<style>
.thumb-zone-layout {
position: relative;
max-width: 480px;
margin: 0 auto;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.app-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
border-bottom: 1px solid #e5e7eb;
}
.app-header h1 {
font-size: 1.125rem;
font-weight: 600;
}
.app-content {
flex: 1;
padding: 1rem;
}
.bottom-actions {
display: flex;
gap: 0.75rem;
padding: 1rem;
border-top: 1px solid #e5e7eb;
background: #fff;
position: sticky;
bottom: 0;
}
.bottom-actions .btn {
flex: 1;
}
.fab {
position: fixed;
bottom: 5rem;
right: 1.5rem;
width: 56px;
height: 56px;
border-radius: 50%;
background: #3b82f6;
color: #fff;
border: none;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.4);
cursor: pointer;
z-index: 100;
transition: transform 0.2s, box-shadow 0.2s;
-webkit-tap-highlight-color: transparent;
}
.fab:active {
transform: scale(0.92);
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
}
</style>
Expected output: The primary button appears at the bottom of the screen, within easy thumb reach. The floating action button sits in the bottom-right corner, the optimal thumb zone for right-handed users.
Common Mistakes
- Buttons smaller than 44x44px — Users struggle to tap accurately, leading to frustration and errors.
- No gap between adjacent buttons — Users tap the wrong button because targets are too close.
- Same size for primary and secondary buttons — No visual hierarchy. Users cannot distinguish the recommended action.
- No active/pressed state — Users are unsure if their tap registered, so they tap again.
- Disabled buttons without explanation — Users cannot tell why they cannot proceed. Show the reason instead of graying out the button.
- Touch action not set — The browser may delay tap response by 300ms waiting for a double-tap.
- Loading state without disabling — Users can press the button multiple times, submitting duplicate data.
Practice Questions
- What is the minimum tap target size according to WCAG? 44x44px for pointer inputs (WCAG 2.5.5).
- What CSS property prevents the 300ms tap delay on mobile? touch-action: manipulation eliminates the delay by disabling double-tap zoom.
- How do you prevent double form submission from double-tapping? Disable the submit button immediately on click and show a loading state.
- What is the ideal button placement for one-handed mobile use? Bottom of the screen, within the natural thumb arc (bottom-right for right-handed, bottom-left for left-handed).
- What attribute makes icon-only buttons accessible? aria-label describing the button's action.
Challenge
Build a mobile checkout flow with three steps (Cart, Shipping, Payment). Each step has primary and secondary buttons at the bottom. The primary button must show a loading state with spinner when clicked. The "Place Order" button on the final step must validate all fields before enabling. Include a floating action button for "Add Coupon" that appears only on the cart step. Test on a real device with one-handed use.
FAQ
Mini Project
Build a settings screen for a mobile app. Include: (1) a top toolbar with back button (icon, 44x44px) and title, (2) toggle switches for each setting (44px minimum height, with on/off states), (3) a segmented control for theme selection (Light/Dark/System), (4) a bottom action bar with "Cancel" and "Save Changes" buttons (48px height, 12px gap), (5) a destructive "Delete Account" button styled in red with confirmation dialog, (6) proper focus-visible styles for keyboard navigation, (7) touch-action: manipulation on all interactive elements, (8) loading state on the save button.
What's Next
Continue with Lesson 9: Mobile-First Inputs to design touch-friendly input fields for mobile interfaces.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro