Claude Code Plugins

Community-maintained marketplace

Feedback

Mobile UX patterns, touch interactions, gesture design, mobile-first principles, app navigation, and mobile performance

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name mobile-design
description Mobile UX patterns, touch interactions, gesture design, mobile-first principles, app navigation, and mobile performance
category design
tags mobile, ux, touch, gestures, navigation, mobile-first, ios, android
version 1.0.0

Mobile Design Skill

When to Use This Skill

Use this skill when working on:

  • Mobile-First Web Applications: Building responsive websites that prioritize mobile user experience
  • Native Mobile Apps: Designing iOS or Android applications with platform-specific patterns
  • Progressive Web Apps (PWAs): Creating app-like experiences in the browser
  • Hybrid Mobile Applications: Developing cross-platform apps using React Native, Flutter, or similar frameworks
  • Responsive Design Systems: Creating components that adapt seamlessly across devices
  • Touch-First Interfaces: Designing for touchscreen interactions rather than mouse/keyboard
  • Mobile E-commerce: Building shopping experiences optimized for small screens
  • Mobile Dashboards: Adapting data-heavy interfaces for mobile consumption
  • Gesture-Based Interfaces: Implementing swipe, pinch, and other touch gestures
  • Accessibility Audits: Ensuring mobile interfaces meet accessibility standards

This skill helps you create mobile experiences that feel native, perform well, and delight users on smartphones and tablets.

Core Concepts

Mobile-First Design Philosophy

Mobile-first design starts with the smallest screen and progressively enhances for larger devices:

Why Mobile-First?

  • Forces prioritization of essential content and features
  • Improves performance by default (lighter assets, simpler layouts)
  • Easier to scale up than scale down
  • Reflects actual user behavior (mobile traffic often exceeds desktop)
  • Ensures core functionality works on all devices

Mobile-First vs Desktop-First:

/* Mobile-First Approach (Recommended) */
/* Base styles for mobile */
.container {
  padding: 16px;
  font-size: 14px;
}

/* Tablet enhancements */
@media (min-width: 768px) {
  .container {
    padding: 24px;
    font-size: 16px;
  }
}

/* Desktop enhancements */
@media (min-width: 1024px) {
  .container {
    padding: 32px;
    max-width: 1200px;
    margin: 0 auto;
  }
}

/* Desktop-First Approach (Not Recommended) */
/* Base styles for desktop */
.container {
  padding: 32px;
  max-width: 1200px;
  margin: 0 auto;
  font-size: 16px;
}

/* Tablet overrides */
@media (max-width: 1023px) {
  .container {
    padding: 24px;
  }
}

/* Mobile overrides */
@media (max-width: 767px) {
  .container {
    padding: 16px;
    font-size: 14px;
  }
}

Touch Targets and Ergonomics

Minimum Touch Target Sizes:

  • Apple: 44×44 points (iOS Human Interface Guidelines)
  • Google: 48×48 dp (Material Design)
  • Microsoft: 40×40 pixels (Windows Phone)
  • Recommended: 48×48 pixels minimum, 56×56 pixels optimal

Touch Target Spacing:

  • Minimum 8px spacing between interactive elements
  • Optimal 12-16px spacing for frequently used controls
  • Edge-to-edge buttons can touch if they're different types (e.g., cancel vs confirm)

Thumb Zones:

Mobile screens have three ergonomic zones:

  1. Easy Zone (Green): Bottom third, center - easiest to reach with thumb
  2. Stretch Zone (Yellow): Middle area - requires slight reach
  3. Difficult Zone (Red): Top corners - hardest to reach one-handed

Design Implications:

  • Place primary actions in the easy zone (bottom center)
  • Put destructive actions in difficult zones (top corners)
  • Navigation typically at top or bottom, never middle
  • Consider both left-handed and right-handed users
// React Native: Bottom-aligned primary action (easy zone)
<View style={styles.container}>
  <ScrollView style={styles.content}>
    {/* Main content */}
  </ScrollView>

  <View style={styles.bottomActions}>
    <TouchableOpacity style={styles.primaryButton}>
      <Text>Continue</Text>
    </TouchableOpacity>
  </View>
</View>

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  content: {
    flex: 1,
  },
  bottomActions: {
    padding: 16,
    paddingBottom: 32, // Extra padding for iPhone home indicator
    backgroundColor: '#fff',
    borderTopWidth: 1,
    borderTopColor: '#e0e0e0',
  },
  primaryButton: {
    height: 56, // Optimal touch target
    borderRadius: 28,
    backgroundColor: '#007AFF',
    justifyContent: 'center',
    alignItems: 'center',
  },
});

Viewport and Screen Considerations

Common Mobile Breakpoints:

/* Extra small devices (phones, 320px - 479px) */
@media (min-width: 320px) { }

/* Small devices (large phones, 480px - 767px) */
@media (min-width: 480px) { }

/* Medium devices (tablets, 768px - 1023px) */
@media (min-width: 768px) { }

/* Large devices (small laptops, 1024px - 1279px) */
@media (min-width: 1024px) { }

/* Extra large devices (desktops, 1280px and up) */
@media (min-width: 1280px) { }

Viewport Meta Tag:

<!-- Responsive viewport (required for mobile) -->
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5, user-scalable=yes">

<!-- PWA with standalone mode -->
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">

Safe Areas (iPhone X and later):

/* Account for notch and home indicator */
.header {
  padding-top: env(safe-area-inset-top);
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}

.footer {
  padding-bottom: env(safe-area-inset-bottom);
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}

Touch Interactions

Tap (Primary Interaction)

Single Tap:

  • Primary action on buttons, links, list items
  • Should provide immediate visual feedback (0-100ms delay)
  • Minimum size: 48×48 pixels
// React: Tap with visual feedback
import { useState } from 'react';

function TapButton({ onPress, children }) {
  const [isPressed, setIsPressed] = useState(false);

  return (
    <button
      className={`tap-button ${isPressed ? 'pressed' : ''}`}
      onTouchStart={() => setIsPressed(true)}
      onTouchEnd={() => setIsPressed(false)}
      onTouchCancel={() => setIsPressed(false)}
      onClick={onPress}
    >
      {children}
    </button>
  );
}

