--- name: mobile-components description: Mobile-first UI components including bottom navigation, bottom sheets, pull-to-refresh, and swipe actions. Touch-optimized with proper gesture handling. license: MIT compatibility: TypeScript/JavaScript, React metadata: category: frontend time: 3h source: drift-masterguide --- # Mobile Components Touch-optimized UI components for mobile-first experiences. ## When to Use This Skill - Building mobile-first or responsive web applications - Need native-feeling mobile interactions - Implementing bottom sheets, pull-to-refresh, or swipe actions - Desktop components feel awkward on mobile ## Core Concepts Mobile UX differs from desktop: bottom navigation is reachable, sheets slide up from bottom, touch targets need 44px minimum, and gestures replace clicks. ## Implementation ### TypeScript/React ```typescript // Bottom Navigation interface NavItem { href: string; label: string; icon: string; } function MobileNav({ items }: { items: NavItem[] }) { const pathname = usePathname(); return ( ); } // Bottom Sheet interface BottomSheetProps { isOpen: boolean; onClose: () => void; children: ReactNode; title?: string; } function BottomSheet({ isOpen, onClose, children, title }: BottomSheetProps) { const [dragY, setDragY] = useState(0); const [isDragging, setIsDragging] = useState(false); const startY = useRef(0); useEffect(() => { document.body.style.overflow = isOpen ? 'hidden' : ''; return () => { document.body.style.overflow = ''; }; }, [isOpen]); const handleTouchStart = (e: React.TouchEvent) => { startY.current = e.touches[0].clientY; setIsDragging(true); }; const handleTouchMove = (e: React.TouchEvent) => { if (!isDragging) return; const diff = e.touches[0].clientY - startY.current; if (diff > 0) setDragY(diff); }; const handleTouchEnd = () => { setIsDragging(false); if (dragY > 100) onClose(); setDragY(0); }; if (!isOpen) return null; return ( <>
{title && (

{title}

)}
{children}
); } // Pull to Refresh function PullToRefresh({ onRefresh, children }: { onRefresh: () => Promise; children: ReactNode; }) { const [isPulling, setIsPulling] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false); const [pullDistance, setPullDistance] = useState(0); const startY = useRef(0); const containerRef = useRef(null); const THRESHOLD = 80; const handleTouchStart = (e: React.TouchEvent) => { if (containerRef.current?.scrollTop === 0) { startY.current = e.touches[0].clientY; setIsPulling(true); } }; const handleTouchMove = (e: React.TouchEvent) => { if (!isPulling || isRefreshing) return; const diff = e.touches[0].clientY - startY.current; if (diff > 0) { setPullDistance(Math.min(diff * 0.5, THRESHOLD * 1.5)); } }; const handleTouchEnd = async () => { if (!isPulling) return; if (pullDistance >= THRESHOLD && !isRefreshing) { setIsRefreshing(true); setPullDistance(THRESHOLD); try { await onRefresh(); } finally { setIsRefreshing(false); } } setIsPulling(false); setPullDistance(0); }; return (
{isRefreshing ? (
) : (
)}
{children}
); } // Swipeable Row function SwipeableRow({ children, onSwipeLeft, onSwipeRight, leftAction, rightAction, }: { children: ReactNode; onSwipeLeft?: () => void; onSwipeRight?: () => void; leftAction?: ReactNode; rightAction?: ReactNode; }) { const [translateX, setTranslateX] = useState(0); const startX = useRef(0); const isDragging = useRef(false); const THRESHOLD = 80; const handleTouchStart = (e: React.TouchEvent) => { startX.current = e.touches[0].clientX; isDragging.current = true; }; const handleTouchMove = (e: React.TouchEvent) => { if (!isDragging.current) return; const diff = e.touches[0].clientX - startX.current; setTranslateX(Math.max(-100, Math.min(100, diff))); }; const handleTouchEnd = () => { isDragging.current = false; if (translateX > THRESHOLD) onSwipeRight?.(); else if (translateX < -THRESHOLD) onSwipeLeft?.(); setTranslateX(0); }; return (
{leftAction && (
{leftAction}
)} {rightAction && (
{rightAction}
)}
{children}
); } ``` ## Usage Examples ```typescript export default function DashboardPage() { const [sheetOpen, setSheetOpen] = useState(false); const handleRefresh = async () => { await fetchData(); }; return (
{items.map((item) => ( deleteItem(item.id)} rightAction={🗑️} > setSheetOpen(true)}> {item.title} ))}
setSheetOpen(false)} title="Details">

Sheet content here

); } ``` ## Best Practices 1. Minimum touch target: 44x44px (Apple) / 48x48dp (Google) 2. Use `safe-area-bottom` for bottom navigation on notched devices 3. Always provide visual feedback on touch (active states) 4. Lock body scroll when sheets are open 5. Use `touch-none` on drag handles to prevent scroll interference ## Common Mistakes - Touch targets too small (causes mis-taps) - Not handling safe areas (content hidden behind notch) - Missing active states (no touch feedback) - Forgetting to unlock body scroll on unmount - Not debouncing pull-to-refresh ## Related Patterns - design-tokens (consistent spacing/sizing) - pwa-setup (full mobile experience)