generic-react-ux-designer

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

React UX Designer

React UX 设计师

Professional UX expertise for React/TypeScript applications.
Extends: Generic UX Designer - Read base skill for design thinking process, user psychology, heuristic evaluation, and research methods.
针对React/TypeScript应用的专业UX技能。
扩展自: 通用UX设计师 - 请阅读基础技能文档了解设计思维流程、用户心理学、启发式评估和研究方法。

React Interaction Patterns

React 交互模式

Micro-interactions with Framer Motion

基于Framer Motion的微交互

tsx
// Checkbox animation
<motion.div
  animate={{ scale: checked ? 1 : 0 }}
  transition={{ type: "spring", stiffness: 500 }}
>
  <Check className="w-4 h-4" />
</motion.div>

// Button press feedback
<motion.button
  whileTap={{ scale: 0.98 }}
  whileHover={{ scale: 1.02 }}
  transition={{ type: "spring", stiffness: 400 }}
>
  Click me
</motion.button>

// Toast notification
<motion.div
  initial={{ opacity: 0, y: 50 }}
  animate={{ opacity: 1, y: 0 }}
  exit={{ opacity: 0, y: 50 }}
>
  <Toast message={message} />
</motion.div>
tsx
// Checkbox animation
<motion.div
  animate={{ scale: checked ? 1 : 0 }}
  transition={{ type: "spring", stiffness: 500 }}
>
  <Check className="w-4 h-4" />
</motion.div>

// Button press feedback
<motion.button
  whileTap={{ scale: 0.98 }}
  whileHover={{ scale: 1.02 }}
  transition={{ type: "spring", stiffness: 400 }}
>
  Click me
</motion.button>

// Toast notification
<motion.div
  initial={{ opacity: 0, y: 50 }}
  animate={{ opacity: 1, y: 0 }}
  exit={{ opacity: 0, y: 50 }}
>
  <Toast message={message} />
</motion.div>

Loading States

加载状态

tsx
// Skeleton (preferred over spinners)
<div className="animate-pulse space-y-4">
  <div className="h-8 bg-slate-200 rounded w-3/4" />
  <div className="h-4 bg-slate-200 rounded" />
</div>

// Progress indicator for long operations
<div className="relative w-full h-2 bg-slate-200 rounded">
  <motion.div
    className="absolute h-full bg-primary rounded"
    initial={{ width: 0 }}
    animate={{ width: `${progress}%` }}
  />
</div>
tsx
// Skeleton (preferred over spinners)
<div className="animate-pulse space-y-4">
  <div className="h-8 bg-slate-200 rounded w-3/4" />
  <div className="h-4 bg-slate-200 rounded" />
</div>

// Progress indicator for long operations
<div className="relative w-full h-2 bg-slate-200 rounded">
  <motion.div
    className="absolute h-full bg-primary rounded"
    initial={{ width: 0 }}
    animate={{ width: `${progress}%` }}
  />
</div>

Optimistic UI Pattern

乐观UI模式

tsx
const handleLike = async () => {
  // Update UI immediately
  setLiked(true);
  setCount((c) => c + 1);

  try {
    await api.like(id);
  } catch {
    // Rollback on error
    setLiked(false);
    setCount((c) => c - 1);
    toast.error("Failed to save");
  }
};
tsx
const handleLike = async () => {
  // Update UI immediately
  setLiked(true);
  setCount((c) => c + 1);

  try {
    await api.like(id);
  } catch {
    // Rollback on error
    setLiked(false);
    setCount((c) => c - 1);
    toast.error("Failed to save");
  }
};

React Accessibility Patterns

React 可访问性模式

Focus Management

焦点管理

