GODRICH
#css-animation#button#loader#transition#scroll#ui#xu-huong-thiet-ke

9 css shake animation — hiệu ứng rung báo lỗi form

Bạn từng thấy ô mật khẩu rung qua rung lại khi gõ sai chưa? Đó là css shake animation — vài dòng CSS, không cần JS, đủ báo rằng form đang sai.

Tự chạy · bấm ô để tới mục đó · cả 9 trong một zip

Chín mục xếp theo lỗi phát sinh ở đâu chứ không theo độ nổi tiếng. Bắt đầu từ chính giá trị nhập vào bị sai (01~03), sang tín hiệu tiết chế chỉ nháy phần tử trên màn (04), rồi qua những thất bại nặng hơn như mật khẩu (05~06), tới trường hợp trình duyệt tự lo (07) và khi cùng một lỗi lặp lại (08), cuối cùng là bài toán giữ tín hiệu cho người đã giảm chuyển động (09).

01Rung ngang

css shake div làm rung cái gì

Nhập sai định dạng vào ô email rồi bấm ra chỗ khác, cả ô nhập rung ngang một nhịp ngắn rồi đứng lại.

Rung trái phải
Mở trong tab mới

01 · Rung trái phải — Diện tích đo được 0.494% · khung hình chuyển động 8/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

translateX@keyframes500ms

Dựng bằng cách nào

Toàn bộ chỉ là một @keyframes xếp giá trị translateX theo thứ tự -7px, 7px, -5px, 3px, 0px, biên độ hẹp dần nên trông đúng như một cơn rung thật.

@keyframes sh-shake {
  0%, 22%, 100% { transform: translateX(0); }
  2%  { transform: translateX(-7px); }
  4%  { transform: translateX(7px); }
  6%  { transform: translateX(-7px); }
  8%  { transform: translateX(7px); }
  10% { transform: translateX(-5px); }
  12% { transform: translateX(5px); }
  14% { transform: translateX(-3px); }
  16% { transform: translateX(3px); }
}
.sh__input.sh--invalid { animation: sh-shake $duration $easing; }

02Lắc nghiêng

Trông ra sao

Bấm gửi mà chưa tick checkbox điều khoản, cả dòng chứa checkbox lẫn câu chữ nghiêng qua nghiêng lại.

Wiggle xoay nghiêng
Mở trong tab mới

02 · Wiggle xoay nghiêng — Diện tích đo được 1.942% · khung hình chuyển động 22/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

rotatetransform-origin600ms

Đọc code thế nào

@keyframes này chỉ đổi mỗi góc rotate, hình dáng checkbox không bị đụng tới một chút nào.

@keyframes wg-wiggle {
  0%, 100% { transform: rotate(0); }
  15% { transform: rotate(-4deg); }
  30% { transform: rotate(4deg); }
  45% { transform: rotate(-3deg); }
  60% { transform: rotate(3deg); }
  75% { transform: rotate(-1.5deg); }
  90% { transform: rotate(1.5deg); }
}
.wg__row.wg--wiggle { animation: wg-wiggle $duration $easing; }

03Dập dềnh như thạch

Dùng khi nào

Bấm nút lưu mà thất bại thì nút dập dềnh như miếng thạch rồi lắng xuống.

Lắc lư như jello
Mở trong tab mới

03 · Lắc lư như jello — Diện tích đo được 1.069% · khung hình chuyển động 23/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

skewXskewY800ms

Điểm mấu chốt của code

Cái lõi nằm ở chỗ bẻ skewX cùng skewY trong chung một keyframe, để nút méo đồng thời theo hai hướng chéo.

@keyframes je-jello {
  0%, 100% { transform: none; }
  15% { transform: skewX(-9deg) skewY(-9deg); }
  30% { transform: skewX(7deg) skewY(7deg); }
  45% { transform: skewX(-5deg) skewY(-5deg); }
  60% { transform: skewX(3deg) skewY(3deg); }
  75% { transform: skewX(-2deg) skewY(-2deg); }
  90% { transform: skewX(1deg) skewY(1deg); }
}
.je__btn.je--jello { animation: je-jello $duration $easing; }

04Nháy viền báo lỗi

Màn hình ra sao

Ô số thẻ đứng yên không nhúc nhích, chỉ lớp viền đỏ xếp chồng phía sau nháy lên đúng hai lần.

Viền lỗi nhấp nháy
Mở trong tab mới

04 · Viền lỗi nhấp nháy — Diện tích đo được 0.869% · khung hình chuyển động 16/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

opacityinset: 0500ms

Nguyên lý hoạt động

Rung cả ô nhập dễ làm mấy ô nằm kề bên trông như cũng rung theo, nên ở đây chỉ đổi opacity và giữ nguyên layout.

