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

9 micro interactions css — dán 1 dòng chạy ngay

Micro interactions css là những phản hồi cực nhỏ trên giao diện — nút lún xuống, icon đổi hình, toast xếp chồng — báo cho bạn biết thao tác vừa có tác dụng.

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

Thứ tự chín mục đi từ hẹp ra rộng. Từ 01 đến 03 là phản hồi ngay tại chỗ bạn vừa chạm — một nút, một icon lưu, một icon copy — chỉ đổi trạng thái của chính phần tử đó. Từ 04 đến 06 là phản hồi cho những trạng thái không do một cú nhấn trực tiếp quyết định — thích một bài viết, dữ liệu vừa tải xong, danh sách không có kết quả. 07 đến 09 trải rộng trên nhiều phần tử cùng lúc — một ô nhập cùng dòng cảnh báo, một hàng tab, một chồng thông báo. Ở lưới phía trên, bạn xem từng hiệu ứng tự lặp lại — bản đầy đủ, không cắt dòng nào, nằm trong file zip.

01Nút lún xuống khi nhấn

Bạn thấy gì

Nút này co lại còn scale(0.96) ngay khi bạn nhấn giữ, nhờ pseudo-class :active chứ không cần JavaScript, rồi bật trở lại bằng easing kiểu lò xo ngay khi bạn thả tay.

Nút lún xuống khi nhấn
Mở trong tab mới

01 · Nút lún xuống khi nhấn — Diện tích đo được 1.256% · khung hình chuyển động 7/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

scale(0.96)스프링 이징active 상태

Dùng ở đâu

Dùng cho nút gửi, thẻ có thể bấm, hoặc nút icon — bất cứ chỗ nào cần xác nhận rằng cú chạm đã được ghi nhận.

.btn {
  transform: scale(1);
  box-shadow: $shadow-raised;
  transition: transform $duration $easing, box-shadow $duration $easing;

  &:active {
    transform: scale(.96);
    box-shadow: $shadow-press;
  }

  &.is-demo {
    animation: press-loop $dur-loop $easing infinite;
  }
}

02Hoạt ảnh lưu thành tick

Chuyện gì diễn ra trên màn hình

Khi bạn bấm nút lưu, class is-saved bật lên: hình đĩa mềm mờ dần biến mất trong lúc hai vạch của dấu tick tự vẽ ra bằng scaleX, với animation-delay lệch nhau để nét ngắn đi trước nét dài.

Hoạt ảnh lưu thành tick
Mở trong tab mới

02 · Hoạt ảnh lưu thành tick — Diện tích đo được 0.583% · khung hình chuyển động 11/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

scaleX 획 긋기색상 전환0.3초

Chỗ dùng

Một vòng tròn nền cũng phồng lên cùng lúc để nhấn mạnh việc lưu đã xong.

&.is-saved &__ring  { animation: ring-pop $duration $easing forwards; }
&.is-saved &__disk  { animation: disk-out $duration $easing forwards; }
&.is-saved &__check-bar--short { animation: check-short $duration $easing forwards; animation-delay: $duration * .35; }
&.is-saved &__check-bar--long  { animation: check-long $duration $easing forwards; animation-delay: $duration * .5; }

@keyframes ring-pop  { from { opacity: 0; transform: scale(.4); } to { opacity: 1; transform: scale(1); } }
@keyframes disk-out  { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(.5); } }
@keyframes check-short { to { transform: rotate(45deg) scaleX(1); } }
@keyframes check-long  { to { transform: rotate(-52deg) scaleX(1); } }

03Phản hồi sao chép xong

Chuyển động thế nào

Bấm vào icon copy, icon đổi ngay sang dấu tick trong khi hai hình chữ nhật chồng lên nhau mờ dần biến mất, và một tooltip nhỏ nổi lên rồi tự tắt theo đúng bốn mốc của keyframe tip-float.

Phản hồi sao chép xong
Mở trong tab mới