// CSS
.tap-button {
  padding: 16px 24px;
  background: #007AFF;
  color: white;
  border: none;
  border-radius: 8px;
  font-size: 16px;
  min-height: 48px;
  transition: transform 0.1s, background 0.1s;
  -webkit-tap-highlight-color: transparent;
}

.tap-button.pressed {
  transform: scale(0.96);
  background: #0051D5;
}

.tap-button:active {
  transform: scale(0.96);
}

Double Tap:

  • Zoom in/out (maps, images)
  • Like/favorite (Instagram, Twitter)
  • Less common, use sparingly

iOS Double-Tap Zoom Prevention:

/* Prevent double-tap zoom while allowing pinch zoom */
touch-action: manipulation;

Swipe Gestures

Horizontal Swipe:

  • Navigate between screens/pages
  • Reveal actions (swipe-to-delete, swipe-to-archive)
  • Dismiss cards/modals
  • Switch tabs
// React: Swipeable list item
import { useState } from 'react';

function SwipeableListItem({ children, onDelete, onArchive }) {
  const [touchStart, setTouchStart] = useState(null);
  const [touchEnd, setTouchEnd] = useState(null);
  const [translateX, setTranslateX] = useState(0);

  const minSwipeDistance = 50;

  const onTouchStart = (e) => {
    setTouchEnd(null);
    setTouchStart(e.targetTouches[0].clientX);
  };

  const onTouchMove = (e) => {
    setTouchEnd(e.targetTouches[0].clientX);
    const distance = touchStart - e.targetTouches[0].clientX;
    setTranslateX(-distance);
  };

  const onTouchEnd = () => {
    if (!touchStart || !touchEnd) return;

    const distance = touchStart - touchEnd;
    const isLeftSwipe = distance > minSwipeDistance;
    const isRightSwipe = distance < -minSwipeDistance;

    if (isLeftSwipe) {
      setTranslateX(-80); // Show actions
    } else if (isRightSwipe) {
      setTranslateX(0); // Reset
    } else {
      setTranslateX(0); // Snap back
    }
  };

  return (
    <div className="swipeable-item-container">
      <div className="swipe-actions">
        <button onClick={onArchive} className="archive-btn">Archive</button>
        <button onClick={onDelete} className="delete-btn">Delete</button>
      </div>

      <div
        className="swipeable-item"
        style={{ transform: `translateX(${translateX}px)` }}
        onTouchStart={onTouchStart}
        onTouchMove={onTouchMove}
        onTouchEnd={onTouchEnd}
      >
        {children}
      </div>
    </div>
  );
}

Vertical Swipe:

  • Pull to refresh (downward swipe from top)
  • Scroll content
  • Dismiss bottom sheets/modals (downward swipe)
// Pull to Refresh
function PullToRefresh({ onRefresh, children }) {
  const [pulling, setPulling] = useState(false);
  const [pullDistance, setPullDistance] = useState(0);
  const threshold = 80;

  const handleTouchStart = (e) => {
    if (window.scrollY === 0) {
      setPulling(true);
    }
  };

  const handleTouchMove = (e) => {
    if (pulling && window.scrollY === 0) {
      const distance = e.touches[0].clientY - e.touches[0].target.getBoundingClientRect().top;
      setPullDistance(Math.min(distance, threshold * 1.5));
    }
  };

  const handleTouchEnd = () => {
    if (pullDistance >= threshold) {
      onRefresh();
    }
    setPulling(false);
    setPullDistance(0);
  };

  return (
    <div
      onTouchStart={handleTouchStart}
      onTouchMove={handleTouchMove}
      onTouchEnd={handleTouchEnd}
    >
      {pullDistance > 0 && (
        <div className="pull-indicator" style={{ height: pullDistance }}>
          {pullDistance >= threshold ? '↻ Release to refresh' : '↓ Pull to refresh'}
        </div>
      )}
      {children}
    </div>
  );
}

Pinch and Spread (Zoom)

Used for:

  • Image galleries
  • Maps
  • PDF viewers
  • Any zoomable content
// React: Pinch to Zoom
function PinchZoomImage({ src, alt }) {
  const [scale, setScale] = useState(1);
  const [lastScale, setLastScale] = useState(1);

  const handleTouchMove = (e) => {
    if (e.touches.length === 2) {
      e.preventDefault();

      const touch1 = e.touches[0];
      const touch2 = e.touches[1];

      const distance = Math.hypot(
        touch1.clientX - touch2.clientX,
        touch1.clientY - touch2.clientY
      );

      if (lastDistance) {
        const newScale = lastScale * (distance / lastDistance);
        setScale(Math.max(1, Math.min(newScale, 4))); // Limit 1x to 4x
      }

      lastDistance = distance;
    }
  };

  const handleTouchEnd = () => {
    setLastScale(scale);
    lastDistance = null;
  };

  let lastDistance = null;

  return (
    <div
      className="pinch-zoom-container"
      onTouchMove={handleTouchMove}
      onTouchEnd={handleTouchEnd}
    >
      <img
        src={src}
        alt={alt}
        style={{
          transform: `scale(${scale})`,
          transition: lastDistance ? 'none' : 'transform 0.2s',
        }}
      />
    </div>
  );
}

Long Press

Used for:

  • Context menus
  • Item selection mode
  • Drag-and-drop initiation
  • Additional options
// React: Long Press Handler
function useLongPress(callback, ms = 500) {
  const [startLongPress, setStartLongPress] = useState(false);

  useEffect(() => {
    let timerId;
    if (startLongPress) {
      timerId = setTimeout(callback, ms);
    } else {
      clearTimeout(timerId);
    }

    return () => {
      clearTimeout(timerId);
    };
  }, [startLongPress, callback, ms]);

  return {
    onTouchStart: () => setStartLongPress(true),
    onTouchEnd: () => setStartLongPress(false),
    onTouchMove: () => setStartLongPress(false),
  };
}

// Usage
function LongPressItem({ item }) {
  const longPressProps = useLongPress(() => {
    console.log('Long press detected!');
    // Show context menu
  }, 500);

  return (
    <div {...longPressProps} className="long-press-item">
      {item.name}
    </div>
  );
}

Drag and Drop

// React Native: Drag and Drop
import { PanResponder, Animated } from 'react-native';

