关于 mobile-ux-optimizer
mobile-ux-optimizer 是面向移动端网页设计与开发的技能,提供触控友好、响应式和高性能体验的实现指导。内容覆盖视口与安全区域、导航、手势和设备测试。
主要功能
- 01移动优先响应式布局
- 02视口与安全区域处理
- 03触控目标和移动导航
- 04滑动与下拉刷新手势
- 05移动性能优化
适用场景
- 01修复地址栏导致的视口裁切
- 02为刘海和主屏指示条留出空间
- 03设计移动导航和触控交互
- 04改善移动端加载与滚动体验
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ curiositech/ some_claude_skills --skill mobile-ux-optimizer安装后在 Claude Code 中输入/mobile-ux-optimizer确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
.claude-plugin/plugin.jsonreferences/gestures.mdnavigation.md详细介绍
移动优先的 UX 优化
构建经过触控优化、性能出色的移动端体验,正确处理视口并采用响应式模式。
适用场景
✅ 以下场景请使用此技能:
- 视口问题(
100vh问题、安全区域、刘海屏) - 触控目标的尺寸和间距
- 移动端导航模式(底部导航、抽屉、汉堡菜单)
- 滑动手势和下拉刷新
- 响应式断点策略
- 移动端性能优化
❌ 以下场景请勿使用:
- 原生应用开发 → 使用
react-native或swift-executor技能 - 仅限桌面端的功能 → 无需使用技能,采用标准模式即可
- 通用 CSS/Tailwind 问题 → 使用 Tailwind 文档或
web-design-expert - PWA 安装/Service Worker → 使用
pwa-expert技能
核心原则
移动优先意味着逐步增强,而非逐步简化
/* ❌ ANTI-PATTERN: Desktop-first (scale down) */
.card { width: 400px; }
@media (max-width: 768px) { .card { width: 100%; } }
/* ✅ CORRECT: Mobile-first (scale up) */
.card { width: 100%; }
@media (min-width: 768px) { .card { width: 400px; } }
44px 规则
Apple 的人机界面指南规定,最小触控目标为 44×44 点。Google Material 建议为 48×48dp。
// Touch-friendly button
<button className="min-h-[44px] min-w-[44px] px-4 py-3">
Tap me
</button>
// Touch-friendly link with adequate padding
<a href="/page" className="inline-block py-3 px-4">
Link text
</a>
视口处理
dvh 方案
移动浏览器具有动态工具栏。100vh 会包含 URL 栏,导致内容被截断。
/* ❌ ANTI-PATTERN: Content hidden behind browser UI */
.full-screen { height: 100vh; }
/* ✅ CORRECT: Responds to browser chrome */
.full-screen { height: 100dvh; }
/* Fallback for older browsers */
.full-screen {
height: 100vh;
height: 100dvh;
}
安全区域内边距(刘海屏和主屏幕指示器)
/* Handle iPhone notch and home indicator */
.bottom-nav {
padding-bottom: env(safe-area-inset-bottom, 0);
}
.header {
padding-top: env(safe-area-inset-top, 0);
}
/* Full safe area padding */
.safe-container {
padding: env(safe-area-inset-top)
env(safe-area-inset-right)
env(safe-area-inset-bottom)
env(safe-area-inset-left);
}
必需的 meta 标签:
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
Tailwind 安全区域类
// Custom Tailwind utilities (add to globals.css)
@layer utilities {
.pb-safe { padding-bottom: env(safe-area-inset-bottom); }
.pt-safe { padding-top: env(safe-area-inset-top); }
.h-screen-safe { height: calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom)); }
}
// Usage
<nav className="fixed bottom-0 pb-safe bg-leather-900">
<BottomNav />
</nav>
移动端导航模式
底部导航(推荐用于移动端)
// components/BottomNav.tsx
'use client';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
const navItems = [
{ href: '/', icon: HomeIcon, label: 'Home' },
{ href: '/meetings', icon: CalendarIcon, label: 'Meetings' },
{ href: '/tools', icon: ToolsIcon, label: 'Tools' },
{ href: '/my', icon: UserIcon, label: 'My Recovery' },
];
export function BottomNav() {
const pathname = usePathname();
return (
<nav className="fixed bottom-0 left-0 right-0 bg-leather-900 border-t border-leather-700 pb-safe">
<div className="flex justify-around">
{navItems.map(({ href, icon: Icon, label }) => {
const isActive = pathname === href || pathname.startsWith(`${href}/`);
return (
<Link
key={href}
href={href}
className={`
flex flex-col items-center py-2 px-3 min-h-[56px] min-w-[64px]
${isActive ? 'text-ember-400' : 'text-leather-400'}
`}
>
<Icon className="w-6 h-6" />
<span className="text-xs mt-1">{label}</span>
</Link>
);
})}
</div>
</nav>
);
}
滑出式抽屉(侧边菜单)
'use client';
import { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
interface DrawerProps {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
}
export function Drawer({ isOpen, onClose, children }: DrawerProps) {
// Prevent body scroll when open
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
}
return () => {
document.body.style.overflow = '';
};
}, [isOpen]);
// Close on escape
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}, [onClose]);
if (!isOpen) return null;
return createPortal(
<div className="fixed inset-0 z-50">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
onClick={onClose}
aria-hidden="true"
/>
{/* Drawer */}
<div
className="absolute left-0 top-0 h-full w-[280px] max-w-[80vw]
bg-leather-900 shadow-xl transform transition-transform
animate-slide-in-left"
role="dialog"
aria-modal="true"
>
<div className="h-full overflow-y-auto pt-safe pb-safe">
{children}
</div>
</div>
</div>,
document.body
);
}
触控手势
完整实现请见
references/gestures.md
| Hook | 用途 |
|---|---|
useSwipe() | 检测带方向的滑动手势,可配置阈值 |
usePullToRefresh() | 带视觉反馈和阻尼效果的下拉刷新 |
快速用法:
// Swipe to dismiss
const { handleTouchStart, handleTouchEnd } = useSwipe({
onSwipeLeft: () => dismiss(),
threshold: 50,
});
// Pull to refresh
const { containerRef, pullDistance, isRefreshing, handlers } =
usePullToRefresh(async () => await refetchData());
移动端性能
图像优化
import Image from 'next/image';
// Responsive images with proper sizing
<Image
src="/hero.jpg"
alt="Hero"
fill
sizes="(max-width: 768px) 100vw, 50vw"
priority // For above-the-fold images
className="object-cover"
/>
// Lazy load below-fold images
<Image
src="/feature.jpg"
alt="Feature"
width={400}
height={300}
loading="lazy"
/>
减小构建包体积
// Dynamic imports for heavy components
const HeavyChart = dynamic(() => import('@/components/Chart'), {
loading: () => <ChartSkeleton />,
ssr: false, // Skip server render for client-only
});
// Lazy load below-fold sections
const Comments = dynamic(() => import('@/components/Comments'));
骨架屏(而非加载旋转图标)
// Skeleton that matches final content layout
function MeetingCardSkeleton() {
return (
<div className="p-4 bg-leather-800 rounded-lg animate-pulse">
<div className="h-4 bg-leather-700 rounded w-3/4 mb-2" />
<div className="h-3 bg-leather-700 rounded w-1/2 mb-4" />
<div className="flex gap-2">
<div className="h-6 w-16 bg-leather-700 rounded" />
<div className="h-6 w-16 bg-leather-700 rounded" />
</div>
</div>
);
}
// Usage
{isLoading ? (
<div className="space-y-4">
{[...Array(5)].map((_, i) => <MeetingCardSkeleton key={i} />)}
</div>
) : (
meetings.map(m => <MeetingCard key={m.id} meeting={m} />)
)}
响应式模式
Tailwind 断点策略
sm: 640px - Large phones (landscape)
md: 768px - Tablets
lg: 1024px - Small laptops
xl: 1280px - Desktops
2xl: 1536px - Large screens
// Mobile: stack, Tablet+: side-by-side
<div className="flex flex-col md:flex-row gap-4">
<aside className="w-full md:w-64">Sidebar</aside>
<main className="flex-1">Content</main>
</div>
// Mobile: bottom nav, Desktop: sidebar
<nav className="md:hidden fixed bottom-0 left-0 right-0">
<BottomNav />
</nav>
<aside className="hidden md:block w-64">
<SidebarNav />
</aside>
容器查询(纯 CSS 响应式组件)
/* Component responds to its container, not viewport */
@container (min-width: 400px) {
.card { flex-direction: row; }
}
<div className="@container">
<div className="flex flex-col @md:flex-row">
{/* Responds to parent container width */}
</div>
</div>
在真实设备上测试
Chrome DevTools 移动端模拟
- 打开 DevTools(F12)
- 切换设备工具栏(Ctrl+Shift+M)
- 选择设备或设置自定义尺寸
- 限制网络/CPU 速度,以模拟真实性能
必测场景
- 内容不会被刘海屏/主屏幕指示器截断
- 触控目标至少为 44×44px
- 滚动流畅(无卡顿)
- 底部导航不会遮挡内容
- 虚拟键盘显示时表单仍可正常使用
- 横屏方向下正常显示
- 下拉刷新不会与滚动操作冲突
BrowserStack/真实设备测试
# Expose local dev server to internet
npx localtunnel --port 3000
# or
ngrok http 3000
快速参考
| 问题 | 解决方案 |
|---|---|
| 底部内容被截断 | 使用 100dvh 替代 100vh |
| 刘海屏遮挡内容 | 添加 pt-safe / pb-safe |
| 触控目标太小 | 最小尺寸为 44×44px |
| 滚动被锁定 | 检查 body 上的 overflow: hidden |
| 键盘遮挡输入框 | 使用 visualViewport API |
| 滚动卡顿 | 使用 will-change: transform |
| 双击缩放 | 添加 touch-action: manipulation |
参考资料
详细指南请见 /references/:
keyboard-handling.md- 虚拟键盘和表单 UXanimations.md- 适合触控操作的动画accessibility.md- 移动端无障碍要求
评论
支持 Markdown