Loading...
Loading...
Mobile-specific UX principles - touch targets, hover-less doctrine, thumb zones, safe areas, gestures, mobile perf budgets. Cross-platform (web mobile, iOS, Android).
npx skill4agent add athevon/genjutsu mobile-principlesTouch-first UX context. Loaded when mobile is detected (web mobile, iOS, Android). Concise rules here. Deep-dive in.references/
| Platform | Minimum | Recommended | Spec |
|---|---|---|---|
| Apple iOS | 44pt | 44pt + 8pt spacing | Apple HIG |
| Android | 48dp | 48dp + 8dp spacing | Material Design |
| Web mobile | 44px | 44px + 8px spacing | WCAG 2.5.5 |
hitSlop:hover.card { opacity: 1; transform: translateY(0); }
@media (hover: hover) and (pointer: fine) {
.card { opacity: 0.85; }
.card:hover { opacity: 1; transform: translateY(-2px); }
}Image(systemName: "heart")
.onTapGesture { toggleLike() }
.contextMenu {
Button("Share", systemImage: "square.and.arrow.up", action: share)
Button("Report", systemImage: "flag", role: .destructive, action: report)
}Box(
modifier = Modifier
.combinedClickable(
onClick = { toggleLike() },
onLongClick = { showContextMenu() },
)
) {
Icon(Icons.Default.Favorite, contentDescription = "Like")
}+------+----+------+
| HARD | OK | HARD | <- top: stretch, two-handed only
+------+----+------+
| OK | OK | OK | <- middle: comfortable
+------+----+------+
| EASY |EASY| EASY | <- bottom: natural thumb arc
+------+----+------+| Platform | API | Insets respected |
|---|---|---|
| Web | `env(safe-area-inset-top | right |
| SwiftUI | | nav bar, tab bar, notch, home |
| Compose | | system bars, IME, cutouts |
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">.fab {
position: fixed;
bottom: calc(env(safe-area-inset-bottom) + 16px);
right: calc(env(safe-area-inset-right) + 16px);
}ScrollView { content }
.safeAreaInset(edge: .bottom) {
PrimaryCTA().padding()
}Column(
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.safeDrawing)
) { /* content */ }| Platform | API |
|---|---|
| Web CSS | |
| Web JS | |
| SwiftUI | |
| UIKit | |
| Compose | Custom helper using |
struct Hero: View {
@Environment(\.accessibilityReduceMotion) var reduceMotion
@State private var shown = false
var body: some View {
Text("Welcome")
.opacity(shown ? 1 : 0)
.offset(y: shown ? 0 : (reduceMotion ? 0 : 20))
.animation(reduceMotion ? .none : .easeOut(duration: 0.3), value: shown)
.onAppear { shown = true }
}
}let duration = UIAccessibility.isReduceMotionEnabled ? 0 : 0.3
UIView.animate(withDuration: duration) {
view.alpha = 1
view.transform = .identity
}@Composable
fun rememberReduceMotion(): Boolean {
val context = LocalContext.current
return remember {
Settings.Global.getFloat(
context.contentResolver,
Settings.Global.ANIMATOR_DURATION_SCALE,
1f,
) == 0f
}
}
val reduceMotion = rememberReduceMotion()
val spec = if (reduceMotion) snap() else tween<Float>(durationMillis = 300)Note: prefer(API 26+) - it returnsValueAnimator.areAnimatorsEnabled()when the animator duration scale is 0, which the developer-options "Animation off" toggle, Battery Saver, and the user-facing "Remove animations" (Settings -> Accessibility) toggle all set. Deep dive infalse.references/accessibility-mobile.md
WorkManagerBGTaskSchedulerSave-Datanavigator.connection.saveDataURLSessionConfiguration.allowsCellularAccessNWPathMonitorConnectivityManagerNetworkCapabilities/* BAD - on mobile the action button literally never appears */
.card .actions { opacity: 0; }
.card:hover .actions { opacity: 1; }/* GOOD - visible by default, hover is a desktop enhancement */
.card .actions { opacity: 1; }
@media (hover: hover) and (pointer: fine) {
.card .actions { opacity: 0; transition: opacity 0.15s ease-out; }
.card:hover .actions { opacity: 1; }
}// BAD - 32dp icon button, mistappable, fails Material guideline
IconButton(
onClick = onDelete,
modifier = Modifier.size(32.dp),
) { Icon(Icons.Default.Delete, contentDescription = "Delete") }// GOOD - 48dp minimum even when the visible icon is 24dp
IconButton(
onClick = onDelete,
modifier = Modifier.size(48.dp),
) {
Icon(
Icons.Default.Delete,
contentDescription = "Delete",
modifier = Modifier.size(24.dp),
)
}// BAD - the CTA sits under the home indicator on every modern iPhone
VStack {
Spacer()
Button("Continue", action: next)
.frame(maxWidth: .infinity)
.padding()
}// GOOD - safeAreaInset keeps the button reachable and visible
ScrollView { content }
.safeAreaInset(edge: .bottom) {
Button("Continue", action: next)
.frame(maxWidth: .infinity)
.padding()
}| Need | Load |
|---|---|
| Gesture deep-dive | |
| Mobile a11y deep-dive | |
| Compose-specific anim | |
| SwiftUI-specific anim | |