function DraggableCard({ children }) {
  const pan = useRef(new Animated.ValueXY()).current;

  const panResponder = useRef(
    PanResponder.create({
      onStartShouldSetPanResponder: () => true,
      onPanResponderGrant: () => {
        pan.setOffset({
          x: pan.x._value,
          y: pan.y._value,
        });
      },
      onPanResponderMove: Animated.event(
        [null, { dx: pan.x, dy: pan.y }],
        { useNativeDriver: false }
      ),
      onPanResponderRelease: () => {
        pan.flattenOffset();
        Animated.spring(pan, {
          toValue: { x: 0, y: 0 },
          useNativeDriver: true,
        }).start();
      },
    })
  ).current;

  return (
    <Animated.View
      {...panResponder.panHandlers}
      style={{
        transform: [{ translateX: pan.x }, { translateY: pan.y }],
      }}
    >
      {children}
    </Animated.View>
  );
}

Navigation Patterns

Tab Bar Navigation

Bottom Tab Bar (iOS standard, Android common):

// React Native: Bottom Tab Navigation
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import Icon from 'react-native-vector-icons/Ionicons';

const Tab = createBottomTabNavigator();

function AppNavigator() {
  return (
    <Tab.Navigator
      screenOptions={({ route }) => ({
        tabBarIcon: ({ focused, color, size }) => {
          let iconName;

          if (route.name === 'Home') {
            iconName = focused ? 'home' : 'home-outline';
          } else if (route.name === 'Search') {
            iconName = focused ? 'search' : 'search-outline';
          } else if (route.name === 'Profile') {
            iconName = focused ? 'person' : 'person-outline';
          }

          return <Icon name={iconName} size={size} color={color} />;
        },
        tabBarActiveTintColor: '#007AFF',
        tabBarInactiveTintColor: '#8E8E93',
        tabBarStyle: {
          height: 88, // Account for safe area
          paddingBottom: 34, // iPhone home indicator
        },
      })}
    >
      <Tab.Screen name="Home" component={HomeScreen} />
      <Tab.Screen name="Search" component={SearchScreen} />
      <Tab.Screen name="Profile" component={ProfileScreen} />
    </Tab.Navigator>
  );
}

Best Practices:

  • 3-5 tabs maximum
  • Always show labels (don't rely on icons alone)
  • Highlight active tab clearly
  • Keep tabs visible at all times
  • Most important section on the left (for LTR languages)

Hamburger Menu (Drawer Navigation)

// React Native: Drawer Navigation
import { createDrawerNavigator } from '@react-navigation/drawer';

const Drawer = createDrawerNavigator();

function DrawerNavigator() {
  return (
    <Drawer.Navigator
      screenOptions={{
        drawerPosition: 'left',
        drawerType: 'slide',
        drawerStyle: {
          width: 280,
        },
        headerShown: true,
      }}
    >
      <Drawer.Screen
        name="Home"
        component={HomeScreen}
        options={{
          drawerIcon: ({ color, size }) => (
            <Icon name="home-outline" color={color} size={size} />
          ),
        }}
      />
      <Drawer.Screen
        name="Settings"
        component={SettingsScreen}
        options={{
          drawerIcon: ({ color, size }) => (
            <Icon name="settings-outline" color={color} size={size} />
          ),
        }}
      />
    </Drawer.Navigator>
  );
}

When to Use:

  • Secondary navigation
  • Many navigation options (6+)
  • Infrequently accessed features
  • Settings and account options

Avoid When:

  • Primary navigation is needed
  • User needs quick access to all sections
  • You have 5 or fewer main sections (use tabs instead)

Bottom Sheets and Modals

Bottom Sheet (Material Design):

// React: Bottom Sheet
function BottomSheet({ isOpen, onClose, children }) {
  const [startY, setStartY] = useState(0);
  const [currentY, setCurrentY] = useState(0);

  const handleTouchStart = (e) => {
    setStartY(e.touches[0].clientY);
  };

  const handleTouchMove = (e) => {
    const delta = e.touches[0].clientY - startY;
    if (delta > 0) { // Only allow downward drag
      setCurrentY(delta);
    }
  };

  const handleTouchEnd = () => {
    if (currentY > 100) { // Threshold for closing
      onClose();
    }
    setCurrentY(0);
  };

  if (!isOpen) return null;

  return (
    <>
      <div className="bottom-sheet-backdrop" onClick={onClose} />
      <div
        className="bottom-sheet"
        style={{ transform: `translateY(${currentY}px)` }}
        onTouchStart={handleTouchStart}
        onTouchMove={handleTouchMove}
        onTouchEnd={handleTouchEnd}
      >
        <div className="bottom-sheet-handle" />
        <div className="bottom-sheet-content">
          {children}
        </div>
      </div>
    </>
  );
}

// CSS
.bottom-sheet-backdrop {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  z-index: 999;
}

.bottom-sheet {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  background: white;
  border-radius: 20px 20px 0 0;
  padding: 16px;
  max-height: 80vh;
  z-index: 1000;
  transition: transform 0.3s;
}

.bottom-sheet-handle {
  width: 40px;
  height: 4px;
  background: #D1D1D6;
  border-radius: 2px;
  margin: 8px auto 16px;
}

Full-Screen Modal:

// iOS-style modal with slide-up animation
function Modal({ isOpen, onClose, children }) {
  if (!isOpen) return null;

  return (
    <div className="modal-overlay">
      <div className="modal-container">
        <div className="modal-header">
          <button onClick={onClose} className="modal-close">
            Done
          </button>
        </div>
        <div className="modal-content">
          {children}
        </div>
      </div>
    </div>
  );
}

// CSS
.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  z-index: 1000;
  animation: fadeIn 0.3s;
}

.modal-container {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: white;
  animation: slideUp 0.3s;
}

@keyframes slideUp {
  from {
    transform: translateY(100%);
  }
  to {
    transform: translateY(0);
  }
}

Stack Navigation

// React Navigation: Stack Navigator
import { createStackNavigator } from '@react-navigation/stack';

const Stack = createStackNavigator();

function StackNavigator() {
  return (
    <Stack.Navigator
      screenOptions={{
        headerStyle: {
          backgroundColor: '#007AFF',
        },
        headerTintColor: '#fff',
        headerTitleStyle: {
          fontWeight: 'bold',
        },
        cardStyleInterpolator: ({ current, layouts }) => {
          return {
            cardStyle: {
              transform: [
                {
                  translateX: current.progress.interpolate({
                    inputRange: [0, 1],
                    outputRange: [layouts.screen.width, 0],
                  }),
                },
              ],
            },
          };
        },
      }}
    >
      <Stack.Screen name="List" component={ListScreen} />
      <Stack.Screen name="Details" component={DetailsScreen} />
      <Stack.Screen name="Edit" component={EditScreen} />
    </Stack.Navigator>
  );
}