tsx
// Modal focus trap
function Modal({ isOpen, onClose, children }: ModalProps) {
  const modalRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (isOpen) {
      const firstFocusable = modalRef.current?.querySelector(
        "button, [href], input, select, textarea",
      ) as HTMLElement;
      firstFocusable?.focus();
    }
  }, [isOpen]);

  // Trap focus within modal
  const handleKeyDown = (e: KeyboardEvent) => {
    if (e.key === "Tab") {
      const focusables = modalRef.current?.querySelectorAll(
        "button, [href], input, select, textarea",
      );
      // Handle tab cycling...
    }
    if (e.key === "Escape") onClose();
  };

  return (
    <div
      ref={modalRef}
      role="dialog"
      aria-modal="true"
      onKeyDown={handleKeyDown}
    >
      {children}
    </div>
  );
}
tsx
// Modal focus trap
function Modal({ isOpen, onClose, children }: ModalProps) {
  const modalRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (isOpen) {
      const firstFocusable = modalRef.current?.querySelector(
        "button, [href], input, select, textarea",
      ) as HTMLElement;
      firstFocusable?.focus();
    }
  }, [isOpen]);

  // Trap focus within modal
  const handleKeyDown = (e: KeyboardEvent) => {
    if (e.key === "Tab") {
      const focusables = modalRef.current?.querySelectorAll(
        "button, [href], input, select, textarea",
      );
      // Handle tab cycling...
    }
    if (e.key === "Escape") onClose();
  };

  return (
    <div
      ref={modalRef}
      role="dialog"
      aria-modal="true"
      onKeyDown={handleKeyDown}
    >
      {children}
    </div>
  );
}

Keyboard Navigation

键盘导航

tsx
// Custom keyboard shortcut
useEffect(() => {
  const handleKeyDown = (e: KeyboardEvent) => {
    if ((e.metaKey || e.ctrlKey) && e.key === "k") {
      e.preventDefault();
      openCommandPalette();
    }
  };
  window.addEventListener("keydown", handleKeyDown);
  return () => window.removeEventListener("keydown", handleKeyDown);
}, []);

// Arrow key navigation in list
const handleKeyDown = (e: KeyboardEvent, index: number) => {
  switch (e.key) {
    case "ArrowDown":
      e.preventDefault();
      focusItem(index + 1);
      break;
    case "ArrowUp":
      e.preventDefault();
      focusItem(index - 1);
      break;
  }
};
tsx
// Custom keyboard shortcut
useEffect(() => {
  const handleKeyDown = (e: KeyboardEvent) => {
    if ((e.metaKey || e.ctrlKey) && e.key === "k") {
      e.preventDefault();
      openCommandPalette();
    }
  };
  window.addEventListener("keydown", handleKeyDown);
  return () => window.removeEventListener("keydown", handleKeyDown);
}, []);

// Arrow key navigation in list
const handleKeyDown = (e: KeyboardEvent, index: number) => {
  switch (e.key) {
    case "ArrowDown":
      e.preventDefault();
      focusItem(index + 1);
      break;
    case "ArrowUp":
      e.preventDefault();
      focusItem(index - 1);
      break;
  }
};

Motion Accessibility

动画可访问性

tsx
// Respect prefers-reduced-motion
const prefersReducedMotion = window.matchMedia(
  "(prefers-reduced-motion: reduce)",
).matches;

<motion.div
  initial={{ opacity: 0, y: prefersReducedMotion ? 0 : 20 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: prefersReducedMotion ? 0 : 0.3 }}
/>;
tsx
// Respect prefers-reduced-motion
const prefersReducedMotion = window.matchMedia(
  "(prefers-reduced-motion: reduce)",
).matches;

<motion.div
  initial={{ opacity: 0, y: prefersReducedMotion ? 0 : 20 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: prefersReducedMotion ? 0 : 0.3 }}
/>;

Form UX Patterns

表单UX模式

React Hook Form Integration

React Hook Form 集成

