recovery-app-onboarding

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Recovery App Onboarding Excellence

康复类应用新手引导设计最佳实践

Build compassionate, effective onboarding experiences for recovery and wellness applications that serve vulnerable populations with dignity and practical utility.
为服务弱势群体的康复与健康类应用构建兼具人文关怀与实用价值的新手引导(Onboarding)体验。

When to Use

适用场景

USE this skill for:
  • First-time user onboarding flows
  • Feature discovery and app tours
  • Progressive disclosure design
  • Permission request timing and framing
  • Welcome screens and value propositions
  • Recovery program selection flows
  • Crisis resource integration
  • Privacy and anonymity communication
DO NOT use for:
  • General mobile responsive design → use
    mobile-ux-optimizer
  • Marketing/conversion optimization → use
    seo-visibility-expert
  • Native iOS/Android development → use platform-specific skills
  • Database schema design → use
    supabase-admin
推荐使用本技能的场景:
  • 首次用户新手引导流程
  • 功能探索与应用导览
  • 渐进式信息披露设计
  • 权限请求的时机与话术设计
  • 欢迎界面与价值主张呈现
  • 康复项目选择流程
  • 危机资源整合
  • 隐私与匿名性说明
请勿使用本技能的场景:
  • 通用移动端响应式设计 → 使用
    mobile-ux-optimizer
  • 营销/转化优化 → 使用
    seo-visibility-expert
  • 原生iOS/Android开发 → 使用平台专属技能
  • 数据库 schema 设计 → 使用
    supabase-admin

Core Principles

核心原则

1. Compassion First, Features Second

1. 人文关怀优先,功能次之

Recovery users are often in vulnerable states. Every onboarding decision must consider:
❌ ANTI-PATTERN: "Sign up to track your progress!"
✅ CORRECT: "You're taking a brave step. Let's set up a private space for your journey."

❌ ANTI-PATTERN: Requiring account creation before showing any value
✅ CORRECT: Let users explore core features anonymously, then offer accounts for persistence
康复类应用的用户往往处于脆弱状态。每一个新手引导决策都必须考虑:
❌ 反模式:“注册账号以跟踪你的进度!”
✅ 正确表述:“你正在迈出勇敢的一步。让我们为你的旅程搭建一个私密空间。”

❌ 反模式:要求用户先创建账号才能查看任何内容
✅ 正确做法:允许用户匿名探索核心功能,再提供账号选项以保存数据

2. Value Before Commitment

2. 先展示价值,再请求承诺

Show meaningful value before asking for personal information:
WRONG ORDER:
1. Create account
2. Enter personal details
3. Grant permissions
4. Finally see the app

RIGHT ORDER:
1. Show immediate value (meeting finder, crisis resources)
2. Demonstrate app benefits
3. Offer optional account for saved data
4. Request permissions contextually
在要求用户提供个人信息前,先展示应用的实际价值:
错误顺序:
1. 创建账号
2. 填写个人信息
3. 授予权限
4. 终于能看到应用内容

正确顺序:
1. 展示即时价值(如会议查找器、危机资源)
2. 演示应用优势
3. 提供可选的账号功能以保存数据
4. 结合场景请求权限

3. Non-Judgmental Language

3. 使用无评判性语言

Avoid shame-inducing or triggering language:
❌ "How many days since your last relapse?"
✅ "What's your current sobriety date?"

❌ "You failed to complete..."
✅ "No worries—pick up where you left off"

❌ "Are you an alcoholic or drug addict?"
✅ "Which recovery programs interest you?"
避免使用带有羞耻感或触发负面情绪的语言:
❌ “你上次复吸距今多少天?”
✅ “你当前的 sobriety 起始日期是哪天?”

❌ “你未能完成...“
✅ “没关系——从你上次中断的地方继续即可”

❌ “你是酗酒者还是吸毒成瘾者?”
✅ “你对哪些康复项目感兴趣?”

Mobile Onboarding UX (2025 Best Practices)

移动应用新手引导UX(2025最佳实践)

Progressive Disclosure

渐进式信息披露

