Loading...
Loading...
Mobile-first UX best practices enforcer for web applications. Covers touch targets, responsive design, mobile gestures, performance optimization, and accessibility. Use when creating or editing mobile UI components, implementing responsive layouts, handling touch interactions, optimizing for mobile devices, or working with viewport configurations, breakpoints, and mobile-specific patterns.
npx skill4agent add ramunasnognys/workflow-template mobile-ux-improver// Mobile-first approach
<div className="w-full md:w-1/2 lg:w-1/3">
// Breakpoints
sm: 640px // Small tablets
md: 768px // Tablets
lg: 1024px // Small desktops
xl: 1280px // Large desktops
2xl: 1536px // Extra large// ✅ CORRECT - Mobile first, then desktop
<button className="w-full px-4 py-3 md:w-auto md:px-6">
Tap Me
</button>
// ❌ WRONG - Desktop first
<button className="w-auto px-6 sm:w-full sm:px-4">
Tap Me
</button>// ✅ CORRECT - 48px touch target
<button className="min-h-[48px] min-w-[48px] p-3">
<Icon className="w-6 h-6" />
</button>
// ❌ WRONG - Too small
<button className="p-1">
<Icon className="w-4 h-4" />
</button>// ✅ CORRECT - Adequate spacing
<div className="flex gap-3">
<button className="p-3">A</button>
<button className="p-3">B</button>
</div>
// ❌ WRONG - Targets too close
<div className="flex gap-1">
<button className="p-1">A</button>
<button className="p-1">B</button>
</div>// ✅ CORRECT - Scales with viewport
<h1 className="text-2xl md:text-4xl lg:text-5xl">
Heading
</h1>
// ✅ CORRECT - Clamp for fluid scaling
<p className="text-base leading-relaxed">
Body text
</p>
// ⚠️ AVOID - Fixed pixel sizes
<h1 style={{ fontSize: '60px' }}>
Heading
</h1><meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5">| Element Type | Minimum Size | Recommended Size | Spacing |
|---|---|---|---|
| Primary Button | 44px | 48px | 12px |
| Icon Button | 44px | 48px | 8px |
| Link in Text | 44px height | 48px height | 8px vertical |
| Checkbox/Radio | 44px | 48px | 12px |
| Slider Thumb | 44px | 48px | N/A |
<button className="w-full px-6 py-4 text-lg font-semibold rounded-lg bg-brand-blue text-white hover:bg-blue-600 active:bg-blue-700 transition-colors min-h-[48px]">
Continue
</button><button
className="p-3 rounded-full hover:bg-gray-100 active:bg-gray-200 transition-colors min-h-[48px] min-w-[48px] flex items-center justify-center"
aria-label="Close"
>
<XIcon className="w-6 h-6" />
</button><nav className="space-y-2">
{links.map(link => (
<a
key={link.id}
href={link.url}
className="block px-4 py-3 text-base hover:bg-gray-50 active:bg-gray-100 rounded-lg min-h-[48px]"
>
{link.label}
</a>
))}
</nav>// Layout structure
<div className="
flex flex-col gap-4 /* Mobile: stack vertically */
md:flex-row md:gap-6 /* Tablet: horizontal */
lg:gap-8 /* Desktop: more spacing */
">
<aside className="w-full md:w-64 lg:w-80">
Sidebar
</aside>
<main className="flex-1">
Content
</main>
</div>// Full-width on mobile, constrained on desktop
<div className="
w-full px-4 /* Mobile: full width, padding */
md:px-6 /* Tablet: more padding */
lg:max-w-7xl lg:mx-auto lg:px-8 /* Desktop: centered, max width */
">
{children}
</div><div className="
grid grid-cols-1 gap-4 /* Mobile: 1 column */
sm:grid-cols-2 sm:gap-6 /* Small tablet: 2 columns */
lg:grid-cols-3 lg:gap-8 /* Desktop: 3 columns */
">
{items.map(item => (
<Card key={item.id} {...item} />
))}
</div>// Mobile menu vs desktop nav
<>
{/* Mobile menu button */}
<button className="md:hidden p-3">
<MenuIcon />
</button>
{/* Desktop navigation */}
<nav className="hidden md:flex gap-6">
<NavLink href="/about">About</NavLink>
<NavLink href="/contact">Contact</NavLink>
</nav>
</><div className="
flex gap-4 overflow-x-auto snap-x snap-mandatory
scrollbar-hide /* Hide scrollbar on mobile */
pb-4 /* Padding for scroll indicator */
-mx-4 px-4 /* Edge-to-edge on mobile */
md:mx-0 md:px-0 /* Contained on desktop */
">
{items.map(item => (
<div
key={item.id}
className="flex-shrink-0 w-72 snap-center"
>
<Card {...item} />
</div>
))}
</div><main className="
overscroll-contain /* Prevent chain scrolling */
overflow-y-auto
h-screen
">
{content}
</main>// Global CSS or Tailwind
<html className="scroll-smooth">
// Component-level
<div className="overflow-y-auto scroll-smooth">
{longContent}
</div><img
src="/hero-mobile.jpg"
srcSet="
/hero-mobile.jpg 640w,
/hero-tablet.jpg 1024w,
/hero-desktop.jpg 1920w
"
sizes="
(max-width: 640px) 100vw,
(max-width: 1024px) 50vw,
33vw
"
alt="Hero image"
loading="lazy"
className="w-full h-auto"
/>import { lazy, Suspense } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function Page() {
return (
<Suspense fallback={<LoadingSpinner />}>
<HeavyComponent />
</Suspense>
);
}<link
rel="preload"
href="/fonts/custom-font.woff2"
as="font"
type="font/woff2"
crossorigin
/><button
aria-label="Close dialog"
className="p-3"
>
<XIcon className="w-6 h-6" />
</button><button className="
p-3 rounded-lg
focus:outline-none focus:ring-2 focus:ring-brand-blue focus:ring-offset-2
active:bg-gray-100
">
Action
</button>// ❌ WRONG - No hover on touch devices
<div className="hover:visible">
Hidden content
</div>
// ✅ CORRECT - Click/tap to reveal
<button onClick={() => setVisible(true)}>
Show content
</button>// ❌ WRONG - Header blocks content
<header className="fixed top-0 w-full h-16 bg-white">
<main className="pt-0"> /* Content hidden behind header */
// ✅ CORRECT - Account for header
<header className="fixed top-0 w-full h-16 bg-white z-10">
<main className="pt-16"> /* Content starts below header */// ❌ WRONG - Unreadable on mobile
<p className="text-xs">
Important information
</p>
// ✅ CORRECT - Readable base size
<p className="text-base md:text-sm">
Important information
</p><!-- ❌ WRONG - Prevents accessibility -->
<meta name="viewport" content="user-scalable=no, maximum-scale=1">
<!-- ✅ CORRECT - Allow zoom for accessibility -->
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5">import React from 'react';
interface ProductCardProps {
title: string;
price: number;
image: string;
onAddToCart: () => void;
}
export const ProductCard: React.FC<ProductCardProps> = ({
title,
price,
image,
onAddToCart
}) => {
return (
<div className="
bg-white rounded-lg shadow-sm overflow-hidden
hover:shadow-md transition-shadow
w-full /* Mobile: full width */
sm:w-64 /* Tablet: fixed width */
">
<img
src={image}
alt={title}
loading="lazy"
className="w-full h-48 object-cover"
/>
<div className="p-4">
<h3 className="text-lg font-semibold mb-2 line-clamp-2">
{title}
</h3>
<p className="text-2xl font-bold text-brand-blue mb-4">
${price.toFixed(2)}
</p>
<button
onClick={onAddToCart}
className="
w-full px-6 py-3 rounded-lg
bg-brand-blue text-white font-semibold
hover:bg-blue-600 active:bg-blue-700
transition-colors
min-h-[48px] /* Touch-friendly height */
focus:outline-none focus:ring-2 focus:ring-brand-blue focus:ring-offset-2
"
aria-label={`Add ${title} to cart`}
>
Add to Cart
</button>
</div>
</div>
);
};
export default ProductCard;FilterPill.tsxTeamCard.tsxFab.tsx