03 · Phản hồi sao chép xong — Diện tích đo được 1.494% · khung hình chuyển động 14/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

아이콘 스왑툴팁 페이드1.2초 유지

Dùng khi nào

Hợp cho nút copy trong khối code hoặc nút copy API key.

&.is-copied &__ring { animation: ring-pop $duration $easing forwards; }
&.is-copied &__sheet--front { opacity: 0; transform: scale(.6); transition-delay: 0s; }
&.is-copied &__sheet--back  { opacity: 0; transform: scale(.6); }
&.is-copied &__check { animation: check-in $duration $easing forwards; }
&.is-copied &__check-bar--short { animation: bar-short $duration $easing forwards; animation-delay: $duration * .3; }
&.is-copied &__check-bar--long  { animation: bar-long $duration $easing forwards; animation-delay: $duration * .45; }
&.is-copied &__tip { animation: tip-float 1.4s $easing forwards; }

@keyframes tip-float {
  0%   { opacity: 0; transform: translate(-50%, 4px); }
  15%  { opacity: 1; transform: translate(-50%, -2px); }
  85%  { opacity: 1; transform: translate(-50%, -2px); }
  100% { opacity: 0; transform: translate(-50%, -10px); }
}

04Thích lạc quan (tức thời + rollback)

Bạn thấy gì

Chạm vào trái tim, giao diện tô đỏ và tăng số đếm ngay lập tức mà không đợi phản hồi server, đúng kiểu optimistic update.

Thích lạc quan (tức thời + rollback)
Mở trong tab mới

04 · Thích lạc quan (tức thời + rollback) — Diện tích đo được 0.354% · khung hình chuyển động 9/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

낙관적 업데이트롤백 애니메이션0.8초

Dùng ở đâu

Đoạn script còn giả lập luôn cảnh thất bại: sau 800ms trạng thái like tự rollback về ban đầu.

var b = document.querySelector('.like');
b.addEventListener('click', function () {
  b.classList.remove('is-demo');
  var liked = b.classList.toggle('is-liked');
  b.setAttribute('aria-pressed', liked);
  if (liked) setTimeout(function () {
    b.classList.remove('is-liked');
    b.setAttribute('aria-pressed', 'false');
  }, 800);
});

05Crossfade skeleton sang nội dung

Chuyện gì diễn ra trên màn hình

Hai lớp — khối skeleton xám và nội dung thật — nằm chồng tuyệt đối lên nhau trong cùng một thẻ card, mỗi lớp chỉ đổi opacity và scale riêng để tạo crossfade chứ không lớp nào bị gỡ khỏi DOM.

Crossfade skeleton sang nội dung
Mở trong tab mới

05 · Crossfade skeleton sang nội dung — Diện tích đo được 3.401% · khung hình chuyển động 10/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

opacity 크로스페이드겹친 레이어0.4초

Chỗ dùng

Dùng cho danh sách card đang tải hoặc lần tải đầu của trang hồ sơ.

.card__layer--skeleton {
  flex-direction: row;
  opacity: 1;
  transform: scale(1);
  transition: opacity $duration $easing, transform $duration $easing;
}
.card__layer--content {
  flex-direction: row;
  align-items: flex-start;
  opacity: 0;
  transform: scale(.98);
  transition: opacity $duration $easing, transform $duration $easing;
}

06Icon trạng thái rỗng nảy nhẹ

Chuyển động thế nào

Icon khay hiện ra khi danh sách rỗng chỉ dịch chuyển translateY lên xuống 10px theo chu kỳ vô hạn, không có gì khác đổi, để nói rằng màn hình trống này là chủ ý chứ không phải lỗi tải.

Icon trạng thái rỗng nảy nhẹ
Mở trong tab mới

06 · Icon trạng thái rỗng nảy nhẹ — Diện tích đo được 1.361% · 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

translateY 바운스ease-out 이징무한 반복

Dùng khi nào

Hợp cho màn hình không có kết quả tìm kiếm hoặc giỏ hàng rỗng.