Break complex onboarding into digestible stages:
tsx
// ✅ GOOD: Staged onboarding with clear progress
const ONBOARDING_STAGES = [
  { id: 'welcome', required: false },    // Emotional connection
  { id: 'programs', required: false },    // Personalization
  { id: 'features', required: false },    // Value discovery
  { id: 'preferences', required: false }, // Customization
  { id: 'safety', required: false },      // Crisis setup
];

// Each stage should be:
// - Skippable (never trap users)
// - Under 60 seconds to complete
// - Visually distinct with progress indicators
// - Reversible (can go back)
将复杂的新手引导拆分为易于消化的阶段:
tsx
// ✅ 优秀示例:分阶段引导,进度清晰
const ONBOARDING_STAGES = [
  { id: 'welcome', required: false },    // 情感连接
  { id: 'programs', required: false },    // 个性化设置
  { id: 'features', required: false },    // 价值展示
  { id: 'preferences', required: false }, // 自定义配置
  { id: 'safety', required: false },      // 危机设置
];

// 每个阶段应满足:
// - 可跳过(绝不困住用户)
// - 完成时间不超过60秒
// - 视觉区分明显,带有进度指示器
// - 可回退(能返回上一阶段)

Permission Priming (28% Higher Grant Rate)

权限请求前置说明(授权率提升28%)

Never request permissions out of context:
tsx
// ❌ BAD: Immediate system permission dialog
useEffect(() => {
  requestLocationPermission();
}, []);

// ✅ GOOD: Contextual priming before system dialog
function MeetingSearchPrimer() {
  return (
    <div className="p-4 bg-blue-50 rounded-lg mb-4">
      <MapPin className="text-blue-500 mb-2" />
      <h3>Find Meetings Near You</h3>
      <p className="text-sm text-gray-600 mb-3">
        To show meetings within walking distance, we need your location.
        Your location stays on your device.
      </p>
      <button onClick={handleEnableLocation} className="btn-primary">
        Enable Location
      </button>
      <button onClick={handleSkip} className="btn-text">
        Search by ZIP instead
      </button>
    </div>
  );
}
切勿脱离场景直接请求系统权限:
tsx
// ❌ 错误示例:立即弹出系统权限对话框
useEffect(() => {
  requestLocationPermission();
}, []);

// ✅ 正确示例:在系统对话框前先结合场景说明
function MeetingSearchPrimer() {
  return (
    <div className="p-4 bg-blue-50 rounded-lg mb-4">
      <MapPin className="text-blue-500 mb-2" />
      <h3>查找你附近的互助会议</h3>
      <p className="text-sm text-gray-600 mb-3">
        为了向你展示步行可达范围内的会议,我们需要获取你的位置信息。你的位置信息仅存储在你的设备上。
      </p>
      <button onClick={handleEnableLocation} className="btn-primary">
        启用位置权限
      </button>
      <button onClick={handleSkip} className="btn-text">
        改为按邮编搜索
      </button>
    </div>
  );
}

"Everboarding" - Beyond First Launch

“持续引导” - 不止于首次启动

Onboarding isn't a one-time event:
tsx
// Feature discovery on first use of each feature
function useFeatureOnboarding(featureId: string) {
  const [hasSeenTooltip, setHasSeen] = useLocalStorage(`onboard:${featureId}`, false);

  return {
    showTooltip: !hasSeenTooltip,
    dismissTooltip: () => setHasSeen(true),
  };
}

// Example: First time using gratitude journal
function GratitudeJournal() {
  const { showTooltip, dismissTooltip } = useFeatureOnboarding('gratitude');

  return (
    <>
      {showTooltip && (
        <FeatureTooltip
          title="Daily Gratitude"
          description="Write 3 things you're grateful for. Research shows this builds resilience."
          onDismiss={dismissTooltip}
        />
      )}
      {/* Journal UI */}
    </>
  );
}
新手引导并非一次性事件:
tsx
// 首次使用某功能时展示功能引导
function useFeatureOnboarding(featureId: string) {
  const [hasSeenTooltip, setHasSeen] = useLocalStorage(`onboard:${featureId}`, false);

  return {
    showTooltip: !hasSeenTooltip,
    dismissTooltip: () => setHasSeen(true),
  };
}

