AgentSkillsCN

ux-patterns

当您需要实现表单、输入框、模态框、加载状态,或用户反馈功能时,此技能将为您提供全方位指导。涵盖 OTP 验证、信用卡输入、下拉菜单、Toast 与内联反馈、骨架屏加载等实用功能。

SKILL.md
--- frontmatter
name: ux-patterns
description: Use when implementing forms, inputs, modals, loading states, or user feedback. Covers OTP verification, credit card inputs, dropdowns, toast vs inline feedback, skeleton loading.

UX Good Patterns

Quick Reference

ComponentGoodBad
Credit card inputAuto-format with spaces, inputmode="numeric"Raw input, type="number"
OTP fieldsSeparate digits + paste support + auto-submitSingle field or no paste
Submit buttonAlways enabled, validate on submitDisabled until "valid"
Loading >2sProgressive messages or skeletonStatic "Loading..." spinner
Modal closeX + outside click + Escape keyX button only
FeedbackInline near actionToast notification
Options 2-5Radio buttons/cards visibleDropdown
Options 10+Searchable comboboxLong dropdown

Patterns

Input Formatting

Credit cards, phone, IBAN: Auto-format with visual separators.

tsx
// Store raw, display formatted
const [raw, setRaw] = useState('')
const formatted = raw.replace(/(\d{4})/g, '$1 ').trim()

<input
  inputMode="numeric"        // NOT type="number"
  value={formatted}
  placeholder="4242 4242 4242 4242"
  onChange={e => setRaw(e.target.value.replace(/\D/g, ''))}
/>

OTP Verification

Must have: Separate digit fields + paste support + auto-submit on last digit.

tsx
const handlePaste = (e: ClipboardEvent) => {
  const digits = e.clipboardData.getData('text').replace(/\D/g, '')
  digits.split('').forEach((d, i) => fields[i]?.setValue(d))
  if (digits.length === maxLength) submitOtp(digits)
}

const handleChange = (index: number, value: string) => {
  // Auto-advance to next field
  if (value && index < maxLength - 1) fields[index + 1].focus()
  // Auto-submit when complete
  if (getAllDigits().length === maxLength) submitOtp(getAllDigits())
}

Desktop: Add visible "Paste" button (browser permission prompt is acceptable).

Modal Closing

Support all three methods:

tsx
<dialog
  onClick={e => e.target === e.currentTarget && close()}  // outside click
  onKeyDown={e => e.key === 'Escape' && close()}          // escape key
>
  <button onClick={close}>×</button>                       // X button
  {children}
</dialog>

Exception: Disable outside-click for destructive/critical confirmations.

Submit Buttons

Keep enabled. Validate on submit, show inline errors.

tsx
// Bad: disabled={!isValid}
// Good: always enabled, validate on click
<button type="submit">Submit</button>
{errors.map(e => <span className="error">{e}</span>)}

Only disable: During submission (with loading indicator).

Loading States

DurationPattern
<300msNothing
300ms-2sSpinner or skeleton
>2sProgressive messages
tsx
// Progressive messages for long operations
const messages = ['Connecting...', 'Processing...', 'Almost done...']
const [msgIndex, setMsgIndex] = useState(0)

useEffect(() => {
  const timer = setInterval(() => setMsgIndex(i => Math.min(i + 1, messages.length - 1)), 2000)
  return () => clearInterval(timer)
}, [])

Skeleton loading: Match actual content layout (not generic spinner).

Inline vs Toast Feedback

Prefer inline for contextual actions. Place feedback near user focus.

tsx
// Bad: toast("Copied!")
// Good: inline confirmation
<button onClick={copy}>
  {copied ? '✓ Copied' : 'Copy'}
</button>

Toasts: Only for background operations user didn't initiate.

Dropdowns vs Visible Options

OptionsUse
2-5Radio buttons, cards, segmented control
6-9Dropdown acceptable
10+Searchable combobox required

FAB (Floating Action Button)

  • Primary action only (create, compose)
  • Bottom-right corner
  • One per screen max
  • Hide on scroll down, show on scroll up

Scroll to Top

  • Show after 300-500px scroll
  • Bottom-right, upward arrow
  • Skip if content < 2 viewport heights

Source

UX Good Patterns