Some_claude_skills mobile-ux-optimizer
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,
install
source · Clone the upstream repo
git clone https://github.com/curiositech/some_claude_skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/curiositech/some_claude_skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/mobile-ux-optimizer" ~/.claude/skills/curiositech-some-claude-skills-mobile-ux-optimizer && rm -rf "$T"
manifest:
.claude/skills/mobile-ux-optimizer/SKILL.mdsource content
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 (
problems, safe areas, notches)100vh - 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
orreact-native
skillsswift-executor - 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
skillpwa-expert
Core Principles
Mobile-First Means Build Up, Not Down
/* ❌ ANTI-PATTERN: Desktop-first (scale down) */ .card { width: 400px; } @media (max-width: 768px) { .card { width: 100%; } } /* ✅ CORRECT: Mobile-first (scale up) */ .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.
// Touch-friendly button <button className="min-h-[44px] min-w-[44px] px-4 py-3"> Tap me </button> // Touch-friendly link with adequate padding <a href="/page" className="inline-block py-3 px-4"> Link text </a>
Viewport Handling
The dvh
Solution
dvhMobile browsers have dynamic toolbars.
100vh includes the URL bar, causing content to be cut off.
/* ❌ ANTI-PATTERN: Content hidden behind browser UI */ .full-screen { height: 100vh; } /* ✅ CORRECT: Responds to browser chrome */ .full-screen { height: 100dvh; } /* Fallback for older browsers */ .full-screen { height: 100vh; height: 100dvh; }
Safe Area Insets (Notches & Home Indicators)
/* Handle iPhone notch and home indicator */ .bottom-nav { padding-bottom: env(safe-area-inset-bottom, 0); } .header { padding-top: env(safe-area-inset-top, 0); } /* Full safe area padding */ .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
// Custom Tailwind utilities (add to globals.css) @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)); } } // Usage <nav className="fixed bottom-0 pb-safe bg-leather-900"> <BottomNav /> </nav>
Mobile Navigation Patterns
Bottom Navigation (Recommended for Mobile)
// components/BottomNav.tsx '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 key={href} href={href} className={` flex flex-col items-center py-2 px-3 min-h-[56px] min-w-[64px] ${isActive ? 'text-ember-400' : 'text-leather-400'} `} > <Icon className="w-6 h-6" /> <span className="text-xs mt-1">{label}</span> </Link> ); })} </div> </nav> ); }
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) { // Prevent body scroll when open useEffect(() => { if (isOpen) { document.body.style.overflow = 'hidden'; } return () => { document.body.style.overflow = ''; }; }, [isOpen]); // Close on escape useEffect(() => { const handleEscape = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; document.addEventListener('keydown', handleEscape); return () => document.removeEventListener('keydown', handleEscape); }, [onClose]); if (!isOpen) return null; return createPortal( <div className="fixed inset-0 z-50"> {/* Backdrop */} <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} aria-hidden="true" /> {/* Drawer */} <div className="absolute left-0 top-0 h-full w-[280px] max-w-[80vw] bg-leather-900 shadow-xl transform transition-transform animate-slide-in-left" role="dialog" aria-modal="true" > <div className="h-full overflow-y-auto pt-safe pb-safe"> {children} </div> </div> </div>, document.body ); }
Touch Gestures
Full implementations in
references/gestures.md
| Hook | Purpose |
|---|---|
| Directional swipe detection with configurable threshold |
| Pull-to-refresh with visual feedback and resistance |
Quick usage:
// Swipe to dismiss const { handleTouchStart, handleTouchEnd } = useSwipe({ onSwipeLeft: () => dismiss(), threshold: 50, }); // Pull to refresh const { containerRef, pullDistance, isRefreshing, handlers } = usePullToRefresh(async () => await refetchData());
Mobile Performance
Image Optimization
import Image from 'next/image'; // Responsive images with proper sizing <Image src="/hero.jpg" alt="Hero" fill sizes="(max-width: 768px) 100vw, 50vw" priority // For above-the-fold images className="object-cover" /> // Lazy load below-fold images <Image src="/feature.jpg" alt="Feature" width={400} height={300} loading="lazy" />
Reduce Bundle Size
// Dynamic imports for heavy components const HeavyChart = dynamic(() => import('@/components/Chart'), { loading: () => <ChartSkeleton />, ssr: false, // Skip server render for client-only }); // Lazy load below-fold sections const Comments = dynamic(() => import('@/components/Comments'));
Skeleton Screens (Not Spinners)
// Skeleton that matches final content layout 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> ); } // Usage {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
// Mobile: stack, Tablet+: side-by-side <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> // Mobile: bottom nav, Desktop: sidebar <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)
/* Component responds to its container, not viewport */ @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
- Content doesn't get cut off by notch/home indicator
- Touch targets are at least 44×44px
- Scrolling is smooth (no jank)
- Bottom nav doesn't block content
- Forms work with virtual keyboard visible
- Landscape orientation works
- Pull-to-refresh doesn't fight with scroll
BrowserStack/Real Device Testing
# Expose local dev server to internet npx localtunnel --port 3000 # or ngrok http 3000
Quick Reference
| Issue | Solution |
|---|---|
| Content cut off at bottom | Use instead of |
| Notch overlaps content | Add / |
| Touch targets too small | Min 44×44px |
| Scroll locked | Check on body |
| Keyboard covers input | Use API |
| Janky scrolling | Use |
| Double-tap zoom | Add |
References
See
/references/ for detailed guides:
- Virtual keyboard and form UXkeyboard-handling.md
- Touch-friendly animationsanimations.md
- Mobile a11y requirementsaccessibility.md