// 示例:首次使用感恩日记功能
function GratitudeJournal() {
  const { showTooltip, dismissTooltip } = useFeatureOnboarding('gratitude');

  return (
    <>
      {showTooltip && (
        <FeatureTooltip
          title="每日感恩"
          description="写下3件你心怀感恩的事。研究表明这能提升心理韧性。"
          onDismiss={dismissTooltip}
        />
      )}
      {/* 日记UI */}
    </>
  );
}

Skeleton Loaders, Never Spinners

骨架屏加载,而非加载动画

Recovery users in crisis need immediate feedback:
tsx
// ❌ BAD: Spinner creates anxiety
{isLoading && <Loader2 className="animate-spin" />}

// ✅ GOOD: Skeleton shows structure immediately
{isLoading && <MeetingCardSkeleton count={5} />}

// Skeleton implementation
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>
  );
}
处于危机状态的康复用户需要即时反馈:
tsx
// ❌ 错误示例:加载动画会引发焦虑
{isLoading && <Loader2 className="animate-spin" />}

// ✅ 正确示例:骨架屏立即展示页面结构
{isLoading && <MeetingCardSkeleton count={5} />}

// 骨架屏实现
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>
  );
}

Recovery App Feature Categories

康复类应用功能分类

Essential Features (Showcase First)

核心功能(优先展示)

  1. Meeting Finder - Core utility, immediate value
  2. Crisis Resources - Life-saving, always accessible
  3. Safety Planning - Personal support network
  1. 会议查找器 - 核心实用功能,即时体现价值
  2. 危机资源 - 救命功能,随时可访问
  3. 安全计划 - 个人支持网络

Engagement Features (Second Priority)

互动功能(次优先)

  1. Daily Check-ins - HALT awareness
  2. Recovery Journal - Reflection and growth
  3. Gratitude Practice - Positive reinforcement
  1. 每日签到 - HALT意识提醒
  2. 康复日记 - 反思与成长
  3. 感恩练习 - 正向强化

Advanced Features (Discover Over Time)

进阶功能(逐步探索)

  1. Sobriety Counter - Milestone tracking
  2. Community Forum - Peer support
  3. Recovery Plan - AI-assisted guidance
  4. Online Meetings - Virtual options
  1. ** sobriety 计数器** - 里程碑追踪
  2. 社区论坛 - 同伴支持
  3. 康复计划 - AI辅助指导
  4. 线上会议 - 虚拟选项

Feature Card Pattern

功能卡片模式

tsx
interface OnboardingFeature {
  id: string;
  icon: React.ComponentType;
  title: string;
  description: string;
  highlight: string;        // Key benefit (e.g., "24/7 support available")
  color: string;            // Brand color for visual distinction
  category: 'essential' | 'engagement' | 'advanced';
}

const FEATURES: OnboardingFeature[] = [
  {
    id: 'meetings',
    icon: MapPin,
    title: 'Find Meetings Near You',
    description: 'Search thousands of AA, NA, CMA, SMART, and other recovery meetings.',
    highlight: '100,000+ meetings nationwide',
    color: 'bg-blue-500',
    category: 'essential',
  },
  {
    id: 'crisis',
    icon: Phone,
    title: 'Crisis Resources',
    description: 'One-tap access to crisis hotlines and emergency support.',
    highlight: '24/7 support available',
    color: 'bg-red-500',
    category: 'essential',
  },
  // ... more features
];
tsx
interface OnboardingFeature {
  id: string;
  icon: React.ComponentType;
  title: string;
  description: string;
  highlight: string;        // 核心优势(例如:“24/7 支持随时可用”)
  color: string;            // 用于视觉区分的品牌色
  category: 'essential' | 'engagement' | 'advanced';
}