.bf__ring {
  position: absolute;
  inset: -$sp-1;
  border: 2px solid $color;
  border-radius: $r-sm;
  opacity: 0;
}
@keyframes bf-flash {
  0%, 100% { opacity: 0; }
  15% { opacity: 1; }
  30% { opacity: 0; }
  45% { opacity: 1; }
  60% { opacity: 0; }
}
.bf__ring.bf--flash { animation: bf-flash $duration $easing; }

05Ô nhập lắc đầu

Cái gì chuyển động

Khi ô xác nhận mật khẩu không khớp, nó xoay nhẹ có chiều sâu, y như đang lắc đầu từ chối.

Ô nhập lắc đầu
Mở trong tab mới

05 · Ô nhập lắc đầu — Diện tích đo được 2.372% · khung hình chuyển động 15/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

rotateYperspective900ms

Cách dựng

Dùng chung translateX với rotateY trong một keyframe, và phải cho phần tử cha một perspective thì cảm giác nổi khối mới sống.

.hs { perspective: 500px; }
@keyframes hs-shake {
  0% { transform: translateX(0); }
  6.5% { transform: translateX(-6px) rotateY(-9deg); }
  18.5% { transform: translateX(5px) rotateY(7deg); }
  31.5% { transform: translateX(-3px) rotateY(-5deg); }
  43.5% { transform: translateX(2px) rotateY(3deg); }
  50%, 100% { transform: translateX(0); }
}
.hs__input.hs--shake { animation: hs-shake $duration $easing; }

06Icon cảnh báo nảy lên

Dùng ở đâu

Icon tròn vẽ dấu chấm than nảy vọt lên rồi tiếp đất, kéo ánh nhìn về phía câu báo lỗi.

Icon cảnh báo nảy lên
Mở trong tab mới

06 · Icon cảnh báo nảy lên — Diện tích đo được 0.726% · khung hình chuyển động 18/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

scaletranslateY700ms

Vì sao dựng kiểu này

Gắn cùng lúc scale với translateY cho ra cảm giác nảy tưng tưng, còn icon chỉ là trang trí nên được gắn aria-hidden.

@keyframes ib-bounce {
  0%, 100% { transform: translateY(0) scale(1); }
  20% { transform: translateY(-10px) scale(1.15); }
  40% { transform: translateY(0) scale(.95); }
  55% { transform: translateY(-4px) scale(1.05); }
  70% { transform: translateY(0) scale(1); }
}
.ib__icon.ib--bounce { animation: ib-bounce $duration $easing; }

07Tự rung bằng :user-invalid

Thứ đập vào mắt

Phần CSS không viết lấy một dòng JS mà trình duyệt tự rung giúp, và chỉ rung khi người dùng đã nhập vào ô email rồi rời đi mà định dạng vẫn sai.

Rung tự động bằng :user-invalid
Mở trong tab mới

07 · Rung tự động bằng :user-invalid — Diện tích đo được 0.507% · khung hình chuyển động 8/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

:user-invalidpattern500ms

Lõi của cách làm

Gắn animation vào đúng một pseudo-class input:user-invalid là xong, còn lúc chưa ai đụng vào giá trị hay trang vừa mở thì nó im lặng.

.ui__input:user-invalid {
  border-color: $color;
  animation: ui-shake $duration $easing;
}
@keyframes ui-shake {
  0%, 22%, 100% { transform: translateX(0); }
  2%  { transform: translateX(-7px); }
  4%  { transform: translateX(7px); }
  6%  { transform: translateX(-7px); }
  8%  { transform: translateX(7px); }
  10% { transform: translateX(-5px); }
  12% { transform: translateX(5px); }
  14% { transform: translateX(-3px); }
  16% { transform: translateX(3px); }
}

08Mẫu reset để phát lại một lần

Tình huống nào

Bấm "Kiểm tra lại" liên tiếp bao nhiêu lần thì mỗi lần vẫn rung mới từ đầu.

Mẫu reset chạy lại từ đầu
Mở trong tab mới

08 · Mẫu reset chạy lại từ đầu — Diện tích đo được 0.56% · khung hình chuyển động 23/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

animationendreflowJS 6줄

Cái gì là mấu chốt

Gỡ class rồi gắn lại ngay lập tức thì trình duyệt tưởng animation vẫn đang chạy nên bỏ qua, vì vậy phải đọc offsetWidth một lần sau khi gỡ để ép reflow rồi mới gắn lại.

var input = document.getElementById('rs-pw');
document.getElementById('rs-check').addEventListener('pointerdown', function () {
  input.classList.remove('rs--shake');
  void input.offsetWidth;
  input.classList.add('rs--shake');
});

