Quantity Stepper: spring và direction-aware motion

Quantity stepper xuất hiện ở mọi nơi — giỏ hàng, settings, form. Nhưng hầu hết chỉ thay số mà không có animation. Thêm motion đúng chỗ làm người dùng cảm nhận được hướng của thay đổi, không chỉ giá trị của nó.

0

Click + hoặc − để xem spring animation trên số

Direction-aware: pop-up vs pop-down

Khi tăng số, animation pop-up — scale to + dịch nhẹ lên. Khi giảm, pop-down — scale nhỏ + dịch nhẹ xuống. Motion này map với không gian vật lý: số lớn hơn = ở trên, số nhỏ hơn = ở dưới.

@keyframes qsd-pop-up {
  0%   { transform: scale(1) translateY(0); }
  40%  { transform: scale(1.45) translateY(-3px); }
  100% { transform: scale(1) translateY(0); }
}
@keyframes qsd-pop-down {
  0%   { transform: scale(1) translateY(0); }
  40%  { transform: scale(0.7) translateY(3px); }
  100% { transform: scale(1) translateY(0); }
}

Pop-up overshoot rồi settle = spring feel. Pop-down compress rồi mở lại = elastic feel. Cả hai dùng cubic-bezier spring.

Void reflow để restart animation

Nếu click liên tiếp nhanh, cần restart animation:

numEl.classList.remove('pop-up', 'pop-down');
void numEl.offsetWidth; // force reflow
numEl.classList.add('pop-up');

Boundary state

Khi count = 0: nút bị disabled (opacity 0.25, pointer-events none). Người dùng không cần đọc label để biết đã đến giới hạn — visual đã đủ.

Không dùng cursor: not-allowed vì nó gây frustration không cần thiết. Fade out là đủ.

Button press feedback

:active { transform: scale(0.88) } — button nhấn xuống nhẹ khi click. Spring trên số + depress trên button tạo hai lớp feedback cùng lúc, reinforcing nhau.