const FEATURES: OnboardingFeature[] = [
  {
    id: 'meetings',
    icon: MapPin,
    title: 'Find Meetings Near You',
    description: 'Search thousands of AA, NA, CMA, SMART, and other recovery meetings.',
    highlight: '100,000+ meetings nationwide',
    color: 'bg-blue-500',
    category: 'essential',
  },
  {
    id: 'crisis',
    icon: Phone,
    title: 'Crisis Resources',
    description: 'One-tap access to crisis hotlines and emergency support.',
    highlight: '24/7 support available',
    color: 'bg-red-500',
    category: 'essential',
  },
  // ... 更多功能
];

Crisis Resource Integration

危机资源整合

Critical: Only 45% of mental health apps include crisis resources. This is non-negotiable for recovery apps.
关键提示: 仅有45%的心理健康类应用整合了危机资源。这对于康复类应用而言是必不可少的。

Always-Visible Crisis Access

随时可见的危机入口

tsx
// Crisis resources should be accessible from EVERY screen
function Navigation() {
  return (
    <nav>
      {/* Normal nav items */}
      <CrisisButton className="fixed bottom-20 right-4" />
    </nav>
  );
}

// Crisis button design
function CrisisButton({ className }: { className?: string }) {
  return (
    <Link
      href="/crisis"
      className={cn(
        'flex items-center justify-center',
        'w-14 h-14 rounded-full',
        'bg-red-600 hover:bg-red-700',
        'shadow-lg shadow-red-600/30',
        'text-white',
        className
      )}
      aria-label="Crisis resources"
    >
      <Phone size={24} />
    </Link>
  );
}
tsx
// 危机资源应从每一个页面都能访问
function Navigation() {
  return (
    <nav>
      {/* 常规导航项 */}
      <CrisisButton className="fixed bottom-20 right-4" />
    </nav>
  );
}

// 危机按钮设计
function CrisisButton({ className }: { className?: string }) {
  return (
    <Link
      href="/crisis"
      className={cn(
        'flex items-center justify-center',
        'w-14 h-14 rounded-full',
        'bg-red-600 hover:bg-red-700',
        'shadow-lg shadow-red-600/30',
        'text-white',
        className
      )}
      aria-label="Crisis resources"
    >
      <Phone size={24} />
    </Link>
  );
}

Onboarding Safety Plan Step

新手引导中的安全计划步骤

tsx
// Include safety planning in onboarding, but make it optional
function SafetyPlanStep() {
  return (
    <div className="space-y-4">
      <h2>Your Safety Network</h2>
      <p className="text-gray-600">
        Having support contacts ready can make all the difference.
        This is optional—you can set this up anytime.
      </p>

      <EmergencyContactInput
        label="Emergency Contact"
        placeholder="Someone who can help in a crisis"
      />

      <SupportPersonInput
        label="Support Person"
        placeholder="Sponsor, therapist, or trusted friend"
      />

      <div className="flex gap-2 mt-6">
        <button className="btn-secondary flex-1" onClick={handleSkip}>
          Set Up Later
        </button>
        <button className="btn-primary flex-1" onClick={handleSave}>
          Save Contacts
        </button>
      </div>
    </div>
  );
}
tsx
// 在新手引导中加入安全计划步骤,但设为可选
function SafetyPlanStep() {
  return (
    <div className="space-y-4">
      <h2>你的安全支持网络</h2>
      <p className="text-gray-600">
        提前准备好支持联系人会带来很大帮助。这一步是可选的——你可以随时设置。
      </p>

      <EmergencyContactInput
        label="紧急联系人"
        placeholder="危机中可求助的人"
      />

      <SupportPersonInput
        label="支持人员"
        placeholder=" sponsor、治疗师或信任的朋友"
      />

      <div className="flex gap-2 mt-6">
        <button className="btn-secondary flex-1" onClick={handleSkip}>
          稍后设置
        </button>
        <button className="btn-primary flex-1" onClick={handleSave}>
          保存联系人
        </button>
      </div>
    </div>
  );
}

Recovery Program Selection

康复项目选择

Inclusive Multi-Selection

包容性多选设计