.empty {
  &__icon {
    position: relative;
    width: 52px;
    height: 40px;
    transform: translateY(0);
  }

  &.is-demo &__icon { animation: bounce $duration $easing infinite; }
}

@keyframes bounce {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-10px); }
}

07Ô nhập rung khi sai

Bạn thấy gì

Khi giá trị nhập sai, khung input rung translateX qua lại theo năm mốc thời gian bất đối xứng để tạo cảm giác lắc thật, trong khi một vòng viền đỏ tách riêng chỉ đổi opacity thay vì animate border-color.

Ô nhập rung khi sai
Mở trong tab mới

07 · Ô nhập rung khi sai — Diện tích đo được 3.311% · khung hình chuyển động 10/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

translateX 흔들림빨강 테두리0.4초

Dùng ở đâu

Hợp cho ô mật khẩu ở form đăng nhập hoặc ô số thẻ ở form thanh toán.

&.is-error &__frame { animation: shake $duration $easing both; }
&.is-error &__ring  { opacity: 1; }
&.is-error &__msg   { opacity: 1; transform: translateY(0); }

@keyframes shake {
  10%, 90% { transform: translateX(-$amount * .5); }
  20%, 80% { transform: translateX($amount); }
  30%, 70% { transform: translateX(-$amount); }
  40%, 60% { transform: translateX($amount * .7); }
  50%      { transform: translateX(-$amount * .7); }
}

08Vạch chỉ báo tab trượt

Chuyện gì diễn ra trên màn hình

Vạch chỉ báo dưới tab dịch chuyển bằng translateX nhân với biến CSS tuỳ chỉnh --i, và click handler trong JavaScript chỉ làm một việc là ghi số thứ tự tab mới vào biến đó.

Vạch chỉ báo tab trượt
Mở trong tab mới

08 · Vạch chỉ báo tab trượt — Diện tích đo được 0.5% · khung hình chuyển động 11/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

transform 슬라이드width 보간0.25초

Chỗ dùng

Hợp cho tab trang cài đặt hoặc tab danh mục trên dashboard.

&__indicator {
  position: absolute;
  left: 4px; bottom: 4px;
  width: calc((100% - 8px) / 3);
  height: 3px;
  border-radius: $r-pill;
  background: $color;
  transform: translateX(calc(var(--i, 0) * 100%));
  transition: transform $duration $easing;
}

09Toast xếp chồng

Chuyển động thế nào

Mỗi toast cũ bị đẩy lên translateY(-42px) và thu nhỏ nhẹ bằng scale(.96) mỗi khi có một toast mới xuất hiện phía dưới, nên ba thông báo luôn xếp chồng lên nhau theo bậc thay vì đè lên nhau.

Toast xếp chồng
Mở trong tab mới

09 · Toast xếp chồng — Diện tích đo được 19.122% · khung hình chuyển động 10/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

translateY 밀림scale 축소순차 스택

Dùng khi nào

Hợp cho thông báo lưu thành công hoặc tiến trình tải nhiều file cùng lúc.

@keyframes slot-1 {
  0%, 4%    { opacity: 0; transform: translateY(16px) scale(1); }
  12%, 38%  { opacity: 1; transform: translateY(0) scale(1); }
  50%, 68%  { opacity: .88; transform: translateY(-42px) scale(.96); }
  80%, 92%  { opacity: .7; transform: translateY(-84px) scale(.92); }
  98%, 100% { opacity: 0; transform: translateY(-84px) scale(.92); }
}

Chỗ dễ hỏng ở đâu? — cái bẫy

Chiều cao cố định 118px của khung toast

Cái bẫy đầu tiên nằm ở mục 09: khung .toasts__list được đặt chiều cao cố định 118px, đúng bằng một slot 34px cộng hai lần dịch chuyển 42px. Con số đó chỉ đủ cho đúng ba toast xếp chồng như trong demo — nếu bạn thêm toast thứ tư vào cùng danh sách mà không tính lại chiều cao, toast mới nhất sẽ tràn ra khỏi khung chứ không tự xếp thêm bậc.

