GODRICH

9 hiệu ứng chuột máy tính bạn có thể copy-paste

Hiệu ứng chuột máy tính là tương tác nhỏ phản ứng theo cách con trỏ di chuyển, click, kéo hoặc cuộn.

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

Chín hiệu ứng dưới đây không xếp theo độ phổ biến, mà theo đúng thứ tự con trỏ gặp trên một trang: đứng yên rồi đổi hình trên liên kết (01), bị hút về phía nút (02), ánh sáng và độ nghiêng lướt qua các thẻ (03·04), kéo màn hình sang ngang (05), bấm chuột phải để mở menu (06), tô màu câu vừa chọn (07), vòng tròn đầy dần khi đọc xuống trang (08), và cuối cùng là caption lướt qua lưới ảnh (09).

01Con trỏ đổi hình dạng

Di chuột vào liên kết, chấm con trỏ nhỏ phồng thành vòng tròn xanh có mũi tên bên trong; rời ra thì thu lại thành chấm. pointermove di chuyển vị trí chấm, còn pointerenter/pointerleave đổi hình dạng.

pointermove translatedata-cursor 속성pointer:coarse 터치 제외
area.addEventListener('pointermove', function (e) {
  var r = area.getBoundingClientRect();
  cursor.style.transform =
    'translate(' + (e.clientX - r.left) + 'px,' +
    (e.clientY - r.top) + 'px)';
});
link.addEventListener('pointerenter', function () {
  cursor.classList.add('is-link');
});
link.addEventListener('pointerleave', function () {
  cursor.classList.remove('is-link');
});

02Nút hút nam châm

Đưa con trỏ đến gần nút sẽ kéo nút hơi lệch về phía đó, rời xa thì bật trở lại vị trí cũ. Khoảng cách từ tâm vùng chạm được nhân với hệ số (0.4) để tính giá trị translate.

pointermove 거리×계수transform: translatepointerleave 리셋
zone.addEventListener('pointermove', function (e) {
  var r = zone.getBoundingClientRect();
  var dx = e.clientX - (r.left + r.width / 2);
  var dy = e.clientY - (r.top + r.height / 2);
  btn.style.transform =
    'translate(' + dx * 0.4 + 'px,' + dy * 0.4 + 'px)';
});
zone.addEventListener('pointerleave', function () {
  btn.style.transform = '';
});

03Thẻ lộ sáng theo con trỏ

Di chuột trên thẻ, một đốm sáng tròn di chuyển theo và làm sáng chữ mờ ở nơi ánh sáng lướt qua. mix-blend-mode: soft-light trộn ánh sáng vào nền, còn screen làm sáng chữ bên dưới.

mix-blend-mode: soft-lightpointermove translatehover:none 대체
card.addEventListener('pointermove', function (e) {
  var r = card.getBoundingClientRect();
  spot.style.transform =
    'translate(' + (e.clientX - r.left) + 'px,' +
    (e.clientY - r.top) + 'px)';
});
card.addEventListener('pointerleave', function () {
  spot.style.transform = '';
});

04Thẻ nghiêng 3D

Thẻ nghiêng theo không gian 3D tùy vị trí con trỏ, kèm ánh sáng chéo lướt qua. Vị trí con trỏ được đổi thành tỉ lệ 0–1, rồi thành góc rotateX/rotateY.

perspective + rotateX/rotateYbackground-position 스윕신뢰감 있는 밝은 톤
wrap.addEventListener('pointermove', function (e) {
  var r = wrap.getBoundingClientRect();
  var px = (e.clientX - r.left) / r.width;
  var py = (e.clientY - r.top) / r.height;
  var rx = (0.5 - py) * 6 * 2;
  var ry = (px - 0.5) * 6 * 2;
  card.style.transform =
    'perspective(600px) rotateX(' + rx + 'deg) rotateY(' + ry + 'deg)';
});

05Thư viện kéo để cuộn

Kéo các ô như nắm bằng tay sẽ cuộn thư viện theo chiều ngang, thả ra thì dừng lại. Đây là scrollLeft thật, không phải transform, nên cuộn bằng tay trên điện thoại vẫn giữ nguyên cảm giác cuộn gốc.

scrollLeft 직접 조작overflow-x: auto 네이티브 터치cursor: grab/grabbing
vp.addEventListener('pointerdown', function (e) {
  dragging = true;
  startX = e.clientX;
  startScroll = vp.scrollLeft;
  vp.setPointerCapture(e.pointerId);
});
vp.addEventListener('pointermove', function (e) {
  if (!dragging) return;
  vp.scrollLeft = startScroll - (e.clientX - startX);
});

06Menu chuột phải tùy chỉnh

Bấm chuột phải vào vùng này sẽ làm menu biểu tượng sao chép/sửa/xóa bật lên tại điểm bấm, thay cho menu mặc định của trình duyệt. Toạ độ được giới hạn để menu không bao giờ tràn ra ngoài vùng.

contextmenu preventDefault좌표 클램프Escape·바깥 클릭 닫기
pad.addEventListener('contextmenu', function (e) {
  e.preventDefault();
  var r = pad.getBoundingClientRect();
  var x = Math.min(e.clientX - r.left, r.width - 132);
  var y = Math.min(e.clientY - r.top, r.height - 112);
  menu.style.left = x + 'px';
  menu.style.top = y + 'px';
  pad.classList.add('is-open');
});

07Màu tô sáng khi chọn văn bản

Kéo chọn văn bản sẽ tô bằng màu highlighter đã chọn thay vì màu xanh mặc định, và huy hiệu sao chép hiện phía trên. Màu được đặt bằng đúng một dòng ::selection, vị trí huy hiệu lấy từ selectionchange.