09Thay bằng outline khi giảm chuyển động

Chuyện gì xảy ra

Bình thường nó rung y như mục 01, nhưng bật giảm chuyển động trong hệ thống thì rung được thay bằng vòng viền đỏ nháy đúng hai lần.

Đổi outline khi giảm chuyển động
Mở trong tab mới

09 · Đổi outline khi giảm chuyển động — Diện tích đo được 0.336% · khung hình chuyển động 8/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

prefers-reduced-motionrm__ringopacity

Điểm cần nhớ

Mục đích là tắt chuyển động mà không tắt luôn tín hiệu cho biết đang có lỗi.

@media (prefers-reduced-motion: reduce) {
  .rm__textarea.rm--invalid { animation: none !important; }
  .rm__ring.rm--invalid { animation: rm-ring-pulse $dur-loop ease-in-out; }
}
@keyframes rm-ring-pulse {
  0%, 100% { opacity: 0; }
  25% { opacity: 1; }
  50% { opacity: 0; }
  75% { opacity: 1; }
}

Hiệu ứng rung dễ hỏng ở đâu — một cái bẫy

Vấn đề nằm ở chỗ nào

Dù bạn rung nguyên ô nhập hay chỉ rung chữ kiểu css shake text, tám mục (01~07 và 09) đều dính chung một cái bẫy: animation của CSS chạy một lần là hết. Người dùng có nhập sai lại đúng giá trị cũ thì class vẫn nằm sẵn trên phần tử, trình duyệt coi đó là "animation đang chạy dở" nên không phát lại nữa. Mục 08 chứa đoạn code chữa bệnh này bằng ba bước classList.remove() rồi ép đọc offsetWidth rồi classList.add(), và bạn chép nguyên mẫu đó sang các mục còn lại đều được. Cách này cũng là nền của mọi css error shake animation gắn bằng class.

Tránh bằng cách nào

Nếu bạn dùng :user-invalid (mục 07) thì vấn đề này không tồn tại — mỗi lần giá trị đổi, trình duyệt tính lại trạng thái và tự phát. File nén chứa toàn bộ code vanilla lẫn React của chín mục mở bằng 3v34eb5k.

Khả năng tiếp cận

Cái gì còn lại khi reduced-motion

Cả chín mục đều tắt chuyển động bằng animation: none !important dưới prefers-reduced-motion: reduce. Có điều mấy mục mà chuyển động là tín hiệu lỗi duy nhất như 04 và 09, tắt animation xong là lỗi biến mất luôn, nên 09 bật vòng viền nháy bằng opacity thay cho rung, còn các mục khác luôn để câu chữ hiện cùng. Các pseudo-class kiểm tra gốc như :user-invalid, theo tài liệu MDN, chạy trên Chrome/Edge 119+, Firefox 88+, Safari 16.4+, còn bản cũ hơn thì lặng lẽ bỏ qua, nên form quan trọng như thanh toán hay đăng ký vẫn nên lấy cách gắn class như 01 và 05 làm nền.

Bài đọc tiếp

Muốn xem chỉ báo trượt mượt lúc đổi tab thì có 9 hiệu ứng CSS tab menu transition, còn trang này ngoài tín hiệu lỗi ra còn bàn gì nữa thì xem mục CSS animation.

FAQ

Tại sao hiệu ứng rung chỉ nên dùng opacity hay transform?

Rung bằng margin hay left thì mỗi khung hình trình duyệt phải tính lại layout nên vừa giật vừa đẩy luôn mấy ô nằm kề. transform với opacity không tính lại layout, chỉ dời hoặc làm mờ đúng những pixel đã vẽ sẵn, nhờ vậy mượt hơn hẳn.

:user-invalid với :invalid khác nhau thế nào?

:invalid khớp ngay từ lúc trang vừa mở nếu giá trị trống hoặc sai định dạng. :user-invalid chỉ khớp sau khi người dùng thật sự đụng vào giá trị rồi rời focus, nhờ đó tránh được cái lỗi ngớ ngẩn là rung đỏ một ô trống mà chưa ai nhập gì.

Làm sao cho hiệu ứng rung chỉ phát đúng một lần?

Làm như mục 08: gỡ class, đọc offsetWidth một lần để ép reflow, rồi gắn class lại thì dù vẫn là class cũ nó vẫn phát từ đầu mỗi lần. Với React thì đổi key của phần tử để mount mới hoàn toàn sẽ tự nhiên hơn cách này.

Nhập mật khẩu file nén

Mật khẩu nằm ngay trong bài viết này. Đọc tới đó bạn sẽ thấy.