Mobile UI Components

Cards

Material Design Card:

function Card({ image, title, subtitle, description, actions }) {
  return (
    <div className="card">
      {image && (
        <div className="card-media">
          <img src={image} alt={title} />
        </div>
      )}

      <div className="card-content">
        <h3 className="card-title">{title}</h3>
        {subtitle && <p className="card-subtitle">{subtitle}</p>}
        <p className="card-description">{description}</p>
      </div>

      {actions && (
        <div className="card-actions">
          {actions}
        </div>
      )}
    </div>
  );
}

// CSS
.card {
  background: white;
  border-radius: 12px;
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  margin-bottom: 16px;
}

.card-media img {
  width: 100%;
  height: 200px;
  object-fit: cover;
}

.card-content {
  padding: 16px;
}

.card-title {
  font-size: 20px;
  font-weight: 600;
  margin: 0 0 8px 0;
}

.card-subtitle {
  font-size: 14px;
  color: #666;
  margin: 0 0 8px 0;
}

.card-description {
  font-size: 14px;
  line-height: 1.5;
  color: #333;
}

.card-actions {
  padding: 8px 16px 16px;
  display: flex;
  gap: 8px;
}

Lists

iOS-Style List:

function IOSList({ items, onItemPress }) {
  return (
    <div className="ios-list">
      {items.map((item, index) => (
        <div
          key={item.id}
          className="ios-list-item"
          onClick={() => onItemPress(item)}
        >
          {item.icon && (
            <div className="ios-list-icon">{item.icon}</div>
          )}

          <div className="ios-list-content">
            <div className="ios-list-title">{item.title}</div>
            {item.subtitle && (
              <div className="ios-list-subtitle">{item.subtitle}</div>
            )}
          </div>

          {item.badge && (
            <div className="ios-list-badge">{item.badge}</div>
          )}

          <div className="ios-list-chevron">›</div>
        </div>
      ))}
    </div>
  );
}

// CSS
.ios-list {
  background: white;
  border-radius: 12px;
  overflow: hidden;
}

.ios-list-item {
  display: flex;
  align-items: center;
  padding: 12px 16px;
  min-height: 56px;
  border-bottom: 0.5px solid #E5E5EA;
  -webkit-tap-highlight-color: transparent;
}

.ios-list-item:active {
  background: #F2F2F7;
}

.ios-list-item:last-child {
  border-bottom: none;
}