Support multiple recovery pathways without judgment:
tsx
const RECOVERY_PROGRAMS = [
  { id: 'aa', name: 'Alcoholics Anonymous', short: 'AA', color: 'bg-blue-500' },
  { id: 'na', name: 'Narcotics Anonymous', short: 'NA', color: 'bg-purple-500' },
  { id: 'cma', name: 'Crystal Meth Anonymous', short: 'CMA', color: 'bg-pink-500' },
  { id: 'smart', name: 'SMART Recovery', short: 'SMART', color: 'bg-green-500' },
  { id: 'dharma', name: 'Recovery Dharma', short: 'Dharma', color: 'bg-amber-500' },
  { id: 'ha', name: 'Heroin Anonymous', short: 'HA', color: 'bg-red-500' },
  { id: 'oa', name: 'Overeaters Anonymous', short: 'OA', color: 'bg-teal-500' },
  { id: 'other', name: 'Other/Multiple', short: 'Other', color: 'bg-gray-500' },
];

// UI should allow multiple selections
function ProgramSelection({ selected, onChange }) {
  return (
    <div className="grid grid-cols-2 gap-3">
      {RECOVERY_PROGRAMS.map((program) => (
        <ProgramCard
          key={program.id}
          program={program}
          isSelected={selected.includes(program.id)}
          onToggle={() => toggleProgram(program.id)}
        />
      ))}
    </div>
  );
}
支持多种康复路径,不带任何评判:
tsx
const RECOVERY_PROGRAMS = [
  { id: 'aa', name: 'Alcoholics Anonymous', short: 'AA', color: 'bg-blue-500' },
  { id: 'na', name: 'Narcotics Anonymous', short: 'NA', color: 'bg-purple-500' },
  { id: 'cma', name: 'Crystal Meth Anonymous', short: 'CMA', color: 'bg-pink-500' },
  { id: 'smart', name: 'SMART Recovery', short: 'SMART', color: 'bg-green-500' },
  { id: 'dharma', name: 'Recovery Dharma', short: 'Dharma', color: 'bg-amber-500' },
  { id: 'ha', name: 'Heroin Anonymous', short: 'HA', color: 'bg-red-500' },
  { id: 'oa', name: 'Overeaters Anonymous', short: 'OA', color: 'bg-teal-500' },
  { id: 'other', name: 'Other/Multiple', short: 'Other', color: 'bg-gray-500' },
];

// UI应支持多选
function ProgramSelection({ selected, onChange }) {
  return (
    <div className="grid grid-cols-2 gap-3">
      {RECOVERY_PROGRAMS.map((program) => (
        <ProgramCard
          key={program.id}
          program={program}
          isSelected={selected.includes(program.id)}
          onToggle={() => toggleProgram(program.id)}
        />
      ))}
    </div>
  );
}

Onboarding Flow Architecture

新手引导流程架构

State Management

状态管理

tsx
interface OnboardingState {
  currentStep: number;
  completedSteps: string[];
  selectedPrograms: string[];
  meetingPreferences: {
    formats: ('in-person' | 'online' | 'hybrid')[];
    days: string[];
    times: ('morning' | 'afternoon' | 'evening')[];
  };
  safetyContacts: {
    emergency?: string;
    support?: string;
  };
  skippedSteps: string[];
}

// Persist across sessions
function useOnboardingState() {
  return useLocalStorage<OnboardingState>('onboarding', defaultState);
}
tsx
interface OnboardingState {
  currentStep: number;
  completedSteps: string[];
  selectedPrograms: string[];
  meetingPreferences: {
    formats: ('in-person' | 'online' | 'hybrid')[];
    days: string[];
    times: ('morning' | 'afternoon' | 'evening')[];
  };
  safetyContacts: {
    emergency?: string;
    support?: string;
  };
  skippedSteps: string[];
}

// 跨会话持久化状态
function useOnboardingState() {
  return useLocalStorage<OnboardingState>('onboarding', defaultState);
}

Step Navigation

步骤导航

