mobile-ux-optimizer

Mobile-First UX Optimization

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "mobile-ux-optimizer" with this command: npx skills add curiositech/some_claude_skills/curiositech-some-claude-skills-mobile-ux-optimizer

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

/* ❌ 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

Mobile 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 */}
  &#x3C;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"
  >
    &#x3C;div className="h-full overflow-y-auto pt-safe pb-safe">
      {children}
    &#x3C;/div>
  &#x3C;/div>
&#x3C;/div>,
document.body

); }

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:

// 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 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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

mobile-ux-optimizer

No summary provided by upstream source.

Repository SourceNeeds Review
General

video-processing-editing

No summary provided by upstream source.

Repository SourceNeeds Review
General

interior-design-expert

No summary provided by upstream source.

Repository SourceNeeds Review
General

project-management-guru-adhd

No summary provided by upstream source.

Repository SourceNeeds Review