tsx
function ContactForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm();

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div className="space-y-1">
        <label htmlFor="email" className="text-sm font-medium">
          Email
        </label>
        <input
          id="email"
          {...register("email", { required: "Email is required" })}
          aria-invalid={errors.email ? "true" : "false"}
          aria-describedby={errors.email ? "email-error" : undefined}
          className={cn("input", errors.email && "border-red-500")}
        />
        {errors.email && (
          <p id="email-error" className="text-sm text-red-500">
            {errors.email.message}
          </p>
        )}
      </div>
    </form>
  );
}
tsx
function ContactForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm();

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div className="space-y-1">
        <label htmlFor="email" className="text-sm font-medium">
          Email
        </label>
        <input
          id="email"
          {...register("email", { required: "Email is required" })}
          aria-invalid={errors.email ? "true" : "false"}
          aria-describedby={errors.email ? "email-error" : undefined}
          className={cn("input", errors.email && "border-red-500")}
        />
        {errors.email && (
          <p id="email-error" className="text-sm text-red-500">
            {errors.email.message}
          </p>
        )}
      </div>
    </form>
  );
}

Inline Validation

内联验证

tsx
// Validate on blur, show on focus
const [touched, setTouched] = useState(false);
const [error, setError] = useState("");

<input
  onBlur={() => {
    setTouched(true);
    setError(validate(value));
  }}
  onFocus={() => setError("")} // Clear while editing
  className={touched && error ? "border-red-500" : ""}
/>;
tsx
// Validate on blur, show on focus
const [touched, setTouched] = useState(false);
const [error, setError] = useState("");

<input
  onBlur={() => {
    setTouched(true);
    setError(validate(value));
  }}
  onFocus={() => setError("")} // Clear while editing
  className={touched && error ? "border-red-500" : ""}
/>;

Modal/Dialog Patterns

模态框/对话框模式

Confirmation Dialog

确认对话框

tsx
function ConfirmDialog({ isOpen, onConfirm, onCancel, title, message }: Props) {
  return (
    <motion.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
    >
      <motion.div
        initial={{ scale: 0.95 }}
        animate={{ scale: 1 }}
        role="alertdialog"
        aria-modal="true"
        aria-labelledby="dialog-title"
        aria-describedby="dialog-desc"
        className="bg-white rounded-xl p-6 max-w-md"
      >
        <h2 id="dialog-title" className="text-lg font-semibold">
          {title}
        </h2>
        <p id="dialog-desc" className="mt-2 text-muted">
          {message}
        </p>
        <div className="mt-4 flex gap-3 justify-end">
          <button onClick={onCancel} className="btn-secondary">
            Cancel
          </button>
          <button onClick={onConfirm} className="btn-primary">
            Confirm
          </button>
        </div>
      </motion.div>
    </motion.div>
  );
}
tsx
function ConfirmDialog({ isOpen, onConfirm, onCancel, title, message }: Props) {
  return (
    <motion.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
    >
      <motion.div
        initial={{ scale: 0.95 }}
        animate={{ scale: 1 }}
        role="alertdialog"
        aria-modal="true"
        aria-labelledby="dialog-title"
        aria-describedby="dialog-desc"
        className="bg-white rounded-xl p-6 max-w-md"
      >
        <h2 id="dialog-title" className="text-lg font-semibold">
          {title}
        </h2>
        <p id="dialog-desc" className="mt-2 text-muted">
          {message}
        </p>
        <div className="mt-4 flex gap-3 justify-end">
          <button onClick={onCancel} className="btn-secondary">
            Cancel
          </button>
          <button onClick={onConfirm} className="btn-primary">
            Confirm
          </button>
        </div>
      </motion.div>
    </motion.div>
  );
}

Command Palette (⌘K Pattern)

命令面板(⌘K 模式)