.ios-list-icon {
  width: 32px;
  height: 32px;
  margin-right: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.ios-list-content {
  flex: 1;
}

.ios-list-title {
  font-size: 17px;
  color: #000;
}

.ios-list-subtitle {
  font-size: 15px;
  color: #8E8E93;
  margin-top: 2px;
}

.ios-list-badge {
  background: #FF3B30;
  color: white;
  font-size: 13px;
  font-weight: 600;
  padding: 2px 8px;
  border-radius: 12px;
  margin-right: 8px;
}

.ios-list-chevron {
  font-size: 24px;
  color: #C7C7CC;
}

Forms

Mobile-Optimized Form:

function MobileForm() {
  return (
    <form className="mobile-form">
      <div className="form-group">
        <label htmlFor="email">Email</label>
        <input
          id="email"
          type="email"
          inputMode="email"
          autoComplete="email"
          placeholder="you@example.com"
        />
      </div>

      <div className="form-group">
        <label htmlFor="phone">Phone</label>
        <input
          id="phone"
          type="tel"
          inputMode="tel"
          autoComplete="tel"
          placeholder="(555) 123-4567"
        />
      </div>

      <div className="form-group">
        <label htmlFor="amount">Amount</label>
        <input
          id="amount"
          type="number"
          inputMode="decimal"
          placeholder="0.00"
        />
      </div>

      <button type="submit" className="submit-button">
        Submit
      </button>
    </form>
  );
}

// CSS
.mobile-form {
  padding: 16px;
}

.form-group {
  margin-bottom: 24px;
}

.form-group label {
  display: block;
  font-size: 14px;
  font-weight: 600;
  margin-bottom: 8px;
  color: #333;
}

.form-group input {
  width: 100%;
  height: 56px;
  padding: 16px;
  font-size: 16px; /* Prevents zoom on iOS */
  border: 2px solid #E5E5EA;
  border-radius: 12px;
  background: white;
  -webkit-appearance: none;
}

.form-group input:focus {
  outline: none;
  border-color: #007AFF;
}

.submit-button {
  width: 100%;
  height: 56px;
  background: #007AFF;
  color: white;
  border: none;
  border-radius: 12px;
  font-size: 17px;
  font-weight: 600;
}

Input Types for Mobile Keyboards:

<!-- Email keyboard -->
<input type="email" inputmode="email">

<!-- Numeric keyboard -->
<input type="number" inputmode="numeric">

<!-- Decimal keyboard (includes . and ,) -->
<input type="number" inputmode="decimal">

<!-- Telephone keyboard -->
<input type="tel" inputmode="tel">

<!-- URL keyboard (includes .com, /, etc.) -->
<input type="url" inputmode="url">

<!-- Search keyboard (includes search button) -->
<input type="search" inputmode="search">

Action Sheets

// iOS-style Action Sheet
function ActionSheet({ isOpen, onClose, title, options }) {
  if (!isOpen) return null;

  return (
    <>
      <div className="action-sheet-backdrop" onClick={onClose} />
      <div className="action-sheet">
        {title && <div className="action-sheet-title">{title}</div>}

        <div className="action-sheet-options">
          {options.map((option, index) => (
            <button
              key={index}
              className={`action-sheet-option ${option.destructive ? 'destructive' : ''}`}
              onClick={() => {
                option.onPress();
                onClose();
              }}
            >
              {option.icon && <span className="option-icon">{option.icon}</span>}
              {option.label}
            </button>
          ))}
        </div>

        <button className="action-sheet-cancel" onClick={onClose}>
          Cancel
        </button>
      </div>
    </>
  );
}

// CSS
.action-sheet {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  background: transparent;
  z-index: 1001;
  padding: 8px;
  animation: slideUp 0.3s;
}

.action-sheet-title {
  background: rgba(255, 255, 255, 0.95);
  padding: 16px;
  text-align: center;
  border-radius: 14px 14px 0 0;
  font-size: 13px;
  color: #8E8E93;
}

.action-sheet-options {
  background: rgba(255, 255, 255, 0.95);
  border-radius: 14px;
  overflow: hidden;
  margin-bottom: 8px;
}

.action-sheet-option {
  width: 100%;
  padding: 16px;
  background: transparent;
  border: none;
  border-bottom: 0.5px solid #E5E5EA;
  font-size: 20px;
  color: #007AFF;
  -webkit-tap-highlight-color: transparent;
}

.action-sheet-option:active {
  background: rgba(0, 0, 0, 0.05);
}

.action-sheet-option.destructive {
  color: #FF3B30;
}

.action-sheet-cancel {
  width: 100%;
  padding: 16px;
  background: rgba(255, 255, 255, 0.95);
  border: none;
  border-radius: 14px;
  font-size: 20px;
  font-weight: 600;
  color: #007AFF;
}

Platform Conventions

iOS Human Interface Guidelines

Navigation Bar:

  • Height: 44pt (plus status bar)
  • Large title: 52pt collapsible header
  • Back button always shows previous screen title
  • Right-aligned action buttons

Tab Bar:

  • Height: 49pt (plus safe area)
  • 5 tabs maximum
  • Badge notifications on tab icons
  • Selected tab uses accent color

Typography:

  • SF Pro (system font)
  • Dynamic Type support required
  • Font sizes: 11pt to 34pt
  • Weight hierarchy: Regular, Medium, Semibold, Bold

Colors:

  • System colors adapt to light/dark mode
  • Blue (#007AFF) for tappable elements
  • Red (#FF3B30) for destructive actions
  • Semantic colors: label, secondaryLabel, tertiaryLabel

Spacing:

  • Minimum margins: 16pt
  • Standard spacing: 8pt, 16pt, 24pt, 32pt
  • Component padding: 16pt horizontal, 12pt vertical
// SwiftUI: iOS Navigation
struct ContentView: View {
    var body: some View {
        NavigationView {
            List(items) { item in
                NavigationLink(destination: DetailView(item: item)) {
                    HStack {
                        Image(systemName: item.icon)
                            .foregroundColor(.accentColor)

                        VStack(alignment: .leading) {
                            Text(item.title)
                                .font(.headline)
                            Text(item.subtitle)
                                .font(.subheadline)
                                .foregroundColor(.secondary)
                        }
                    }
                    .padding(.vertical, 8)
                }
            }
            .navigationTitle("Items")
            .navigationBarTitleDisplayMode(.large)
        }
    }
}

Material Design (Android)

App Bar:

  • Height: 56dp (64dp for tablets)
  • Elevation: 4dp
  • Hamburger icon or back arrow on left
  • Title centered or left-aligned
  • Action icons on right (max 3)

Bottom Navigation:

  • Height: 56dp
  • 3-5 destinations
  • Icons with text labels
  • Active indicator

FAB (Floating Action Button):

  • Size: 56×56dp (regular), 40×40dp (mini)
  • Position: 16dp from edges
  • Primary action only
  • Extended FAB includes text label

Typography:

  • Roboto font family
  • Scale: 12sp to 96sp
  • Line height: 1.5× font size
  • Letter spacing varies by size

Elevation:

  • Shadow depth indicates hierarchy
  • 0dp: flat surface
  • 1-8dp: raised components
  • 16-24dp: modals and dialogs

Spacing:

  • 4dp grid system
  • Keylines: 16dp, 72dp from edges
  • Component spacing: 8dp, 16dp, 24dp
// Jetpack Compose: Material Design
@Composable
fun MaterialCard(item: Item) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp),
        elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(
                text = item.title,
                style = MaterialTheme.typography.headlineSmall
            )

            Spacer(modifier = Modifier.height(8.dp))

            Text(
                text = item.description,
                style = MaterialTheme.typography.bodyMedium,
                color = MaterialTheme.colorScheme.onSurfaceVariant
            )

            Spacer(modifier = Modifier.height(16.dp))

            Row(
                modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.End
            ) {
                TextButton(onClick = { /* Action */ }) {
                    Text("ACTION")
                }
            }
        }
    }
}

Accessibility

Touch Target Sizes

WCAG 2.1 Level AAA:

  • Minimum: 44×44 pixels
  • Recommended: 48×48 pixels or larger
  • Spacing: 8px minimum between targets
// Accessible button component
function AccessibleButton({ children, onPress, variant = 'primary' }) {
  return (
    <button
      className={`accessible-button ${variant}`}
      onClick={onPress}
      style={{
        minWidth: '48px',
        minHeight: '48px',
        padding: '12px 24px',
      }}
    >
      {children}
    </button>
  );
}

Screen Reader Support

Semantic HTML:

function AccessibleMobileNav() {
  return (
    <nav role="navigation" aria-label="Main navigation">
      <ul>
        <li>
          <a href="/home" aria-current="page">
            <Icon name="home" aria-hidden="true" />
            <span>Home</span>
          </a>
        </li>
        <li>
          <a href="/search">
            <Icon name="search" aria-hidden="true" />
            <span>Search</span>
          </a>
        </li>
      </ul>
    </nav>
  );
}

React Native Accessibility:

import { View, Text, TouchableOpacity } from 'react-native';

function AccessibleCard({ title, description, onPress }) {
  return (
    <TouchableOpacity
      accessible={true}
      accessibilityLabel={`${title}. ${description}`}
      accessibilityRole="button"
      accessibilityHint="Double tap to view details"
      onPress={onPress}
    >
      <View>
        <Text>{title}</Text>
        <Text>{description}</Text>
      </View>
    </TouchableOpacity>
  );
}

Color Contrast

WCAG AA Requirements:

  • Normal text: 4.5:1 contrast ratio
  • Large text (18pt+): 3:1 contrast ratio
  • UI components: 3:1 contrast ratio
/* Good contrast examples */
.primary-button {
  background: #0066CC; /* Blue */
  color: #FFFFFF; /* White - 6.4:1 ratio */
}

