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
- 01 Con trỏ đổi hình dạng
- 02 Nút hút nam châm
- 03 Thẻ lộ sáng theo con trỏ
- 04 Thẻ nghiêng 3D
- 05 Thư viện kéo để cuộn
- 06 Menu chuột phải tùy chỉnh
- 07 Màu tô sáng khi chọn văn bản
- 08 Vòng con trỏ theo tiến độ cuộn
- 09 Lưới ảnh hiện chú thích khi hover
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.
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.
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.
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.
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.
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.
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 {
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.
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 và :focus-visible.
.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 và :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