Thời lượng 1,4s viết cứng

Cái bẫy thứ hai nằm ở mục 03: thời lượng của tooltip là 1.4s viết cứng trong khai báo animation: tip-float 1.4s $easing forwards, tách rời khỏi biến $duration dùng cho icon và dấu tick. Đổi $duration ở đầu file để làm icon đổi nhanh hơn sẽ không kéo tooltip nhanh theo, vì đó là hai con số độc lập chứ không phải một biến chung — nếu bạn muốn mở file ra soi từng dòng ngay bây giờ, mật khẩu của zip là dawuywyc.

Lưới xem trước và class is-demo

Cái bẫy thứ ba nằm ngay ở lưới phía trên: mỗi ô đặt trong iframe cố định thấp và hẹp, nên chín hiệu ứng ở đây đều chạy bằng class .is-demo lặp vô hạn thay cho cú nhấn thật của bạn — bạn chỉ xem chuyển động lặp lại chứ chưa thật sự nhấn nút nào cả. Muốn thấy đúng phản hồi khi bạn tự tay chạm vào, hãy mở file vanilla riêng trong tab của mình.

Khả năng tiếp cận

Kết quả cuối khi giảm chuyển động

Chín demo đều phản hồi prefers-reduced-motion, nhưng cách xử lý không phải lúc nào cũng là tắt hẳn — đôi khi là nhảy thẳng đến kết quả cuối. Mục 01, 03, 04, 07 và 09 bỏ animation và giữ transform ở trạng thái cuối cùng chứ không giật về vị trí ban đầu. Mục 02 và 05 khoá luôn ở trạng thái đã lưu và đã tải xong — vòng lặp qua lại giữa hai lớp bị bỏ hẳn. Mục 06 dừng hẳn cú nảy vì một icon đứng yên không mang đủ tính trang trí để đáng giữ khi chuyển động đã tắt, còn mục 08 chỉ bỏ transition của vạch chỉ báo và giữ nguyên vị trí theo biến --i. Hướng dẫn prefers-reduced-motion của MDN có ghi mức hỗ trợ trình duyệt cho media query này, và biến CSS tuỳ chỉnh --i dùng ở mục 08 cũng được MDN giải thích ở đây.

@media (prefers-reduced-motion: reduce) {
  .btn { transition: none; }
  .btn.is-demo { animation: none; }
  .save__ring, .save__disk, .save__check-bar { animation: none !important; }
}

Chín micro interaction này chỉ là một góc nhỏ — chuyên mục CSS gom hết các bài khác cùng kiểu, còn trang giới thiệu giải thích vì sao site chỉ đưa demo đã đo đạc thật.

FAQ

Micro interactions css có cần JavaScript không?

Phần lớn có. Mục 01 chạy hoàn toàn bằng :active nên không cần JavaScript, nhưng 02, 03, 04, 07, 08 và 09 vẫn cần một đoạn script ngắn để toggle class hoặc ghi biến CSS khi bạn nhấn — CSS lo phần chuyển động, JavaScript chỉ lo đúng một việc là đổi trạng thái.

Có dùng chung với React và Tailwind được không?

Được. Thư mục react/ trong zip có sẵn một component cho mỗi hiệu ứng, mỗi component nhận đúng những biến số đó qua props và đi kèm module.scss riêng, nên class tiện ích của Tailwind không xung đột với animation và keyframes của SCSS.

Vì sao demo trong lưới trông khác khi tôi tự bấm trong file thật?

Vì lưới phía trên chạy vòng lặp .is-demo cố định để bạn xem liên tục mà không cần thao tác gì, còn file vanilla thật phản hồi đúng theo cú nhấn hoặc cú chạm của bạn — hai thứ dùng chung SCSS nhưng khác cách kích hoạt.

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.