.secondary-button {
  background: #FFFFFF; /* White */
  color: #333333; /* Dark gray - 12.6:1 ratio */
  border: 2px solid #333333;
}

/* Bad contrast (avoid) */
.bad-button {
  background: #FFCC00; /* Yellow */
  color: #FFFFFF; /* White - 1.4:1 ratio ❌ */
}

Focus Indicators

/* Visible focus states for keyboard navigation */
button:focus-visible {
  outline: 3px solid #007AFF;
  outline-offset: 2px;
}

input:focus-visible {
  border-color: #007AFF;
  box-shadow: 0 0 0 4px rgba(0, 122, 255, 0.2);
}

/* Remove default focus ring, add custom */
*:focus {
  outline: none;
}

*:focus-visible {
  outline: 3px solid #007AFF;
  outline-offset: 2px;
}

Performance

Image Optimization

Responsive Images:

<!-- Serve different sizes based on screen width -->
<img
  src="image-800w.jpg"
  srcset="
    image-400w.jpg 400w,
    image-800w.jpg 800w,
    image-1200w.jpg 1200w
  "
  sizes="
    (max-width: 480px) 100vw,
    (max-width: 768px) 50vw,
    33vw
  "
  alt="Product image"
  loading="lazy"
>

<!-- WebP with fallback -->
<picture>
  <source srcset="image.webp" type="image/webp">
  <source srcset="image.jpg" type="image/jpeg">
  <img src="image.jpg" alt="Fallback image">
</picture>

Lazy Loading:

// React: Intersection Observer for lazy loading
function LazyImage({ src, alt }) {
  const [isLoaded, setIsLoaded] = useState(false);
  const [isInView, setIsInView] = useState(false);
  const imgRef = useRef();

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsInView(true);
          observer.disconnect();
        }
      },
      { rootMargin: '50px' }
    );

    if (imgRef.current) {
      observer.observe(imgRef.current);
    }

    return () => observer.disconnect();
  }, []);

  return (
    <div ref={imgRef} className="lazy-image-container">
      {!isLoaded && <div className="skeleton-loader" />}
      {isInView && (
        <img
          src={src}
          alt={alt}
          onLoad={() => setIsLoaded(true)}
          style={{ opacity: isLoaded ? 1 : 0 }}
        />
      )}
    </div>
  );
}

Loading Strategies

Skeleton Screens:

function SkeletonCard() {
  return (
    <div className="skeleton-card">
      <div className="skeleton skeleton-image" />
      <div className="skeleton skeleton-title" />
      <div className="skeleton skeleton-text" />
      <div className="skeleton skeleton-text short" />
    </div>
  );
}

// CSS
.skeleton {
  background: linear-gradient(
    90deg,
    #f0f0f0 25%,
    #e0e0e0 50%,
    #f0f0f0 75%
  );
  background-size: 200% 100%;
  animation: loading 1.5s infinite;
}

@keyframes loading {
  0% {
    background-position: 200% 0;
  }
  100% {
    background-position: -200% 0;
  }
}

.skeleton-image {
  height: 200px;
  border-radius: 8px 8px 0 0;
}

.skeleton-title {
  height: 24px;
  margin: 16px;
  border-radius: 4px;
}

.skeleton-text {
  height: 16px;
  margin: 8px 16px;
  border-radius: 4px;
}

.skeleton-text.short {
  width: 60%;
}

Progressive Web App (PWA):

// service-worker.js
const CACHE_NAME = 'mobile-app-v1';
const urlsToCache = [
  '/',
  '/styles/main.css',
  '/scripts/main.js',
  '/images/logo.png',
];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then((cache) => cache.addAll(urlsToCache))
  );
});

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request)
      .then((response) => {
        // Return cached version or fetch new
        return response || fetch(event.request);
      })
  );
});

Performance Metrics

Core Web Vitals for Mobile:

  • LCP (Largest Contentful Paint): < 2.5s
  • FID (First Input Delay): < 100ms
  • CLS (Cumulative Layout Shift): < 0.1
// Measure performance
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(entry.name, entry.startTime);
  }
});

observer.observe({ entryTypes: ['navigation', 'paint', 'largest-contentful-paint'] });

Responsive Breakpoints

Common Device Widths

/* iPhone SE (2022) */
@media (min-width: 375px) and (max-width: 667px) {
  /* Small phone styles */
}

/* iPhone 12/13/14 Pro */
@media (min-width: 390px) and (max-width: 844px) {
  /* Standard phone styles */
}

/* iPhone 14 Pro Max */
@media (min-width: 428px) and (max-width: 926px) {
  /* Large phone styles */
}

/* iPad Mini */
@media (min-width: 768px) and (max-width: 1024px) {
  /* Tablet styles */
}

/* iPad Pro */
@media (min-width: 1024px) and (max-width: 1366px) {
  /* Large tablet styles */
}

Orientation-Specific Styles

/* Portrait mode */
@media (orientation: portrait) {
  .container {
    flex-direction: column;
  }
}

/* Landscape mode */
@media (orientation: landscape) {
  .container {
    flex-direction: row;
  }

  .sidebar {
    width: 300px;
  }
}

/* Prevent layout shift on keyboard open */
@media (max-height: 500px) {
  .bottom-nav {
    display: none;
  }
}

Container Queries (Modern Approach)

/* Component adapts to container size, not viewport */
.card-container {
  container-type: inline-size;
}

@container (min-width: 400px) {
  .card {
    display: grid;
    grid-template-columns: 1fr 2fr;
  }
}

@container (min-width: 600px) {
  .card {
    grid-template-columns: 1fr 1fr;
  }
}

Examples

1. Mobile E-commerce Product List

function ProductList({ products }) {
  return (
    <div className="product-list">
      {products.map((product) => (
        <div key={product.id} className="product-card">
          <div className="product-image-container">
            <img
              src={product.image}
              alt={product.name}
              loading="lazy"
            />
            {product.badge && (
              <span className="product-badge">{product.badge}</span>
            )}
          </div>

          <div className="product-info">
            <h3 className="product-name">{product.name}</h3>
            <p className="product-price">${product.price}</p>

            {product.rating && (
              <div className="product-rating">
                {'★'.repeat(product.rating)}
                {'☆'.repeat(5 - product.rating)}
                <span className="review-count">
                  ({product.reviewCount})
                </span>
              </div>
            )}
          </div>

          <button className="add-to-cart-btn">
            Add to Cart
          </button>
        </div>
      ))}
    </div>
  );
}

