| name | mobile-ux-optimizer |
| description | Mobile-first UX optimization for touch interfaces, responsive layouts, and performance. Use for viewport handling, touch targets, gestures, mobile navigation. Activate on mobile, touch, responsive, dvh, viewport, safe area, hamburger menu. NOT for native app development (use React Native skills), desktop-only features, or general CSS (use Tailwind docs). |
| allowed-tools | Read,Write,Edit,Bash,Grep,Glob |
| metadata | {"category":"Design & Creative","tags":["mobile","ux","touch","responsive","viewport","safe-area","navigation"],"pairs-with":[{"skill":"pwa-expert","reason":"PWAs are a primary mobile delivery mechanism requiring touch and viewport optimization"},{"skill":"ux-friction-analyzer","reason":"Mobile-specific friction points (fat finger errors, scroll hijacking) need specialized analysis"},{"skill":"adhd-design-expert","reason":"Mobile ADHD users face amplified cognitive load that requires specialized UX optimization"}]} |
Mobile-First UX Optimization
Build touch-optimized, performant mobile experiences with proper viewport handling and responsive patterns.
When to Use
✅ USE this skill for:
- Viewport issues (
100vh problems, safe areas, notches)
- Touch target sizing and spacing
- Mobile navigation patterns (bottom nav, drawers, hamburger menus)
- Swipe gestures and pull-to-refresh
- Responsive breakpoint strategies
- Mobile performance optimization
❌ DO NOT use for:
- Native app development → use
react-native or swift-executor skills
- Desktop-only features → no skill needed, standard patterns apply
- General CSS/Tailwind questions → use Tailwind docs or
web-design-expert
- PWA installation/service workers → use
pwa-expert skill
Core Principles
Mobile-First Means Build Up, Not Down
.card { width: 400px; }
@media (max-width: 768px) { .card { width: 100%; } }
.card { width: 100%; }
@media (min-width: 768px) { .card { width: 400px; } }
The 44px Rule
Apple's Human Interface Guidelines specify 44×44 points as minimum touch target. Google Material suggests 48×48dp.
<button className="min-h-[44px] min-w-[44px] px-4 py-3">
Tap me
</button>
<a href="/page" className="inline-block py-3 px-4">
Link text
</a>
Viewport Handling
The dvh Solution
Mobile browsers have dynamic toolbars. 100vh includes the URL bar, causing content to be cut off.
.full-screen { height: 100vh; }
.full-screen { height: 100dvh; }
.full-screen {
height: 100vh;
height: 100dvh;
}
Safe Area Insets (Notches & Home Indicators)
.bottom-nav {
padding-bottom: env(safe-area-inset-bottom, 0);
}
.header {
padding-top: env(safe-area-inset-top, 0);
}
.safe-container {
padding: env(safe-area-inset-top)
env(safe-area-inset-right)
env(safe-area-inset-bottom)
env(safe-area-inset-left);
}
Required meta tag:
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
Tailwind Safe Area Classes
@layer utilities {
.pb-safe { padding-bottom: env(safe-area-inset-bottom); }
.pt-safe { padding-top: env(safe-area-inset-top); }
.h-screen-safe { height: calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom)); }
}
<nav className="fixed bottom-0 pb-safe bg-leather-900">
<BottomNav />
</nav>
Mobile Navigation Patterns
Bottom Navigation (Recommended for Mobile)
'use client';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
const navItems = [
{ href: '/', icon: HomeIcon, label: 'Home' },
{ href: '/meetings', icon: CalendarIcon, label: 'Meetings' },
{ href: '/tools', icon: ToolsIcon, label: 'Tools' },
{ href: '/my', icon: UserIcon, label: 'My Recovery' },
];
export function BottomNav() {
const pathname = usePathname();
return (
<nav className="fixed bottom-0 left-0 right-0 bg-leather-900 border-t border-leather-700 pb-safe">
<div className="flex justify-around">
{navItems.map(({ href, icon: Icon, label }) => {
const isActive = pathname === href || pathname.startsWith(`${href}/`);
return (
<Link
=
=
=`
[] []
${ ? '' ''}
`}
>
{label}
);
})}
);
}
Slide-Out Drawer (Side Menu)
'use client';
import { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
interface DrawerProps {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
}
export function Drawer({ isOpen, onClose, children }: DrawerProps) {
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
}
return () => {
document.body.style.overflow = '';
};
}, [isOpen]);
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
document.(, handleEscape);
.(, handleEscape);
}, [onClose]);
(!isOpen) ;
(
,
.
);
}
Touch Gestures
Full implementations in references/gestures.md
| Hook | Purpose |
|---|
useSwipe() | Directional swipe detection with configurable threshold |
usePullToRefresh() | Pull-to-refresh with visual feedback and resistance |
Quick usage:
const { handleTouchStart, handleTouchEnd } = useSwipe({
onSwipeLeft: () => dismiss(),
threshold: 50,
});
const { containerRef, pullDistance, isRefreshing, handlers } =
usePullToRefresh(async () => await refetchData());
Mobile Performance
Image Optimization
import Image from 'next/image';
<Image
src="/hero.jpg"
alt="Hero"
fill
sizes="(max-width: 768px) 100vw, 50vw"
priority // For above-the-fold images
className="object-cover"
/>
<Image
src="/feature.jpg"
alt="Feature"
width={400}
height={300}
loading="lazy"
/>
Reduce Bundle Size
const HeavyChart = dynamic(() => import('@/components/Chart'), {
loading: () => <ChartSkeleton />,
ssr: false,
});
const Comments = dynamic(() => import('@/components/Comments'));
Skeleton Screens (Not Spinners)
function MeetingCardSkeleton() {
return (
<div className="p-4 bg-leather-800 rounded-lg animate-pulse">
<div className="h-4 bg-leather-700 rounded w-3/4 mb-2" />
<div className="h-3 bg-leather-700 rounded w-1/2 mb-4" />
<div className="flex gap-2">
<div className="h-6 w-16 bg-leather-700 rounded" />
<div className="h-6 w-16 bg-leather-700 rounded" />
</div>
</div>
);
}
{isLoading ? (
<div className="space-y-4">
{[...Array(5)].map((_, i) => <MeetingCardSkeleton key={i} />)}
</div>
) : (
meetings.map(m => <MeetingCard key={m.id} meeting={m} />)
)}
Responsive Patterns
Tailwind Breakpoint Strategy
sm: 640px - Large phones (landscape)
md: 768px - Tablets
lg: 1024px - Small laptops
xl: 1280px - Desktops
2xl: 1536px - Large screens
<div className="flex flex-col md:flex-row gap-4">
<aside className="w-full md:w-64">Sidebar</aside>
<main className="flex-1">Content</main>
</div>
<nav className="md:hidden fixed bottom-0 left-0 right-0">
<BottomNav />
</nav>
<aside className="hidden md:block w-64">
<SidebarNav />
</aside>
Container Queries (CSS-only Responsive Components)
@container (min-width: 400px) {
.card { flex-direction: row; }
}
<div className="@container">
<div className="flex flex-col @md:flex-row">
{/* Responds to parent container width */}
</div>
</div>
Testing on Real Devices
Chrome DevTools Mobile Emulation
- Open DevTools (F12)
- Toggle device toolbar (Ctrl+Shift+M)
- Select device or set custom dimensions
- Throttle network/CPU for realistic performance
Must-Test Scenarios
BrowserStack/Real Device Testing
npx localtunnel --port 3000
ngrok http 3000
Quick Reference
| Issue | Solution |
|---|
| Content cut off at bottom | Use 100dvh instead of 100vh |
| Notch overlaps content | Add pt-safe / pb-safe |
| Touch targets too small | Min 44×44px |
| Scroll locked | Check overflow: hidden on body |
| Keyboard covers input | Use visualViewport API |
| Janky scrolling | Use will-change: transform |
| Double-tap zoom | Add touch-action: manipulation |
References
See /references/ for detailed guides:
keyboard-handling.md - Virtual keyboard and form UX
animations.md - Touch-friendly animations
accessibility.md - Mobile a11y requirements