Design interfaces optimized for touch interaction and mobile usability with proper touch targets, gesture support, and accessibility for mobile devices.
Touch-friendly interface design is crucial for creating mobile experiences that feel natural and intuitive. Unlike mouse-driven interfaces, touch interactions require different considerations for target sizing, spacing, feedback, and gesture support.
Modern mobile users expect interfaces that respond immediately to their touch, provide clear visual feedback, and accommodate various hand positions and grip styles. From thumb-friendly navigation to accessible touch targets, every element must be designed with physical interaction in mind.
In this lesson, you'll learn to create interfaces that work seamlessly across different devices, screen sizes, and user contexts, ensuring your responsive designs provide excellent touch experiences.
Touch target sizing is fundamental to mobile usability. Users need adequate space to accurately tap elements without accidentally triggering adjacent controls.
Minimum Touch Target Standards:
1/* Apple iOS Guidelines: 44pt minimum */
2.touch-target {
3 min-width: 44px;
4 min-height: 44px;
5 padding: 12px;
6 margin: 8px;
7}
8
9/* Google Material Design: 48dp minimum */
10.material-touch-target {
11 min-width: 48px;
12 min-height: 48px;
13 padding: 16px;
14 margin: 8px;
15}
16
17/* Microsoft: 34px minimum, 40px recommended */
18.windows-touch-target {
19 min-width: 40px;
20 min-height: 40px;
21 padding: 10px;
22 margin: 6px;
23}
24
25/* Accessible touch targets for all users */
26.accessible-touch-target {
27 min-width: 48px;
28 min-height: 48px;
29 position: relative;
30 display: inline-flex;
31 align-items: center;
32 justify-content: center;
33 cursor: pointer;
34 user-select: none;
35 -webkit-tap-highlight-color: transparent;
36}Smart Touch Target Implementation:
1/* Expandable touch areas using pseudo-elements */
2.expandable-touch {
3 position: relative;
4 display: inline-block;
5 padding: 8px 12px;
6}
7
8.expandable-touch::after {
9 content: '';
10 position: absolute;
11 top: -10px;
12 left: -10px;
13 right: -10px;
14 bottom: -10px;
15 min-width: 44px;
16 min-height: 44px;
17 background: transparent;
18 border-radius: inherit;
19}
20
21/* Icon buttons with adequate touch areas */
22.icon-button {
23 width: 24px;
24 height: 24px;
25 background: none;
26 border: none;
27 cursor: pointer;
28 position: relative;
29 padding: 12px; /* Creates 48px total touch area */
30 border-radius: 50%;
31 transition: background-color 0.2s ease;
32}
33
34.icon-button:hover,
35.icon-button:focus {
36 background-color: rgba(0, 0, 0, 0.04);
37 outline: 2px solid #007AFF;
38 outline-offset: 2px;
39}
40
41.icon-button:active {
42 background-color: rgba(0, 0, 0, 0.08);
43 transform: scale(0.95);
44}
45
46/* Touch-friendly form controls */
47.touch-input {
48 min-height: 48px;
49 padding: 12px 16px;
50 border: 2px solid #E5E5E5;
51 border-radius: 8px;
52 font-size: 16px; /* Prevents zoom on iOS */
53 transition: border-color 0.2s ease;
54}
55
56.touch-input:focus {
57 border-color: #007AFF;
58 outline: none;
59 box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.1);
60}
61
62/* Touch-friendly checkboxes and radio buttons */
63.touch-checkbox {
64 position: relative;
65 display: inline-flex;
66 align-items: center;
67 cursor: pointer;
68 padding: 8px;
69 margin: 4px;
70 min-width: 48px;
71 min-height: 48px;
72}
73
74.touch-checkbox input[type="checkbox"] {
75 position: absolute;
76 opacity: 0;
77 width: 100%;
78 height: 100%;
79 margin: 0;
80 cursor: pointer;
81}
82
83.touch-checkbox .checkmark {
84 width: 20px;
85 height: 20px;
86 border: 2px solid #007AFF;
87 border-radius: 4px;
88 margin-right: 12px;
89 position: relative;
90 transition: all 0.2s ease;
91}
92
93.touch-checkbox input:checked + .checkmark {
94 background-color: #007AFF;
95}
96
97.touch-checkbox input:checked + .checkmark::after {
98 content: '✓';
99 position: absolute;
100 color: white;
101 font-size: 14px;
102 top: -2px;
103 left: 2px;
104}
105
106.touch-checkbox:hover .checkmark {
107 border-color: #0056CC;
108}
109
110.touch-checkbox input:focus + .checkmark {
111 box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.3);
112}Responsive Touch Target Scaling:
1/* Base touch targets for mobile */
2.responsive-button {
3 min-width: 44px;
4 min-height: 44px;
5 padding: 12px 16px;
6 border-radius: 8px;
7 font-size: 16px;
8 border: none;
9 cursor: pointer;
10 transition: all 0.2s ease;
11}
12
13/* Larger targets for smaller screens */
14@media (max-width: 480px) {
15 .responsive-button {
16 min-width: 48px;
17 min-height: 48px;
18 padding: 14px 20px;
19 font-size: 18px;
20 }
21}
22
23/* Adjust for tablet landscape */
24@media (min-width: 768px) and (orientation: landscape) {
25 .responsive-button {
26 min-width: 40px;
27 min-height: 40px;
28 padding: 10px 14px;
29 font-size: 15px;
30 }
31}
32
33/* Desktop hover states */
34@media (hover: hover) {
35 .responsive-button:hover {
36 transform: translateY(-1px);
37 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
38 }
39}
40
41/* High-DPI display adjustments */
42@media (-webkit-min-device-pixel-ratio: 2) {
43 .responsive-button {
44 border-width: 0.5px;
45 }
46}Key Principles:
Modern mobile interfaces rely on intuitive gestures like swiping, pinching, and long-pressing. Implementing gesture support enhances user experience and follows platform conventions.
Basic Touch Event Handling:
1class TouchGestureManager {
2 constructor(element) {
3 this.element = element;
4 this.startPoint = { x: 0, y: 0 };
5 this.endPoint = { x: 0, y: 0 };
6 this.startTime = 0;
7 this.endTime = 0;
8 this.threshold = {
9 distance: 50, // Minimum distance for swipe
10 time: 300 // Maximum time for tap
11 };
12
13 this.init();
14 }
15
16 init() {
17 // Touch events (mobile)
18 this.element.addEventListener('touchstart', (e) => this.handleStart(e), { passive: false });
19 this.element.addEventListener('touchmove', (e) => this.handleMove(e), { passive: false });
20 this.element.addEventListener('touchend', (e) => this.handleEnd(e), { passive: false });
21
22 // Pointer events (universal)
23 this.element.addEventListener('pointerdown', (e) => this.handleStart(e));
24 this.element.addEventListener('pointermove', (e) => this.handleMove(e));
25 this.element.addEventListener('pointerup', (e) => this.handleEnd(e));
26
27 // Mouse events (desktop fallback)
28 this.element.addEventListener('mousedown', (e) => this.handleStart(e));
29 this.element.addEventListener('mousemove', (e) => this.handleMove(e));
30 this.element.addEventListener('mouseup', (e) => this.handleEnd(e));
31 }
32
33 handleStart(e) {
34 const point = this.getEventPoint(e);
35 this.startPoint = point;
36 this.startTime = Date.now();
37
38 this.element.classList.add('touching');
39 this.onTouchStart && this.onTouchStart(point);
40 }
41
42 handleMove(e) {
43 const point = this.getEventPoint(e);
44 const deltaX = point.x - this.startPoint.x;
45 const deltaY = point.y - this.startPoint.y;
46
47 // Prevent default scrolling for horizontal swipes
48 if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 10) {
49 e.preventDefault();
50 }
51
52 this.onTouchMove && this.onTouchMove(point, deltaX, deltaY);
53 }
54
55 handleEnd(e) {
56 const point = this.getEventPoint(e);
57 this.endPoint = point;
58 this.endTime = Date.now();
59
60 this.element.classList.remove('touching');
61
62 const deltaX = this.endPoint.x - this.startPoint.x;
63 const deltaY = this.endPoint.y - this.startPoint.y;
64 const deltaTime = this.endTime - this.startTime;
65 const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
66
67 // Determine gesture type
68 if (deltaTime < this.threshold.time && distance < this.threshold.distance) {
69 this.onTap && this.onTap(this.endPoint);
70 } else if (distance > this.threshold.distance) {
71 const direction = this.getSwipeDirection(deltaX, deltaY);
72 this.onSwipe && this.onSwipe(direction, distance);
73 }
74
75 this.onTouchEnd && this.onTouchEnd(point);
76 }
77
78 getEventPoint(e) {
79 if (e.touches && e.touches.length > 0) {
80 return { x: e.touches[0].clientX, y: e.touches[0].clientY };
81 }
82 return { x: e.clientX, y: e.clientY };
83 }
84
85 getSwipeDirection(deltaX, deltaY) {
86 if (Math.abs(deltaX) > Math.abs(deltaY)) {
87 return deltaX > 0 ? 'right' : 'left';
88 } else {
89 return deltaY > 0 ? 'down' : 'up';
90 }
91 }
92}CSS for Touch-Friendly Interactions:
1/* Touch action controls */
2.swipeable-container {
3 touch-action: pan-x; /* Allow horizontal scrolling only */
4 overflow-x: auto;
5 overflow-y: hidden;
6 -webkit-overflow-scrolling: touch; /* iOS momentum scrolling */
7}
8
9.vertical-scroll {
10 touch-action: pan-y; /* Allow vertical scrolling only */
11 overflow-y: auto;
12 overflow-x: hidden;
13 -webkit-overflow-scrolling: touch;
14}
15
16.pinch-zoom {
17 touch-action: pinch-zoom; /* Allow pinch-to-zoom */
18}
19
20.no-touch-action {
21 touch-action: none; /* Prevent all default touch behaviors */
22}
23
24/* Touch feedback animations */
25.touch-ripple {
26 position: relative;
27 overflow: hidden;
28 transition: all 0.2s ease;
29}
30
31.touch-ripple::after {
32 content: '';
33 position: absolute;
34 top: 50%;
35 left: 50%;
36 width: 0;
37 height: 0;
38 border-radius: 50%;
39 background: rgba(255, 255, 255, 0.5);
40 transform: translate(-50%, -50%);
41 transition: width 0.3s ease, height 0.3s ease, opacity 0.3s ease;
42 pointer-events: none;
43 opacity: 0;
44}
45
46.touch-ripple:active::after,
47.touch-ripple.touching::after {
48 width: 200px;
49 height: 200px;
50 opacity: 1;
51 transition: width 0.1s ease, height 0.1s ease;
52}
53
54/* Long press indication */
55.long-pressable {
56 position: relative;
57 transition: all 0.2s ease;
58}
59
60.long-pressable::before {
61 content: '';
62 position: absolute;
63 top: 0;
64 left: 0;
65 right: 0;
66 height: 3px;
67 background: linear-gradient(90deg, #007AFF 0%, transparent 0%);
68 border-radius: 2px;
69 transition: all 0.1s ease;
70 opacity: 0;
71}
72
73.long-pressable.pressing::before {
74 opacity: 1;
75 background: linear-gradient(90deg, #007AFF var(--progress, 0%), transparent var(--progress, 0%));
76}
77
78/* Swipe indicators */
79.swipe-container {
80 position: relative;
81 overflow: hidden;
82}
83
84.swipe-indicator {
85 position: absolute;
86 top: 50%;
87 right: 20px;
88 transform: translateY(-50%);
89 opacity: 0.6;
90 font-size: 20px;
91 animation: swipeHint 2s ease-in-out infinite;
92}
93
94@keyframes swipeHint {
95 0%, 100% { transform: translateY(-50%) translateX(0); }
96 50% { transform: translateY(-50%) translateX(-10px); }
97}
98
99/* Pull-to-refresh */
100.pull-to-refresh {
101 position: relative;
102 min-height: 100vh;
103 touch-action: pan-y;
104}
105
106.refresh-indicator {
107 position: absolute;
108 top: -60px;
109 left: 50%;
110 transform: translateX(-50%);
111 width: 40px;
112 height: 40px;
113 border-radius: 50%;
114 background: #007AFF;
115 display: flex;
116 align-items: center;
117 justify-content: center;
118 color: white;
119 transition: all 0.3s ease;
120 opacity: 0;
121}
122
123.pull-to-refresh.pulling .refresh-indicator {
124 transform: translateX(-50%) translateY(80px);
125 opacity: 1;
126}
127
128.pull-to-refresh.refreshing .refresh-indicator {
129 animation: spin 1s linear infinite;
130}
131
132@keyframes spin {
133 from { transform: translateX(-50%) translateY(80px) rotate(0deg); }
134 to { transform: translateX(-50%) translateY(80px) rotate(360deg); }
135}Advanced Gesture Implementation:
1// Multi-touch gesture handler
2class MultiTouchHandler {
3 constructor(element) {
4 this.element = element;
5 this.touches = new Map();
6 this.initialDistance = 0;
7 this.initialScale = 1;
8
9 this.init();
10 }
11
12 init() {
13 this.element.addEventListener('touchstart', (e) => this.handleTouchStart(e));
14 this.element.addEventListener('touchmove', (e) => this.handleTouchMove(e));
15 this.element.addEventListener('touchend', (e) => this.handleTouchEnd(e));
16 }
17
18 handleTouchStart(e) {
19 for (let touch of e.touches) {
20 this.touches.set(touch.identifier, {
21 x: touch.clientX,
22 y: touch.clientY
23 });
24 }
25
26 if (e.touches.length === 2) {
27 this.initialDistance = this.getDistance(e.touches[0], e.touches[1]);
28 this.onPinchStart && this.onPinchStart();
29 }
30 }
31
32 handleTouchMove(e) {
33 e.preventDefault();
34
35 if (e.touches.length === 2) {
36 const currentDistance = this.getDistance(e.touches[0], e.touches[1]);
37 const scale = currentDistance / this.initialDistance;
38
39 this.onPinch && this.onPinch(scale);
40 } else if (e.touches.length === 1) {
41 const touch = e.touches[0];
42 const startTouch = this.touches.get(touch.identifier);
43
44 if (startTouch) {
45 const deltaX = touch.clientX - startTouch.x;
46 const deltaY = touch.clientY - startTouch.y;
47
48 this.onPan && this.onPan(deltaX, deltaY);
49 }
50 }
51 }
52
53 handleTouchEnd(e) {
54 for (let touch of e.changedTouches) {
55 this.touches.delete(touch.identifier);
56 }
57
58 if (e.touches.length < 2) {
59 this.onPinchEnd && this.onPinchEnd();
60 }
61 }
62
63 getDistance(touch1, touch2) {
64 const dx = touch1.clientX - touch2.clientX;
65 const dy = touch1.clientY - touch2.clientY;
66 return Math.sqrt(dx * dx + dy * dy);
67 }
68}
69
70// Long press handler
71class LongPressHandler {
72 constructor(element, options = {}) {
73 this.element = element;
74 this.duration = options.duration || 500;
75 this.threshold = options.threshold || 10;
76 this.timeout = null;
77 this.startPoint = null;
78
79 this.init();
80 }
81
82 init() {
83 this.element.addEventListener('touchstart', (e) => this.start(e));
84 this.element.addEventListener('touchmove', (e) => this.move(e));
85 this.element.addEventListener('touchend', (e) => this.end(e));
86 this.element.addEventListener('touchcancel', (e) => this.cancel(e));
87 }
88
89 start(e) {
90 this.startPoint = {
91 x: e.touches[0].clientX,
92 y: e.touches[0].clientY
93 };
94
95 let progress = 0;
96 const progressInterval = 50;
97
98 this.timeout = setTimeout(() => {
99 this.onLongPress && this.onLongPress(e);
100 }, this.duration);
101
102 // Visual progress feedback
103 const progressTimer = setInterval(() => {
104 progress += progressInterval;
105 const percentage = (progress / this.duration) * 100;
106 this.element.style.setProperty('--progress', `${percentage}%`);
107
108 if (progress >= this.duration) {
109 clearInterval(progressTimer);
110 }
111 }, progressInterval);
112 }
113
114 move(e) {
115 if (this.startPoint) {
116 const currentPoint = {
117 x: e.touches[0].clientX,
118 y: e.touches[0].clientY
119 };
120
121 const distance = Math.sqrt(
122 Math.pow(currentPoint.x - this.startPoint.x, 2) +
123 Math.pow(currentPoint.y - this.startPoint.y, 2)
124 );
125
126 if (distance > this.threshold) {
127 this.cancel();
128 }
129 }
130 }
131
132 end(e) {
133 this.cancel();
134 }
135
136 cancel() {
137 if (this.timeout) {
138 clearTimeout(this.timeout);
139 this.timeout = null;
140 }
141
142 this.element.style.removeProperty('--progress');
143 this.element.classList.remove('pressing');
144 this.startPoint = null;
145 }
146}Key Gesture Patterns:
Creating accessible touch interfaces ensures usability for all users, including those with motor impairments, visual challenges, or using assistive technologies.
Accessibility-First Touch Design:
1/* High contrast support */
2@media (prefers-contrast: high) {
3 .touch-button {
4 border: 2px solid currentColor;
5 background-color: ButtonFace;
6 color: ButtonText;
7 }
8
9 .touch-button:focus,
10 .touch-button:hover {
11 outline: 3px solid Highlight;
12 outline-offset: 2px;
13 }
14}
15
16/* Reduced motion support */
17@media (prefers-reduced-motion: reduce) {
18 .touch-button,
19 .gesture-element {
20 transition: none;
21 animation: none;
22 }
23
24 .touch-ripple::after {
25 display: none;
26 }
27}
28
29/* Large text support */
30@media (prefers-reduced-data: reduce) {
31 .icon-button {
32 background-image: none;
33 }
34
35 .icon-button::after {
36 content: attr(aria-label);
37 font-size: 12px;
38 }
39}
40
41/* Touch accommodation preferences */
42.touch-accommodations {
43 --touch-size-multiplier: 1;
44}
45
46@media (prefers-reduced-motion: reduce) {
47 .touch-accommodations {
48 --touch-size-multiplier: 1.2;
49 }
50}
51
52.accessible-touch-target {
53 min-width: calc(44px * var(--touch-size-multiplier));
54 min-height: calc(44px * var(--touch-size-multiplier));
55 padding: calc(12px * var(--touch-size-multiplier));
56}
57
58/* Focus management for touch */
59.touch-focus-visible {
60 outline: none;
61}
62
63.touch-focus-visible:focus-visible {
64 outline: 2px solid #007AFF;
65 outline-offset: 2px;
66 box-shadow: 0 0 0 4px rgba(0, 122, 255, 0.3);
67}
68
69/* Skip to content for mobile */
70.skip-to-content {
71 position: absolute;
72 top: -40px;
73 left: 6px;
74 background: #000;
75 color: #fff;
76 padding: 8px;
77 text-decoration: none;
78 border-radius: 4px;
79 z-index: 10000;
80 transform: translateY(-100%);
81 transition: transform 0.3s ease;
82}
83
84.skip-to-content:focus {
85 transform: translateY(0%);
86}
87
88/* Voice control support */
89.voice-control-target {
90 position: relative;
91}
92
93.voice-control-target::before {
94 content: attr(data-voice-command);
95 position: absolute;
96 top: -20px;
97 left: 0;
98 background: rgba(0, 0, 0, 0.8);
99 color: white;
100 padding: 2px 6px;
101 border-radius: 3px;
102 font-size: 10px;
103 opacity: 0;
104 pointer-events: none;
105 transition: opacity 0.2s ease;
106}
107
108.voice-control-active .voice-control-target::before {
109 opacity: 1;
110}
111
112/* Touch target spacing for motor impairments */
113.motor-friendly-layout {
114 display: flex;
115 flex-wrap: wrap;
116 gap: 16px;
117 padding: 16px;
118}
119
120.motor-friendly-layout .touch-target {
121 min-width: 56px;
122 min-height: 56px;
123 margin: 8px;
124 border-radius: 12px;
125}
126
127/* One-handed operation support */
128.thumb-zone {
129 position: fixed;
130 bottom: 0;
131 left: 0;
132 right: 0;
133 height: 40%;
134 pointer-events: none;
135 z-index: -1;
136}
137
138.thumb-zone::before {
139 content: '';
140 position: absolute;
141 bottom: 0;
142 right: 20px;
143 width: 60%;
144 height: 80%;
145 border-radius: 50% 50% 0 0;
146 background: rgba(0, 122, 255, 0.1);
147 border: 2px dashed rgba(0, 122, 255, 0.3);
148}
149
150/* Primary actions in thumb zone */
151.primary-actions {
152 position: fixed;
153 bottom: 20px;
154 right: 20px;
155 display: flex;
156 flex-direction: column;
157 gap: 12px;
158}
159
160.floating-action-button {
161 width: 56px;
162 height: 56px;
163 border-radius: 50%;
164 background: #007AFF;
165 color: white;
166 border: none;
167 font-size: 24px;
168 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
169 transition: all 0.3s ease;
170 cursor: pointer;
171}
172
173.floating-action-button:hover,
174.floating-action-button:focus {
175 transform: scale(1.1);
176 box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
177 outline: 3px solid rgba(0, 122, 255, 0.3);
178}
179
180.floating-action-button:active {
181 transform: scale(0.95);
182}Screen Reader Optimized Touch Elements:
1<!-- Accessible touch button with comprehensive labeling -->
2<button
3 class="touch-button"
4 type="button"
5 aria-label="Add item to favorites"
6 aria-describedby="favorite-help"
7 data-voice-command="Add favorite">
8 <svg aria-hidden="true" focusable="false">
9 <use href="#heart-icon"></use>
10 </svg>
11 <span class="sr-only">Add to favorites</span>
12</button>
13<div id="favorite-help" class="sr-only">
14 Double-tap to add this item to your favorites list
15</div>
16
17<!-- Touch-friendly form with proper labeling -->
18<form class="touch-form">
19 <div class="form-group">
20 <label for="email" class="form-label">
21 Email Address
22 <span class="required-indicator" aria-label="required">*</span>
23 </label>
24 <input
25 type="email"
26 id="email"
27 class="touch-input"
28 aria-describedby="email-help"
29 aria-required="true"
30 autocomplete="email"
31 inputmode="email">
32 <div id="email-help" class="help-text">
33 We'll use this to send you updates
34 </div>
35 </div>
36
37 <div class="form-group">
38 <fieldset class="touch-fieldset">
39 <legend class="form-legend">Notification Preferences</legend>
40 <div class="checkbox-group">
41 <label class="touch-checkbox">
42 <input type="checkbox" value="email">
43 <span class="checkmark" aria-hidden="true"></span>
44 Email notifications
45 </label>
46 <label class="touch-checkbox">
47 <input type="checkbox" value="push">
48 <span class="checkmark" aria-hidden="true"></span>
49 Push notifications
50 </label>
51 </div>
52 </fieldset>
53 </div>
54
55 <button type="submit" class="submit-button touch-button">
56 Submit Form
57 <span class="loading-indicator" aria-hidden="true"></span>
58 </button>
59</form>Touch-Specific JavaScript Enhancements:
1class AccessibleTouchInterface {
2 constructor() {
3 this.touchTargets = document.querySelectorAll('.touch-target');
4 this.longPressActions = new Map();
5 this.init();
6 }
7
8 init() {
9 this.enhanceTouchTargets();
10 this.addGestureSupport();
11 this.improveAccessibility();
12 }
13
14 enhanceTouchTargets() {
15 this.touchTargets.forEach(target => {
16 // Add minimum touch area
17 const rect = target.getBoundingClientRect();
18 if (rect.width < 44 || rect.height < 44) {
19 target.style.minWidth = '44px';
20 target.style.minHeight = '44px';
21 target.classList.add('enhanced-touch-target');
22 }
23
24 // Add touch feedback
25 this.addTouchFeedback(target);
26 });
27 }
28
29 addTouchFeedback(element) {
30 let feedbackTimeout;
31
32 element.addEventListener('touchstart', (e) => {
33 element.classList.add('touch-active');
34
35 // Haptic feedback if supported
36 if (navigator.vibrate) {
37 navigator.vibrate(10);
38 }
39
40 // Visual feedback
41 this.createRippleEffect(element, e.touches[0]);
42 }, { passive: true });
43
44 element.addEventListener('touchend', () => {
45 feedbackTimeout = setTimeout(() => {
46 element.classList.remove('touch-active');
47 }, 150);
48 }, { passive: true });
49
50 element.addEventListener('touchcancel', () => {
51 clearTimeout(feedbackTimeout);
52 element.classList.remove('touch-active');
53 }, { passive: true });
54 }
55
56 createRippleEffect(element, touch) {
57 const rect = element.getBoundingClientRect();
58 const x = touch.clientX - rect.left;
59 const y = touch.clientY - rect.top;
60
61 const ripple = document.createElement('div');
62 ripple.className = 'touch-ripple-effect';
63 ripple.style.cssText = `
64 position: absolute;
65 left: ${x}px;
66 top: ${y}px;
67 width: 0;
68 height: 0;
69 border-radius: 50%;
70 background: rgba(255, 255, 255, 0.6);
71 transform: translate(-50%, -50%);
72 animation: ripple 0.6s ease-out;
73 pointer-events: none;
74 `;
75
76 element.style.position = 'relative';
77 element.style.overflow = 'hidden';
78 element.appendChild(ripple);
79
80 setTimeout(() => {
81 ripple.remove();
82 }, 600);
83 }
84
85 addGestureSupport() {
86 // Double-tap handler
87 let lastTap = 0;
88
89 document.addEventListener('touchend', (e) => {
90 const now = Date.now();
91 const timeSinceLastTap = now - lastTap;
92
93 if (timeSinceLastTap < 300 && timeSinceLastTap > 0) {
94 const target = e.target.closest('[data-double-tap]');
95 if (target) {
96 e.preventDefault();
97 this.handleDoubleTap(target);
98 }
99 }
100
101 lastTap = now;
102 });
103 }
104
105 handleDoubleTap(element) {
106 const action = element.getAttribute('data-double-tap');
107 element.dispatchEvent(new CustomEvent('doubletap', {
108 detail: { action }
109 }));
110
111 // Visual feedback for double-tap
112 element.style.animation = 'none';
113 element.offsetHeight; // Trigger reflow
114 element.style.animation = 'doubleTapFeedback 0.3s ease';
115 }
116
117 improveAccessibility() {
118 // Announce touch interactions to screen readers
119 const announcer = document.createElement('div');
120 announcer.setAttribute('aria-live', 'polite');
121 announcer.setAttribute('aria-atomic', 'true');
122 announcer.style.cssText = 'position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden;';
123 document.body.appendChild(announcer);
124
125 // Announce gesture availability
126 document.addEventListener('focus', (e) => {
127 const target = e.target;
128 const gestures = [];
129
130 if (target.hasAttribute('data-swipe')) {
131 gestures.push('swipe');
132 }
133 if (target.hasAttribute('data-long-press')) {
134 gestures.push('long press');
135 }
136 if (target.hasAttribute('data-double-tap')) {
137 gestures.push('double tap');
138 }
139
140 if (gestures.length > 0) {
141 announcer.textContent = `Available gestures: ${gestures.join(', ')}`;
142 }
143 });
144
145 // Handle reduced motion preferences
146 if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
147 document.documentElement.style.setProperty('--animation-duration', '0s');
148 }
149 }
150}
151
152// Auto-init on DOM ready
153document.addEventListener('DOMContentLoaded', () => {
154 new AccessibleTouchInterface();
155});Mobile UX Best Practices:
Build a comprehensive touch-optimized interface with proper sizing, gesture support, accessibility features, and mobile UX patterns.
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>Touch-Friendly Interface Design</title>
7 <style>
8 * {
9 margin: 0;
10 padding: 0;
11 box-sizing: border-box;
12 }
13
14 :root {
15 --primary-color: #007AFF;
16 --secondary-color: #5856D6;
17 --success-color: #34C759;
18 --warning-color: #FF9500;
19 --danger-color: #FF3B30;
20 --text-primary: #000000;
21 --text-secondary: #6D6D70;
22 --background: #F2F2F7;
23 --surface: #FFFFFF;
24 --touch-size: 44px;
25 --touch-padding: 12px;
26 --animation-duration: 0.3s;
27 --animation-easing: cubic-bezier(0.4, 0, 0.2, 1);
28 }
29
30 body {
31 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
32 background: var(--background);
33 color: var(--text-primary);
34 line-height: 1.6;
35 -webkit-font-smoothing: antialiased;
36 user-select: none;
37 -webkit-tap-highlight-color: transparent;
38 }
39
40 /* Touch-friendly header */
41 .touch-header {
42 background: var(--surface);
43 padding: 16px 20px;
44 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
45 display: flex;
46 align-items: center;
47 justify-content: space-between;
48 min-height: 64px;
49 position: sticky;
50 top: 0;
51 z-index: 100;
52 }
53
54 .header-title {
55 font-size: 20px;
56 font-weight: 600;
57 color: var(--text-primary);
58 }
59
60 .header-actions {
61 display: flex;
62 gap: 8px;
63 }
64
65 /* Touch button base styles */
66 .touch-btn {
67 min-width: var(--touch-size);
68 min-height: var(--touch-size);
69 padding: var(--touch-padding);
70 border: none;
71 border-radius: 12px;
72 background: var(--primary-color);
73 color: white;
74 font-size: 16px;
75 font-weight: 500;
76 cursor: pointer;
77 display: inline-flex;
78 align-items: center;
79 justify-content: center;
80 gap: 8px;
81 transition: all var(--animation-duration) var(--animation-easing);
82 position: relative;
83 overflow: hidden;
84 user-select: none;
85 }
86
87 .touch-btn:focus {
88 outline: 3px solid rgba(0, 122, 255, 0.3);
89 outline-offset: 2px;
90 }
91
92 .touch-btn:active {
93 transform: scale(0.96);
94 }
95
96 .touch-btn:disabled {
97 opacity: 0.5;
98 cursor: not-allowed;
99 }
100
101 /* Button variants */
102 .btn-secondary {
103 background: var(--secondary-color);
104 }
105
106 .btn-success {
107 background: var(--success-color);
108 }
109
110 .btn-warning {
111 background: var(--warning-color);
112 }
113
114 .btn-danger {
115 background: var(--danger-color);
116 }
117
118 .btn-outline {
119 background: transparent;
120 color: var(--primary-color);
121 border: 2px solid var(--primary-color);
122 }
123
124 .btn-ghost {
125 background: rgba(0, 122, 255, 0.1);
126 color: var(--primary-color);
127 }
128
129 /* Icon buttons */
130 .icon-btn {
131 width: var(--touch-size);
132 height: var(--touch-size);
133 padding: 10px;
134 border-radius: 50%;
135 background: rgba(0, 0, 0, 0.05);
136 color: var(--text-primary);
137 }
138
139 .icon-btn:hover {
140 background: rgba(0, 0, 0, 0.1);
141 }
142
143 .icon-btn:active {
144 background: rgba(0, 0, 0, 0.15);
145 }
146
147 /* Touch-friendly form controls */
148 .form-container {
149 padding: 24px 20px;
150 background: var(--surface);
151 margin: 16px 20px;
152 border-radius: 16px;
153 box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
154 }
155
156 .form-group {
157 margin-bottom: 20px;
158 }
159
160 .form-label {
161 display: block;
162 font-size: 16px;
163 font-weight: 500;
164 margin-bottom: 8px;
165 color: var(--text-primary);
166 }
167
168 .form-input {
169 width: 100%;
170 min-height: 48px;
171 padding: 12px 16px;
172 border: 2px solid #E5E5EA;
173 border-radius: 12px;
174 font-size: 16px;
175 background: var(--surface);
176 color: var(--text-primary);
177 transition: border-color var(--animation-duration);
178 }
179
180 .form-input:focus {
181 border-color: var(--primary-color);
182 outline: none;
183 box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.1);
184 }
185
186 .form-input::placeholder {
187 color: var(--text-secondary);
188 }
189
190 /* Touch-friendly checkboxes */
191 .checkbox-group {
192 display: flex;
193 flex-direction: column;
194 gap: 12px;
195 }
196
197 .touch-checkbox {
198 display: flex;
199 align-items: center;
200 padding: 12px;
201 border-radius: 12px;
202 cursor: pointer;
203 transition: background-color var(--animation-duration);
204 min-height: var(--touch-size);
205 position: relative;
206 }
207
208 .touch-checkbox:hover {
209 background: rgba(0, 0, 0, 0.05);
210 }
211
212 .touch-checkbox input {
213 position: absolute;
214 opacity: 0;
215 cursor: pointer;
216 width: 100%;
217 height: 100%;
218 }
219
220 .checkmark {
221 width: 20px;
222 height: 20px;
223 border: 2px solid var(--primary-color);
224 border-radius: 4px;
225 margin-right: 12px;
226 position: relative;
227 transition: all var(--animation-duration);
228 flex-shrink: 0;
229 }
230
231 .touch-checkbox input:checked + .checkmark {
232 background: var(--primary-color);
233 }
234
235 .touch-checkbox input:checked + .checkmark::after {
236 content: '✓';
237 position: absolute;
238 color: white;
239 font-size: 14px;
240 font-weight: bold;
241 top: -2px;
242 left: 2px;
243 }
244
245 .touch-checkbox input:focus + .checkmark {
246 box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.3);
247 }
248
249 /* Gesture-enabled card component */
250 .gesture-card {
251 background: var(--surface);
252 border-radius: 16px;
253 padding: 20px;
254 margin: 16px 20px;
255 box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
256 position: relative;
257 overflow: hidden;
258 cursor: pointer;
259 touch-action: pan-x;
260 transition: transform var(--animation-duration);
261 }
262
263 .gesture-card.swiping {
264 transition: none;
265 }
266
267 .gesture-card:active {
268 transform: scale(0.98);
269 }
270
271 .card-content {
272 position: relative;
273 z-index: 2;
274 }
275
276 .card-title {
277 font-size: 18px;
278 font-weight: 600;
279 margin-bottom: 8px;
280 }
281
282 .card-description {
283 color: var(--text-secondary);
284 margin-bottom: 16px;
285 }
286
287 /* Swipe actions */
288 .swipe-actions {
289 position: absolute;
290 top: 0;
291 right: -100px;
292 height: 100%;
293 display: flex;
294 align-items: center;
295 background: var(--danger-color);
296 padding: 0 20px;
297 color: white;
298 font-weight: 500;
299 }
300
301 .swipe-actions.left {
302 left: -100px;
303 right: auto;
304 background: var(--success-color);
305 }
306
307 /* Bottom navigation */
308 .bottom-nav {
309 position: fixed;
310 bottom: 0;
311 left: 0;
312 right: 0;
313 background: var(--surface);
314 border-top: 1px solid #E5E5EA;
315 padding: 8px 0;
316 z-index: 100;
317 display: flex;
318 justify-content: space-around;
319 box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
320 }
321
322 .nav-item {
323 display: flex;
324 flex-direction: column;
325 align-items: center;
326 padding: 8px 16px;
327 min-width: var(--touch-size);
328 min-height: var(--touch-size);
329 color: var(--text-secondary);
330 text-decoration: none;
331 border-radius: 8px;
332 transition: all var(--animation-duration);
333 cursor: pointer;
334 }
335
336 .nav-item:hover,
337 .nav-item.active {
338 color: var(--primary-color);
339 background: rgba(0, 122, 255, 0.1);
340 }
341
342 .nav-icon {
343 font-size: 20px;
344 margin-bottom: 4px;
345 }
346
347 .nav-label {
348 font-size: 12px;
349 font-weight: 500;
350 }
351
352 /* Floating action button */
353 .fab {
354 position: fixed;
355 bottom: 80px;
356 right: 20px;
357 width: 56px;
358 height: 56px;
359 border-radius: 50%;
360 background: var(--primary-color);
361 color: white;
362 border: none;
363 font-size: 24px;
364 cursor: pointer;
365 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
366 transition: all var(--animation-duration);
367 z-index: 99;
368 }
369
370 .fab:hover,
371 .fab:focus {
372 transform: scale(1.1);
373 box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
374 outline: 3px solid rgba(0, 122, 255, 0.3);
375 }
376
377 .fab:active {
378 transform: scale(0.9);
379 }
380
381 /* Touch feedback animations */
382 .touch-ripple {
383 position: absolute;
384 border-radius: 50%;
385 background: rgba(255, 255, 255, 0.6);
386 transform: translate(-50%, -50%);
387 animation: ripple 0.6s ease-out;
388 pointer-events: none;
389 }
390
391 @keyframes ripple {
392 from {
393 width: 0;
394 height: 0;
395 opacity: 1;
396 }
397 to {
398 width: 200px;
399 height: 200px;
400 opacity: 0;
401 }
402 }
403
404 /* Long press feedback */
405 .long-press-target {
406 position: relative;
407 }
408
409 .long-press-target::after {
410 content: '';
411 position: absolute;
412 top: 0;
413 left: 0;
414 right: 0;
415 height: 3px;
416 background: linear-gradient(90deg, var(--primary-color) var(--progress, 0%), transparent var(--progress, 0%));
417 border-radius: 2px;
418 opacity: 0;
419 transition: opacity 0.1s;
420 }
421
422 .long-press-target.pressing::after {
423 opacity: 1;
424 }
425
426 /* Accessibility improvements */
427 .sr-only {
428 position: absolute;
429 width: 1px;
430 height: 1px;
431 padding: 0;
432 margin: -1px;
433 overflow: hidden;
434 clip: rect(0, 0, 0, 0);
435 white-space: nowrap;
436 border: 0;
437 }
438
439 /* High contrast mode */
440 @media (prefers-contrast: high) {
441 .touch-btn,
442 .form-input,
443 .gesture-card {
444 border: 2px solid currentColor;
445 }
446 }
447
448 /* Reduced motion */
449 @media (prefers-reduced-motion: reduce) {
450 * {
451 transition-duration: 0.01ms !important;
452 animation-duration: 0.01ms !important;
453 }
454 }
455
456 /* Dark mode support */
457 @media (prefers-color-scheme: dark) {
458 :root {
459 --text-primary: #FFFFFF;
460 --text-secondary: #8E8E93;
461 --background: #000000;
462 --surface: #1C1C1E;
463 }
464
465 .icon-btn {
466 background: rgba(255, 255, 255, 0.1);
467 }
468 }
469
470 /* Content spacing for bottom navigation */
471 .main-content {
472 padding-bottom: 80px;
473 }
474
475 /* Demo section */
476 .demo-section {
477 padding: 20px;
478 background: var(--surface);
479 margin: 16px 20px;
480 border-radius: 16px;
481 }
482
483 .demo-title {
484 font-size: 18px;
485 font-weight: 600;
486 margin-bottom: 16px;
487 color: var(--text-primary);
488 }
489
490 .button-grid {
491 display: grid;
492 grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
493 gap: 12px;
494 margin-bottom: 20px;
495 }
496
497 .gesture-demo {
498 background: rgba(0, 122, 255, 0.1);
499 border: 2px dashed var(--primary-color);
500 border-radius: 12px;
501 padding: 20px;
502 text-align: center;
503 margin: 16px 0;
504 color: var(--primary-color);
505 font-weight: 500;
506 }
507 </style>
508</head>
509<body>
510 <!-- Touch-friendly header -->
511 <header class="touch-header">
512 <h1 class="header-title">👆 Touch Interface</h1>
513 <div class="header-actions">
514 <button class="icon-btn" type="button" aria-label="Search">
515 <span aria-hidden="true">🔍</span>
516 </button>
517 <button class="icon-btn" type="button" aria-label="Settings">
518 <span aria-hidden="true">⚙️</span>
519 </button>
520 </div>
521 </header>
522
523 <div class="main-content">
524 <!-- Button showcase -->
525 <section class="demo-section">
526 <h2 class="demo-title">Touch-Friendly Buttons</h2>
527 <div class="button-grid">
528 <button class="touch-btn">Primary</button>
529 <button class="touch-btn btn-secondary">Secondary</button>
530 <button class="touch-btn btn-success">Success</button>
531 <button class="touch-btn btn-warning">Warning</button>
532 <button class="touch-btn btn-danger">Danger</button>
533 <button class="touch-btn btn-outline">Outline</button>
534 <button class="touch-btn btn-ghost">Ghost</button>
535 <button class="touch-btn" disabled>Disabled</button>
536 </div>
537 <p style="color: var(--text-secondary); font-size: 14px;">
538 All buttons meet minimum 44px touch target requirements
539 </p>
540 </section>
541
542 <!-- Form elements -->
543 <section class="form-container">
544 <h2 class="demo-title">Touch-Optimized Form</h2>
545 <form>
546 <div class="form-group">
547 <label for="name" class="form-label">Full Name</label>
548 <input
549 type="text"
550 id="name"
551 class="form-input"
552 placeholder="Enter your full name"
553 autocomplete="name">
554 </div>
555
556 <div class="form-group">
557 <label for="email" class="form-label">Email Address</label>
558 <input
559 type="email"
560 id="email"
561 class="form-input"
562 placeholder="[email protected]"
563 autocomplete="email">
564 </div>
565
566 <div class="form-group">
567 <label class="form-label">Preferences</label>
568 <div class="checkbox-group">
569 <label class="touch-checkbox">
570 <input type="checkbox" name="notifications" value="email">
571 <span class="checkmark"></span>
572 Email notifications
573 </label>
574 <label class="touch-checkbox">
575 <input type="checkbox" name="notifications" value="push">
576 <span class="checkmark"></span>
577 Push notifications
578 </label>
579 <label class="touch-checkbox">
580 <input type="checkbox" name="notifications" value="sms">
581 <span class="checkmark"></span>
582 SMS updates
583 </label>
584 </div>
585 </div>
586
587 <button type="submit" class="touch-btn" style="width: 100%;">
588 Submit Form
589 </button>
590 </form>
591 </section>
592
593 <!-- Gesture demonstration -->
594 <section class="demo-section">
595 <h2 class="demo-title">Gesture Interactions</h2>
596 <div class="gesture-demo" id="gestureDemo">
597 👆 Tap, long press, or swipe this area
598 <div class="demo-feedback" id="gesturefeedback" style="margin-top: 10px; font-size: 14px;"></div>
599 </div>
600 </section>
601
602 <!-- Swipeable cards -->
603 <div class="gesture-card" data-swipe="true">
604 <div class="swipe-actions left">
605 <span>✅ Complete</span>
606 </div>
607 <div class="swipe-actions">
608 <span>🗑️ Delete</span>
609 </div>
610 <div class="card-content">
611 <h3 class="card-title">Swipeable Card #1</h3>
612 <p class="card-description">
613 Swipe left to delete or right to mark complete. Tap to interact normally.
614 </p>
615 <button class="touch-btn btn-ghost">View Details</button>
616 </div>
617 </div>
618
619 <div class="gesture-card long-press-target" data-long-press="true">
620 <div class="card-content">
621 <h3 class="card-title">Long Press Card</h3>
622 <p class="card-description">
623 Long press this card to see the progress indicator and trigger the context menu.
624 </p>
625 <button class="touch-btn btn-ghost">Normal Action</button>
626 </div>
627 </div>
628
629 <div class="gesture-card" data-double-tap="favorite">
630 <div class="card-content">
631 <h3 class="card-title">Double-Tap Card</h3>
632 <p class="card-description">
633 Double-tap this card to add it to favorites. Single tap for normal interaction.
634 </p>
635 <button class="touch-btn btn-ghost">Single Tap</button>
636 </div>
637 </div>
638 </div>
639
640 <!-- Floating Action Button -->
641 <button class="fab" type="button" aria-label="Add new item">
642 <span aria-hidden="true">+</span>
643 </button>
644
645 <!-- Bottom Navigation -->
646 <nav class="bottom-nav" role="navigation" aria-label="Bottom navigation">
647 <a href="#home" class="nav-item active">
648 <div class="nav-icon">🏠</div>
649 <div class="nav-label">Home</div>
650 </a>
651 <a href="#search" class="nav-item">
652 <div class="nav-icon">🔍</div>
653 <div class="nav-label">Search</div>
654 </a>
655 <a href="#favorites" class="nav-item">
656 <div class="nav-icon">❤️</div>
657 <div class="nav-label">Favorites</div>
658 </a>
659 <a href="#profile" class="nav-item">
660 <div class="nav-icon">👤</div>
661 <div class="nav-label">Profile</div>
662 </a>
663 </nav>
664
665 <!-- Screen reader announcements -->
666 <div id="announcements" class="sr-only" aria-live="polite" aria-atomic="true"></div>
667
668 <script>
669 document.addEventListener('DOMContentLoaded', function() {
670 console.log('👆 Touch-Friendly Interface Demo Loaded');
671
672 // Touch feedback system
673 class TouchFeedback {
674 constructor() {
675 this.init();
676 }
677
678 init() {
679 document.addEventListener('touchstart', this.handleTouchStart.bind(this), { passive: true });
680 document.addEventListener('click', this.handleClick.bind(this));
681 }
682
683 handleTouchStart(e) {
684 const target = e.target.closest('.touch-btn, .icon-btn, .touch-checkbox');
685 if (target && !target.disabled) {
686 this.addRippleEffect(target, e.touches[0]);
687 this.addHapticFeedback();
688 }
689 }
690
691 handleClick(e) {
692 const target = e.target.closest('.touch-btn, .icon-btn');
693 if (target && !target.disabled) {
694 this.announceAction(target);
695 }
696 }
697
698 addRippleEffect(element, touch) {
699 const rect = element.getBoundingClientRect();
700 const x = touch.clientX - rect.left;
701 const y = touch.clientY - rect.top;
702
703 const ripple = document.createElement('div');
704 ripple.className = 'touch-ripple';
705 ripple.style.left = x + 'px';
706 ripple.style.top = y + 'px';
707
708 element.style.position = 'relative';
709 element.style.overflow = 'hidden';
710 element.appendChild(ripple);
711
712 setTimeout(() => ripple.remove(), 600);
713 }
714
715 addHapticFeedback() {
716 if (navigator.vibrate) {
717 navigator.vibrate(10);
718 }
719 }
720
721 announceAction(element) {
722 const announcer = document.getElementById('announcements');
723 const text = element.textContent || element.getAttribute('aria-label');
724 if (announcer && text) {
725 announcer.textContent = text + ' activated';
726 }
727 }
728 }
729
730 // Gesture handling
731 class GestureHandler {
732 constructor() {
733 this.gestures = new Map();
734 this.longPressTimer = null;
735 this.longPressDuration = 500;
736 this.swipeThreshold = 50;
737 this.doubleTapDelay = 300;
738 this.lastTap = 0;
739
740 this.init();
741 }
742
743 init() {
744 document.addEventListener('touchstart', this.handleStart.bind(this), { passive: false });
745 document.addEventListener('touchmove', this.handleMove.bind(this), { passive: false });
746 document.addEventListener('touchend', this.handleEnd.bind(this), { passive: false });
747 document.addEventListener('touchcancel', this.handleCancel.bind(this));
748 }
749
750 handleStart(e) {
751 const target = e.target.closest('[data-swipe], [data-long-press], [data-double-tap], #gestureDemo');
752 if (!target) return;
753
754 const touch = e.touches[0];
755 const gestureData = {
756 startX: touch.clientX,
757 startY: touch.clientY,
758 startTime: Date.now(),
759 target: target,
760 moved: false
761 };
762
763 this.gestures.set(touch.identifier, gestureData);
764
765 // Start long press timer
766 if (target.hasAttribute('data-long-press') || target.id === 'gestureDemo') {
767 this.startLongPress(target);
768 }
769 }
770
771 handleMove(e) {
772 for (let touch of e.changedTouches) {
773 const gestureData = this.gestures.get(touch.identifier);
774 if (!gestureData) continue;
775
776 const deltaX = touch.clientX - gestureData.startX;
777 const deltaY = touch.clientY - gestureData.startY;
778 const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
779
780 if (distance > 10) {
781 gestureData.moved = true;
782 this.cancelLongPress();
783
784 // Handle swipe preview
785 if (gestureData.target.hasAttribute('data-swipe')) {
786 this.handleSwipePreview(gestureData.target, deltaX);
787 }
788 }
789 }
790 }
791
792 handleEnd(e) {
793 for (let touch of e.changedTouches) {
794 const gestureData = this.gestures.get(touch.identifier);
795 if (!gestureData) continue;
796
797 const deltaX = touch.clientX - gestureData.startX;
798 const deltaY = touch.clientY - gestureData.startY;
799 const deltaTime = Date.now() - gestureData.startTime;
800 const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
801
802 this.cancelLongPress();
803
804 // Handle different gesture types
805 if (!gestureData.moved && deltaTime < this.doubleTapDelay) {
806 this.handleTapOrDoubleTap(gestureData.target);
807 } else if (distance > this.swipeThreshold) {
808 this.handleSwipe(gestureData.target, deltaX, deltaY);
809 }
810
811 // Reset swipe transform
812 if (gestureData.target.hasAttribute('data-swipe')) {
813 gestureData.target.style.transform = '';
814 gestureData.target.classList.remove('swiping');
815 }
816
817 this.gestures.delete(touch.identifier);
818 }
819 }
820
821 handleCancel(e) {
822 for (let touch of e.changedTouches) {
823 this.gestures.delete(touch.identifier);
824 }
825 this.cancelLongPress();
826 }
827
828 startLongPress(target) {
829 let progress = 0;
830 const progressStep = 50;
831
832 target.classList.add('pressing');
833
834 const progressInterval = setInterval(() => {
835 progress += progressStep;
836 const percentage = (progress / this.longPressDuration) * 100;
837 target.style.setProperty('--progress', Math.min(percentage, 100) + '%');
838 }, progressStep);
839
840 this.longPressTimer = setTimeout(() => {
841 clearInterval(progressInterval);
842 this.handleLongPress(target);
843 }, this.longPressDuration);
844
845 // Store interval for cleanup
846 this.progressInterval = progressInterval;
847 }
848
849 cancelLongPress() {
850 if (this.longPressTimer) {
851 clearTimeout(this.longPressTimer);
852 this.longPressTimer = null;
853 }
854
855 if (this.progressInterval) {
856 clearInterval(this.progressInterval);
857 this.progressInterval = null;
858 }
859
860 document.querySelectorAll('.pressing').forEach(el => {
861 el.classList.remove('pressing');
862 el.style.removeProperty('--progress');
863 });
864 }
865
866 handleTapOrDoubleTap(target) {
867 const now = Date.now();
868 const timeSinceLastTap = now - this.lastTap;
869
870 if (timeSinceLastTap < this.doubleTapDelay && timeSinceLastTap > 0) {
871 // Double tap
872 if (target.hasAttribute('data-double-tap')) {
873 this.handleDoubleTap(target);
874 } else if (target.id === 'gestureDemo') {
875 this.updateGestureDemo('Double tap detected! 👍');
876 }
877 } else {
878 // Single tap
879 if (target.id === 'gestureDemo') {
880 this.updateGestureDemo('Single tap detected! 👆');
881 }
882 }
883
884 this.lastTap = now;
885 }
886
887 handleLongPress(target) {
888 target.classList.remove('pressing');
889 target.style.removeProperty('--progress');
890
891 if (target.id === 'gestureDemo') {
892 this.updateGestureDemo('Long press detected! ⏰');
893 } else {
894 console.log('Long press on:', target);
895 this.announceGesture('Long press activated');
896 }
897 }
898
899 handleDoubleTap(target) {
900 const action = target.getAttribute('data-double-tap');
901 console.log('Double tap action:', action);
902 this.announceGesture('Double tap: ' + action);
903
904 // Visual feedback
905 target.style.animation = 'none';
906 target.offsetHeight;
907 target.style.animation = 'pulse 0.3s ease';
908 }
909
910 handleSwipe(target, deltaX, deltaY) {
911 const direction = Math.abs(deltaX) > Math.abs(deltaY)
912 ? (deltaX > 0 ? 'right' : 'left')
913 : (deltaY > 0 ? 'down' : 'up');
914
915 if (target.id === 'gestureDemo') {
916 this.updateGestureDemo(`Swipe ${direction} detected! 👋`);
917 } else if (target.hasAttribute('data-swipe')) {
918 console.log('Swipe', direction, 'on card');
919 this.announceGesture(`Swiped ${direction}`);
920 }
921 }
922
923 handleSwipePreview(target, deltaX) {
924 target.classList.add('swiping');
925 target.style.transform = `translateX(${Math.min(Math.abs(deltaX), 100) * Math.sign(deltaX)}px)`;
926 }
927
928 updateGestureDemo(message) {
929 const feedback = document.getElementById('gesturefeedback');
930 if (feedback) {
931 feedback.textContent = message;
932 setTimeout(() => {
933 feedback.textContent = '';
934 }, 3000);
935 }
936 }
937
938 announceGesture(message) {
939 const announcer = document.getElementById('announcements');
940 if (announcer) {
941 announcer.textContent = message;
942 }
943 }
944 }
945
946 // Bottom navigation
947 class BottomNavigation {
948 constructor() {
949 this.navItems = document.querySelectorAll('.nav-item');
950 this.init();
951 }
952
953 init() {
954 this.navItems.forEach(item => {
955 item.addEventListener('click', (e) => {
956 e.preventDefault();
957 this.setActive(item);
958 });
959 });
960 }
961
962 setActive(activeItem) {
963 this.navItems.forEach(item => item.classList.remove('active'));
964 activeItem.classList.add('active');
965
966 const label = activeItem.querySelector('.nav-label').textContent;
967 console.log('Navigated to:', label);
968
969 const announcer = document.getElementById('announcements');
970 if (announcer) {
971 announcer.textContent = label + ' selected';
972 }
973 }
974 }
975
976 // Form enhancements
977 class FormEnhancements {
978 constructor() {
979 this.init();
980 }
981
982 init() {
983 // Add real-time validation feedback
984 const inputs = document.querySelectorAll('.form-input');
985 inputs.forEach(input => {
986 input.addEventListener('blur', this.validateInput.bind(this));
987 input.addEventListener('input', this.clearValidation.bind(this));
988 });
989
990 // Handle form submission
991 const form = document.querySelector('form');
992 if (form) {
993 form.addEventListener('submit', this.handleSubmit.bind(this));
994 }
995 }
996
997 validateInput(e) {
998 const input = e.target;
999 const isValid = input.checkValidity();
1000
1001 input.style.borderColor = isValid ? '#34C759' : '#FF3B30';
1002
1003 if (!isValid) {
1004 this.announceError(input.validationMessage);
1005 }
1006 }
1007
1008 clearValidation(e) {
1009 const input = e.target;
1010 input.style.borderColor = '';
1011 }
1012
1013 handleSubmit(e) {
1014 e.preventDefault();
1015
1016 const submitBtn = e.target.querySelector('button[type="submit"]');
1017 submitBtn.textContent = 'Submitting...';
1018 submitBtn.disabled = true;
1019
1020 setTimeout(() => {
1021 submitBtn.textContent = 'Success!';
1022 submitBtn.style.background = '#34C759';
1023
1024 setTimeout(() => {
1025 submitBtn.textContent = 'Submit Form';
1026 submitBtn.disabled = false;
1027 submitBtn.style.background = '';
1028 }, 2000);
1029 }, 1000);
1030 }
1031
1032 announceError(message) {
1033 const announcer = document.getElementById('announcements');
1034 if (announcer) {
1035 announcer.textContent = 'Validation error: ' + message;
1036 }
1037 }
1038 }
1039
1040 // Performance monitoring
1041 function monitorPerformance() {
1042 let touchCount = 0;
1043 let frameCount = 0;
1044 let lastTime = performance.now();
1045
1046 document.addEventListener('touchstart', () => {
1047 touchCount++;
1048 });
1049
1050 function countFrames() {
1051 frameCount++;
1052 const currentTime = performance.now();
1053
1054 if (currentTime - lastTime >= 5000) {
1055 const fps = Math.round(frameCount / 5);
1056 console.log(`📊 Performance: ${fps} FPS, ${touchCount} touches`);
1057
1058 touchCount = 0;
1059 frameCount = 0;
1060 lastTime = currentTime;
1061 }
1062
1063 requestAnimationFrame(countFrames);
1064 }
1065
1066 requestAnimationFrame(countFrames);
1067 }
1068
1069 // Initialize all systems
1070 new TouchFeedback();
1071 new GestureHandler();
1072 new BottomNavigation();
1073 new FormEnhancements();
1074 monitorPerformance();
1075
1076 console.log('🚀 Touch-friendly interface initialized');
1077 console.log('👆 Try tapping, swiping, and long pressing elements');
1078 });
1079
1080 // Add pulse animation for double-tap feedback
1081 const style = document.createElement('style');
1082 style.textContent = `
1083 @keyframes pulse {
1084 0% { transform: scale(1); }
1085 50% { transform: scale(1.05); }
1086 100% { transform: scale(1); }
1087 }
1088 `;
1089 document.head.appendChild(style);
1090 </script>
1091</body>
1092</html>This comprehensive touch-friendly interface demonstrates essential mobile UX patterns:
Key Features Implemented:
JavaScript Classes:
The implementation follows platform guidelines (iOS Human Interface Guidelines, Material Design) while ensuring cross-platform compatibility and accessibility compliance.
In the next lesson, we'll explore responsive tables and data visualization, learning how to transform complex data tables into mobile-friendly formats and create effective data presentation patterns for small screens.
Your progress is saved in this browser, on this device only.