tsx
function OnboardingWizard() {
  const [state, setState] = useOnboardingState();
  const currentStep = STEPS[state.currentStep];

  const goNext = () => {
    setState(prev => ({
      ...prev,
      currentStep: Math.min(prev.currentStep + 1, STEPS.length - 1),
      completedSteps: [...prev.completedSteps, currentStep.id],
    }));
  };

  const goBack = () => {
    setState(prev => ({
      ...prev,
      currentStep: Math.max(prev.currentStep - 1, 0),
    }));
  };

  const skip = () => {
    setState(prev => ({
      ...prev,
      skippedSteps: [...prev.skippedSteps, currentStep.id],
    }));
    goNext();
  };

  return (
    <div className="min-h-screen flex flex-col">
      <ProgressIndicator
        current={state.currentStep}
        total={STEPS.length}
        completedSteps={state.completedSteps}
      />

      <main className="flex-1 p-4">
        <CurrentStepComponent {...currentStep} state={state} setState={setState} />
      </main>

      <footer className="p-4 border-t border-leather-700">
        <div className="flex gap-3">
          {state.currentStep > 0 && (
            <button onClick={goBack} className="btn-secondary flex-1">
              Back
            </button>
          )}
          {currentStep.skippable && (
            <button onClick={skip} className="btn-text">
              Skip
            </button>
          )}
          <button onClick={goNext} className="btn-primary flex-1">
            {state.currentStep === STEPS.length - 1 ? 'Get Started' : 'Continue'}
          </button>
        </div>
      </footer>
    </div>
  );
}
tsx
function OnboardingWizard() {
  const [state, setState] = useOnboardingState();
  const currentStep = STEPS[state.currentStep];

  const goNext = () => {
    setState(prev => ({
      ...prev,
      currentStep: Math.min(prev.currentStep + 1, STEPS.length - 1),
      completedSteps: [...prev.completedSteps, currentStep.id],
    }));
  };

  const goBack = () => {
    setState(prev => ({
      ...prev,
      currentStep: Math.max(prev.currentStep - 1, 0),
    }));
  };

  const skip = () => {
    setState(prev => ({
      ...prev,
      skippedSteps: [...prev.skippedSteps, currentStep.id],
    }));
    goNext();
  };

  return (
    <div className="min-h-screen flex flex-col">
      <ProgressIndicator
        current={state.currentStep}
        total={STEPS.length}
        completedSteps={state.completedSteps}
      />

      <main className="flex-1 p-4">
        <CurrentStepComponent {...currentStep} state={state} setState={setState} />
      </main>

      <footer className="p-4 border-t border-leather-700">
        <div className="flex gap-3">
          {state.currentStep > 0 && (
            <button onClick={goBack} className="btn-secondary flex-1">
              返回
            </button>
          )}
          {currentStep.skippable && (
            <button onClick={skip} className="btn-text">
              跳过
            </button>
          )}
          <button onClick={goNext} className="btn-primary flex-1">
            {state.currentStep === STEPS.length - 1 ? '开始使用' : '继续'}
          </button>
        </div>
      </footer>
    </div>
  );
}

Micro-Interactions

微交互

Entry Animations

入场动画

tsx
// Staggered entrance for lists
function StaggeredList({ items, renderItem }) {
  return (
    <div className="space-y-3">
      {items.map((item, index) => (
        <div
          key={item.id}
          className="animate-fade-in-up"
          style={{ animationDelay: `${index * 100}ms` }}
        >
          {renderItem(item, index)}
        </div>
      ))}
    </div>
  );
}
tsx
// 列表项 staggered 入场
function StaggeredList({ items, renderItem }) {
  return (
    <div className="space-y-3">
      {items.map((item, index) => (
        <div
          key={item.id}
          className="animate-fade-in-up"
          style={{ animationDelay: `${index * 100}ms` }}
        >
          {renderItem(item, index)}
        </div>
      ))}
    </div>
  );
}

Carousel Navigation

轮播导航