::selection 색 지정selectionchange 이벤트getBoundingClientRect 배지 위치
::selection {
  background: #ff9ebb;
  color: #17141a;
}
document.addEventListener('selectionchange', function () {
  var sel = window.getSelection();
  if (sel.isCollapsed) return;
  var r = sel.getRangeAt(0).getBoundingClientRect();
  badge.style.left = r.left + r.width / 2 + 'px';
  badge.style.top = r.top - 28 + 'px';
  badge.classList.add('is-shown');
});

08Vòng con trỏ theo tiến độ cuộn

Khi cuộn bên trong, vòng nhỏ theo con trỏ sẽ đầy dần theo tiến độ, đến cuối thì chuyển thành dấu tick. stroke-dashoffset của vòng giảm theo đúng tỉ lệ đã cuộn.

stroke-dashoffsetscrollTop/scrollHeight 비율pointermove translate
box.addEventListener('scroll', function () {
  var max = box.scrollHeight - box.clientHeight;
  var pct = max > 0 ? box.scrollTop / max : 0;
  ring.style.setProperty('--dash', (94.2 * (1 - pct)).toFixed(1));
  box.classList.toggle('is-done', pct > 0.98);
});

09Lưới ảnh hiện chú thích khi hover

Di chuột vào ô lưới, dải chú thích trắng trượt lên từ dưới và hiện tên; focus bàn phím cũng phản ứng như vậy. Không cần JS — chỉ :hover:focus-visible.

:hover + :focus-visibletransform: translateYstagger animation-delay
.hg-tile__cap {
  transform: translateY(100%);
  transition: transform .3s ease-out;
}
.hg-tile:hover .hg-tile__cap,
.hg-tile:focus-visible .hg-tile__cap { transform: translateY(0); }

Hiệu ứng chuột máy tính dễ hỏng ở đâu?

Cái đầu tiên gặp phải là thẻ 04, thẻ nghiêng 3D. rotateX/rotateY chỉ nghiêng về mặt hình ảnh và không nên tính lại layout, nhưng ở độ rộng 320px, hộp giới hạn nhìn thấy của thẻ đã xoay vẫn bị tính vào vùng cuộn, khiến trang tràn sang ngang. Góc 9 độ ban đầu vẫn còn tràn; chỉ sau khi giảm xuống 6 độ và thu nhỏ thẻ lại thì bản render 320px mới hết tràn. Cái thứ hai là thẻ 05, thư viện kéo: giả lập việc kéo bằng ngón tay với transform sẽ lệch khỏi vị trí cuộn thật, nên khi kéo thật phải di chuyển trực tiếp scrollLeft. Cái thứ ba là menu chuột phải ở thẻ 06 — nếu không giới hạn toạ độ, menu sẽ bị cắt ra ngoài ở cạnh phải hoặc dưới của vùng.

Tiếp cận — khi con trỏ tùy chỉnh lại gây khó dùng

Các thẻ 01, 03, 08 có phần tử bám theo con trỏ, nên toàn bộ transition/animation của chúng tắt khi prefers-reduced-motion: reduce. Nhưng vấn đề thật không phải là chuyển động mà là chính con trỏ — với người khiếm thị nhẹ, hoặc người dùng switch/joystick thay cho trackpad, một con trỏ tùy chỉnh nhỏ có thể khó thấy hơn con trỏ hệ thống. Vì vậy thẻ 01 ẩn hẳn điểm con trỏ bằng @media (hover: none), (pointer: coarse) trên thiết bị chạm, thẻ 08 thì trả lại con trỏ hệ thống trong cùng điều kiện đó. Thẻ 02, 04 vốn không có phần tử con trỏ riêng để ẩn — chỉ chuyển tiếp theo con trỏ bị tắt. Thẻ 03 thì làm sáng chữ ngay từ đầu thay vì ẩn spotlight. Thẻ 05, thư viện kéo, lại hoạt động tốt hơn trên cảm ứng vì cuộn gốc vẫn còn nguyên bên dưới. Thẻ 06, 07, 09 mỗi thẻ đều đi kèm một cách thay thế bằng bàn phím hoặc focus (Escape, Tab, :focus-visible) cho thao tác chỉ dành cho chuột (chuột phải, kéo chọn, hover), để vẫn dùng được khi không có chuột. Mật khẩu giải nén chín file này là c5gmgmdf, và sau khi giải nén, hai thư mục vanilla và react cho phép so sánh cùng chín hành vi đó cạnh nhau.

FAQ

Hiệu ứng chuột máy tính có thể làm chỉ bằng CSS không?

Một số thì có. Màu chọn ở thẻ 07 và lưới caption hover ở thẻ 09 chạy chỉ bằng ::selection:hover. Số còn lại cần đọc toạ độ con trỏ, nên vài dòng JS thuần với pointermove là không tránh được.

Con trỏ tùy chỉnh có làm trang chậm đi không?

Chỉ đổi transform ở mỗi pointermove thì không cần tính lại layout, nên hầu hết vẫn mượt. Tuy vậy, với phần tử không đổi kích thước, nên lưu lại getBoundingClientRect một lần thay vì đo lại ở mỗi sự kiện.

Các hiệu ứng này có hoạt động trên di động không?

Thẻ 01, 02, 03, 04, 08 giả định có con trỏ chuột, nên trên cảm ứng chúng được ẩn hoặc chuyển sang trạng thái khác bằng CSS. Thư viện kéo ở thẻ 05 và các thẻ 06, 07, 09 được làm riêng để hoạt động tự nhiên trên cảm ứng.

Nguồn: MDN Pointer events, MDN prefers-reduced-motion · Xem thêm: Giới thiệu, Tương tác hover, Tương tác trang landing

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.