// CSS
.product-list {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 16px;
  padding: 16px;
}

@media (min-width: 768px) {
  .product-list {
    grid-template-columns: repeat(3, 1fr);
  }
}

@media (min-width: 1024px) {
  .product-list {
    grid-template-columns: repeat(4, 1fr);
  }
}

.product-card {
  background: white;
  border-radius: 12px;
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.product-image-container {
  position: relative;
  aspect-ratio: 1;
  background: #f5f5f5;
}

.product-image-container img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.product-badge {
  position: absolute;
  top: 8px;
  right: 8px;
  background: #FF3B30;
  color: white;
  padding: 4px 8px;
  border-radius: 4px;
  font-size: 12px;
  font-weight: 600;
}

.product-info {
  padding: 12px;
}

.product-name {
  font-size: 14px;
  font-weight: 600;
  margin: 0 0 4px 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.product-price {
  font-size: 16px;
  font-weight: 700;
  color: #007AFF;
  margin: 0 0 8px 0;
}

.product-rating {
  font-size: 14px;
  color: #FFB800;
}

.review-count {
  color: #666;
  font-size: 12px;
  margin-left: 4px;
}

.add-to-cart-btn {
  width: 100%;
  height: 44px;
  background: #007AFF;
  color: white;
  border: none;
  font-size: 14px;
  font-weight: 600;
  -webkit-tap-highlight-color: transparent;
}

.add-to-cart-btn:active {
  background: #0051D5;
}

2. Infinite Scroll Feed

function InfiniteFeed() {
  const [posts, setPosts] = useState([]);
  const [page, setPage] = useState(1);
  const [loading, setLoading] = useState(false);
  const [hasMore, setHasMore] = useState(true);
  const observerTarget = useRef(null);

  const loadMore = async () => {
    if (loading || !hasMore) return;

    setLoading(true);
    const newPosts = await fetchPosts(page);

    if (newPosts.length === 0) {
      setHasMore(false);
    } else {
      setPosts(prev => [...prev, ...newPosts]);
      setPage(prev => prev + 1);
    }

    setLoading(false);
  };

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting) {
          loadMore();
        }
      },
      { threshold: 0.5 }
    );

    if (observerTarget.current) {
      observer.observe(observerTarget.current);
    }

    return () => observer.disconnect();
  }, [loading, hasMore]);

  return (
    <div className="feed">
      {posts.map(post => (
        <FeedCard key={post.id} post={post} />
      ))}

      {loading && <LoadingSpinner />}

      <div ref={observerTarget} style={{ height: '20px' }} />

      {!hasMore && (
        <div className="feed-end">No more posts</div>
      )}
    </div>
  );
}

3. Mobile Search with Autocomplete

function MobileSearch() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isFocused, setIsFocused] = useState(false);
  const [recentSearches, setRecentSearches] = useState([]);

  const handleSearch = async (value) => {
    setQuery(value);

    if (value.length >= 2) {
      const searchResults = await fetchSearchResults(value);
      setResults(searchResults);
    } else {
      setResults([]);
    }
  };

  const handleSubmit = (searchQuery) => {
    // Save to recent searches
    const updated = [searchQuery, ...recentSearches.slice(0, 4)];
    setRecentSearches(updated);
    localStorage.setItem('recentSearches', JSON.stringify(updated));

    // Navigate to results
    window.location.href = `/search?q=${encodeURIComponent(searchQuery)}`;
  };

  return (
    <div className="mobile-search">
      <div className="search-bar">
        <input
          type="search"
          inputMode="search"
          placeholder="Search products..."
          value={query}
          onChange={(e) => handleSearch(e.target.value)}
          onFocus={() => setIsFocused(true)}
          onBlur={() => setTimeout(() => setIsFocused(false), 200)}
        />

        {query && (
          <button
            className="clear-button"
            onClick={() => {
              setQuery('');
              setResults([]);
            }}
          >
            ✕
          </button>
        )}
      </div>

      {isFocused && (
        <div className="search-dropdown">
          {query.length === 0 && recentSearches.length > 0 && (
            <div className="recent-searches">
              <h4>Recent Searches</h4>
              {recentSearches.map((search, index) => (
                <button
                  key={index}
                  className="search-suggestion"
                  onClick={() => handleSubmit(search)}
                >
                  <span className="icon">🕐</span>
                  {search}
                </button>
              ))}
            </div>
          )}

          {results.length > 0 && (
            <div className="search-results">
              {results.map((result) => (
                <button
                  key={result.id}
                  className="search-result-item"
                  onClick={() => handleSubmit(result.name)}
                >
                  <img src={result.thumbnail} alt="" />
                  <div>
                    <div className="result-name">{result.name}</div>
                    <div className="result-category">{result.category}</div>
                  </div>
                </button>
              ))}
            </div>
          )}
        </div>
      )}
    </div>
  );
}

4. Filter Drawer

function FilterDrawer({ isOpen, onClose, onApply }) {
  const [filters, setFilters] = useState({
    priceRange: [0, 1000],
    category: [],
    rating: 0,
    inStock: false,
  });

  return (
    <>
      {isOpen && (
        <div className="filter-drawer-overlay" onClick={onClose} />
      )}

      <div className={`filter-drawer ${isOpen ? 'open' : ''}`}>
        <div className="filter-header">
          <h2>Filters</h2>
          <button onClick={onClose}>✕</button>
        </div>

        <div className="filter-content">
          <div className="filter-section">
            <h3>Price Range</h3>
            <input
              type="range"
              min="0"
              max="1000"
              value={filters.priceRange[1]}
              onChange={(e) => setFilters({
                ...filters,
                priceRange: [0, parseInt(e.target.value)]
              })}
            />
            <div className="price-display">
              ${filters.priceRange[0]} - ${filters.priceRange[1]}
            </div>
          </div>

          <div className="filter-section">
            <h3>Category</h3>
            {['Electronics', 'Clothing', 'Books', 'Home'].map(cat => (
              <label key={cat} className="checkbox-label">
                <input
                  type="checkbox"
                  checked={filters.category.includes(cat)}
                  onChange={(e) => {
                    if (e.target.checked) {
                      setFilters({
                        ...filters,
                        category: [...filters.category, cat]
                      });
                    } else {
                      setFilters({
                        ...filters,
                        category: filters.category.filter(c => c !== cat)
                      });
                    }
                  }}
                />
                {cat}
              </label>
            ))}
          </div>

          <div className="filter-section">
            <label className="checkbox-label">
              <input
                type="checkbox"
                checked={filters.inStock}
                onChange={(e) => setFilters({
                  ...filters,
                  inStock: e.target.checked
                })}
              />
              In Stock Only
            </label>
          </div>
        </div>

        <div className="filter-actions">
          <button
            className="clear-button"
            onClick={() => setFilters({
              priceRange: [0, 1000],
              category: [],
              rating: 0,
              inStock: false,
            })}
          >
            Clear All
          </button>

          <button
            className="apply-button"
            onClick={() => {
              onApply(filters);
              onClose();
            }}
          >
            Apply Filters
          </button>
        </div>
      </div>
    </>
  );
}

