错误抖动

提交无效的代码时,输入框会左右抖动并在下方显示错误,像在摇头说不。

  • CSS
  • 表单与控件
  • 点击
  • 收录于

AI 提示词

用 React 制作一个校验失败时会抖动的表单字段。提交时用 /^TWL-\d{4}$/i 校验输入值。如果不通过,设置 aria-invalid="true",在一个被 aria-describedby 引用且带 role="alert" 的元素中显示“这个邀请码无效,请检查我们发给你的邮件。”,并对输入框播放一个 CSS keyframe:translateX 依次为 0、-8px、7px、-5px、3px、0,时长 400ms,缓动为 cubic-bezier(0.65, 0, 0.35, 1)。每次提交失败都通过切换 key,或在 animationend 时移除再添加 class 来重新播放抖动。焦点保持在输入框内。如果通过,清除错误并显示“欢迎加入。”。在 prefers-reduced-motion 下不抖动,只改变边框颜色并显示提示。

源代码

Demo.tsx
'use client'

import { useEffect, useId, useRef, useState } from 'react'
import s from './demo.module.css'

export default function ShakeError({ labels }: { labels: Record<string, string> }) {
  const [status, setStatus] = useState<'idle' | 'error' | 'ok'>('idle')
  const [attempt, setAttempt] = useState(0)
  const [value, setValue] = useState('TWL-12')
  const input = useRef<HTMLInputElement>(null)
  const id = useId()

  // the input remounts (key) to restart the shake, so focus the new element afterwards
  useEffect(() => {
    if (attempt) input.current?.focus()
  }, [attempt])

  const submit = (e: React.FormEvent) => {
    e.preventDefault()
    const ok = /^TWL-\d{4}$/i.test(value.trim())
    setStatus(ok ? 'ok' : 'error')
    if (!ok) setAttempt(attempt + 1)
  }

  return (
    <form className={s.form} onSubmit={submit} noValidate>
      <label htmlFor={id} className={s.label}>
        {labels.label}
      </label>
      <div className={s.row}>
        <input
          key={attempt}
          ref={input}
          id={id}
          className={s.input}
          placeholder={labels.placeholder}
          aria-invalid={status === 'error'}
          aria-describedby={`${id}-msg`}
          value={value}
          onChange={(e) => setValue(e.target.value)}
          autoComplete="off"
        />
        <button type="submit" className={s.button}>
          {labels.submit}
        </button>
      </div>
      <p id={`${id}-msg`} className={s.msg} data-status={status} role="alert">
        {status === 'error' ? labels.error : status === 'ok' ? labels.ok : ''}
      </p>
    </form>
  )
}
demo.module.css
.form { display: grid; gap: 8px; width: min(300px, 100%); }

.label { color: var(--stage-muted); font: 500 var(--text-sm) / 1 var(--font-body); }

.row { display: flex; gap: 8px; }

.input {
  flex: 1;
  min-width: 0;
  height: 44px;
  padding: 0 12px;
  border: 1px solid var(--stage-rule);
  border-radius: 10px;
  outline: none;
  background: var(--stage-bg);
  color: var(--stage-ink);
  font: 500 var(--text-base) / 1 var(--font-mono);
}

.input:focus { border-color: var(--stage-muted); }
.input:focus-visible { box-shadow: 0 0 0 3px var(--color-focus); }
.input[aria-invalid='true'] { border-color: var(--color-error); animation: shake 400ms var(--ease-in-out); }

@keyframes shake {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-8px); }
  40% { transform: translateX(7px); }
  60% { transform: translateX(-5px); }
  80% { transform: translateX(3px); }
}

.button {
  height: 44px;
  padding: 0 16px;
  border: 0;
  border-radius: 10px;
  background: var(--stage-ink);
  color: var(--stage-bg);
  font: 600 var(--text-sm) / 1 var(--font-body);
  white-space: nowrap;
  cursor: pointer;
}

.button:hover { background: var(--stage-accent); color: var(--stage-accent-ink); }
.button:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }

.msg { min-height: 2.6em; margin: 0; font-size: var(--text-xs); line-height: 1.3; }
.msg[data-status='error'] { color: var(--color-error); }
.msg[data-status='ok'] { color: var(--stage-accent); }

@media (prefers-reduced-motion: reduce) {
  .input[aria-invalid='true'] { animation: none; }
}

投稿创意

看到想收录的动效或主题?简单描述一下,会打开一个预先填好的 GitHub Issue,确认后即可提交。

你想投稿的是?

需要 GitHub 账号,投稿内容会公开。