tsx
// Swipe gesture support for mobile feature tours
function useSwipeNavigation({ onNext, onPrev, threshold = 50 }) {
  const [touchStart, setTouchStart] = useState<number | null>(null);
  const [touchEnd, setTouchEnd] = useState<number | null>(null);

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

  const onTouchMove = (e: React.TouchEvent) => {
    setTouchEnd(e.targetTouches[0].clientX);
  };

  const onTouchEnd = () => {
    if (!touchStart || !touchEnd) return;
    const distance = touchStart - touchEnd;
    if (distance > threshold) onNext();
    if (distance < -threshold) onPrev();
  };

  return { onTouchStart, onTouchMove, onTouchEnd };
}
tsx
// 移动设备上支持滑动手势的功能导览
function useSwipeNavigation({ onNext, onPrev, threshold = 50 }) {
  const [touchStart, setTouchStart] = useState<number | null>(null);
  const [touchEnd, setTouchEnd] = useState<number | null>(null);

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

  const onTouchMove = (e: React.TouchEvent) => {
    setTouchEnd(e.targetTouches[0].clientX);
  };

  const onTouchEnd = () => {
    if (!touchStart || !touchEnd) return;
    const distance = touchStart - touchEnd;
    if (distance > threshold) onNext();
    if (distance < -threshold) onPrev();
  };

  return { onTouchStart, onTouchMove, onTouchEnd };
}

Accessibility Requirements

无障碍要求

WCAG AA Compliance

WCAG AA 合规

  • Contrast: 4.5:1 for text < 18px, 3:1 for text >= 18px
  • Touch targets: Minimum 44x44px
  • Focus indicators: Visible outline on all interactive elements
  • Screen readers: Announce step changes with
    aria-live
  • 对比度:小于18px的文本对比度4.5:1,大于等于18px的文本对比度3:1
  • 触摸目标:最小44x44px
  • 焦点指示器:所有交互元素都有可见的焦点轮廓
  • 屏幕阅读器:使用
    aria-live
    播报步骤变化

Reduced Motion

减少动画

css
@media (prefers-reduced-motion: reduce) {
  .animate-fade-in-up,
  .animate-slide-in {
    animation: none;
    opacity: 1;
    transform: none;
  }
}
css
@media (prefers-reduced-motion: reduce) {
  .animate-fade-in-up,
  .animate-slide-in {
    animation: none;
    opacity: 1;
    transform: none;
  }
}

Testing Checklist

测试清单

Functional Testing

功能测试

  • All steps can be completed
  • All steps can be skipped
  • Back navigation works
  • Progress persists across sessions
  • Swipe gestures work on mobile
  • Keyboard navigation works
  • 所有步骤均可完成
  • 所有步骤均可跳过
  • 返回导航正常工作
  • 进度在会话间持久化
  • 移动设备上滑动手势正常
  • 键盘导航正常工作

Accessibility Testing

无障碍测试

  • Screen reader announces step changes
  • All interactive elements have labels
  • Focus order is logical
  • Color contrast meets WCAG AA
  • Touch targets are 44x44px minimum
  • 屏幕阅读器可播报步骤变化
  • 所有交互元素都有标签
  • 焦点顺序符合逻辑
  • 颜色对比度符合WCAG AA标准
  • 触摸目标最小为44x44px

Performance Testing

性能测试

  • Initial load under 2 seconds
  • Step transitions under 300ms
  • No layout shifts during animations
  • Works offline after first load
  • 初始加载时间小于2秒
  • 步骤切换时间小于300ms
  • 动画过程中无布局偏移
  • 首次加载后可离线使用

Emotional Testing

情感测试

  • Language is non-judgmental
  • Skip options are clear
  • No pressure tactics
  • Crisis resources visible
  • Privacy messaging clear
  • 使用无评判性语言
  • 跳过选项清晰可见
  • 无施压策略
  • 危机资源可见
  • 隐私说明清晰

References

参考资料

See
/references/
for detailed guides:
  • competitor-analysis.md
    - I Am Sober, Nomo, Sober Grid patterns
  • wellness-patterns.md
    - Headspace, Calm, Daylio insights
  • crisis-integration.md
    - Emergency resource best practices
查看
/references/
获取详细指南:
  • competitor-analysis.md
    - I Am Sober、Nomo、Sober Grid的设计模式
  • wellness-patterns.md
    - Headspace、Calm、Daylio的设计洞察
  • crisis-integration.md
    - 紧急资源整合最佳实践