// CSS
.filter-drawer {
  position: fixed;
  right: -100%;
  top: 0;
  bottom: 0;
  width: 85%;
  max-width: 400px;
  background: white;
  z-index: 1001;
  transition: right 0.3s;
  display: flex;
  flex-direction: column;
}

.filter-drawer.open {
  right: 0;
}

.filter-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px;
  border-bottom: 1px solid #E5E5EA;
}

.filter-content {
  flex: 1;
  overflow-y: auto;
  padding: 16px;
}

.filter-section {
  margin-bottom: 24px;
}

.filter-section h3 {
  font-size: 16px;
  font-weight: 600;
  margin-bottom: 12px;
}

.checkbox-label {
  display: flex;
  align-items: center;
  padding: 12px 0;
  font-size: 15px;
}

.checkbox-label input {
  margin-right: 12px;
  width: 20px;
  height: 20px;
}

.filter-actions {
  display: flex;
  gap: 12px;
  padding: 16px;
  border-top: 1px solid #E5E5EA;
}

.clear-button,
.apply-button {
  flex: 1;
  height: 48px;
  border-radius: 8px;
  font-size: 16px;
  font-weight: 600;
}

.clear-button {
  background: white;
  border: 2px solid #007AFF;
  color: #007AFF;
}

.apply-button {
  background: #007AFF;
  border: none;
  color: white;
}

5. Mobile Payment Form

function MobilePaymentForm() {
  const [cardNumber, setCardNumber] = useState('');
  const [expiry, setExpiry] = useState('');
  const [cvv, setCvv] = useState('');

  const formatCardNumber = (value) => {
    return value
      .replace(/\s/g, '')
      .match(/.{1,4}/g)
      ?.join(' ') || '';
  };

  const formatExpiry = (value) => {
    const cleaned = value.replace(/\D/g, '');
    if (cleaned.length >= 2) {
      return `${cleaned.slice(0, 2)}/${cleaned.slice(2, 4)}`;
    }
    return cleaned;
  };

  return (
    <form className="payment-form">
      <div className="form-group">
        <label>Card Number</label>
        <input
          type="text"
          inputMode="numeric"
          maxLength="19"
          placeholder="1234 5678 9012 3456"
          value={formatCardNumber(cardNumber)}
          onChange={(e) => setCardNumber(e.target.value.replace(/\s/g, ''))}
        />
      </div>

      <div className="form-row">
        <div className="form-group">
          <label>Expiry</label>
          <input
            type="text"
            inputMode="numeric"
            maxLength="5"
            placeholder="MM/YY"
            value={expiry}
            onChange={(e) => setExpiry(formatExpiry(e.target.value))}
          />
        </div>

        <div className="form-group">
          <label>CVV</label>
          <input
            type="text"
            inputMode="numeric"
            maxLength="4"
            placeholder="123"
            value={cvv}
            onChange={(e) => setCvv(e.target.value.replace(/\D/g, ''))}
          />
        </div>
      </div>

      <button type="submit" className="pay-button">
        Pay $99.99
      </button>
    </form>
  );
}

6-20. Additional Examples

For brevity, here are summaries of 14 more essential mobile design patterns:

6. Sticky Header with Scroll Progress

  • Header shrinks on scroll
  • Progress bar shows reading position
  • Back-to-top button appears after scroll

7. Image Gallery with Pinch Zoom

  • Full-screen image viewer
  • Swipe between images
  • Pinch to zoom functionality

8. Mobile-Optimized Data Table

  • Horizontal scroll with sticky first column
  • Card view on small screens
  • Expandable rows for details

9. Bottom Sheet Menu

  • Swipe up to expand
  • Drag to dismiss
  • Multiple snap points (collapsed, half, full)

10. Mobile Calendar Picker

  • Month view optimized for touch
  • Date range selection
  • Quick actions (Today, Tomorrow, Next Week)

11. Floating Action Button (FAB) with Speed Dial

  • Primary action always visible
  • Expands to show related actions
  • Smooth animations

12. Pull to Refresh

  • Custom loading animation
  • Haptic feedback
  • Success/error states

13. Swipeable Tabs

  • Horizontal scroll tabs
  • Active tab indicator
  • Snap to tab on scroll

14. Mobile Video Player

  • Custom controls optimized for touch
  • Picture-in-picture mode
  • Gesture controls (tap to pause, double-tap to skip)

15. Mobile Toast Notifications

  • Non-intrusive messaging
  • Auto-dismiss with manual override
  • Action buttons

16. Collapsible Accordion

  • Touch-friendly expand/collapse
  • Smooth animations
  • Multiple sections

17. Mobile Stepper Form

  • Multi-step process
  • Progress indicator
  • Back/Next navigation

18. Voice Input Interface

  • Microphone button
  • Real-time transcription
  • Voice feedback

19. Onboarding Carousel

  • Swipeable introduction screens
  • Skip option
  • Progress dots

20. Mobile Share Sheet

  • Native-like sharing interface
  • Common share targets
  • Copy link functionality

Conclusion

Mobile design requires deep understanding of touch interactions, platform conventions, and performance optimization. By following mobile-first principles, respecting thumb zones, and implementing platform-appropriate patterns, you create experiences that feel natural and performant on mobile devices.

Remember: mobile users are often on-the-go, have limited attention, and expect instant responsiveness. Prioritize speed, clarity, and ease of use above all else.