tsx
function CommandPalette({ isOpen, onClose }: Props) {
  const [query, setQuery] = useState("");
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    if (isOpen) inputRef.current?.focus();
  }, [isOpen]);

  const filtered = useMemo(
    () =>
      commands.filter((c) =>
        c.label.toLowerCase().includes(query.toLowerCase()),
      ),
    [query],
  );

  return (
    <div role="dialog" aria-label="Command palette">
      <input
        ref={inputRef}
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Type a command..."
        aria-autocomplete="list"
      />
      <ul role="listbox">
        {filtered.map((cmd) => (
          <li key={cmd.id} role="option">
            {cmd.label}
          </li>
        ))}
      </ul>
    </div>
  );
}
tsx
function CommandPalette({ isOpen, onClose }: Props) {
  const [query, setQuery] = useState("");
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    if (isOpen) inputRef.current?.focus();
  }, [isOpen]);

  const filtered = useMemo(
    () =>
      commands.filter((c) =>
        c.label.toLowerCase().includes(query.toLowerCase()),
      ),
    [query],
  );

  return (
    <div role="dialog" aria-label="Command palette">
      <input
        ref={inputRef}
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Type a command..."
        aria-autocomplete="list"
      />
      <ul role="listbox">
        {filtered.map((cmd) => (
          <li key={cmd.id} role="option">
            {cmd.label}
          </li>
        ))}
      </ul>
    </div>
  );
}

Empty & Error States

空状态与错误状态

tsx
// Empty state with action
function EmptyState({ title, description, action }: Props) {
  return (
    <div className="text-center py-12">
      <Icon className="mx-auto h-12 w-12 text-muted" />
      <h3 className="mt-4 text-lg font-medium">{title}</h3>
      <p className="mt-2 text-muted">{description}</p>
      <button onClick={action.onClick} className="mt-4 btn-primary">
        {action.label}
      </button>
    </div>
  );
}

// Error state with retry
function ErrorState({ error, onRetry }: Props) {
  return (
    <div className="text-center py-12" role="alert">
      <AlertCircle className="mx-auto h-12 w-12 text-red-500" />
      <h3 className="mt-4 text-lg font-medium">Something went wrong</h3>
      <p className="mt-2 text-muted">{error.message}</p>
      <button onClick={onRetry} className="mt-4 btn-primary">
        Try again
      </button>
    </div>
  );
}
tsx
// Empty state with action
function EmptyState({ title, description, action }: Props) {
  return (
    <div className="text-center py-12">
      <Icon className="mx-auto h-12 w-12 text-muted" />
      <h3 className="mt-4 text-lg font-medium">{title}</h3>
      <p className="mt-2 text-muted">{description}</p>
      <button onClick={action.onClick} className="mt-4 btn-primary">
        {action.label}
      </button>
    </div>
  );
}

// Error state with retry
function ErrorState({ error, onRetry }: Props) {
  return (
    <div className="text-center py-12" role="alert">
      <AlertCircle className="mx-auto h-12 w-12 text-red-500" />
      <h3 className="mt-4 text-lg font-medium">Something went wrong</h3>
      <p className="mt-2 text-muted">{error.message}</p>
      <button onClick={onRetry} className="mt-4 btn-primary">
        Try again
      </button>
    </div>
  );
}

React UX Checklist

React UX 检查清单

Interaction Quality:
  • Immediate feedback on user actions
  • Loading states for async operations
  • Optimistic updates where appropriate
  • Smooth animations (60fps)
Accessibility:
  • Keyboard navigation complete
  • Focus management in modals
  • Motion respects prefers-reduced-motion
  • ARIA labels on interactive elements
Forms:
  • Inline validation
  • Clear error messages
  • Smart defaults
  • Progress indication for multi-step
交互质量:
  • 用户操作即时反馈
  • 异步操作的加载状态
  • 合理使用乐观更新
  • 流畅动画(60fps)
可访问性:
  • 完整的键盘导航
  • 模态框内的焦点管理
  • 动画遵循 prefers-reduced-motion 偏好
  • 交互元素添加ARIA标签
表单:
  • 内联验证
  • 清晰的错误提示
  • 智能默认值
  • 多步骤表单的进度指示

See Also

另请参阅

  • Generic UX Designer - Design thinking, psychology
  • UX Principles - Research methods, heuristics
  • Design Patterns - Visual patterns
  • 通用UX设计师 - 设计思维、心理学
  • UX原则 - 研究方法、启发式规则
  • 设计模式 - 视觉模式