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

CSS only carousel — 9 image slider css dán là chạy

CSS only carousel là màn hình cho vài tấm ảnh trượt ngang lần lượt, kiểu hay gặp ở shop online hay trang portfolio.

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

Chín bản css carousel dưới đây không xếp theo độ nổi tiếng mà theo vai trò mà carousel đảm nhận trên màn hình. Từ bản cơ bản nhất đẩy bằng tay (01), chuyển sang nhóm bày cách điều khiển bằng chấm, mũi tên, thumbnail (02~04), rồi nhóm tự quay dù bạn không đụng tới (05~06), và cuối cùng là nhóm trang trí nhấn mạnh bạn đang xem tấm nào (07~09).

01scroll-snap cơ bản

Thẻ dừng khít vào khung

Đây là dạng image slider css cơ bản nhất: bốn thẻ ảnh vuông nằm ngang cạnh nhau, đẩy bằng ngón tay hay con lăn chuột thì thẻ dừng khít đúng khung màn hình.

scroll-snap cơ bản
Mở trong tab mới

01 · scroll-snap cơ bản — Diện tích đo được 62.372% · khung hình chuyển động 19/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

scroll-snap-typescroll-snap-alignJS 0줄

Vì sao chỉ hai thuộc tính là xong

Gắn overflow-x:auto với scroll-snap-type là hết việc, không còn dòng JavaScript nào.

.cs__viewport {
  overflow-x: auto;
  scroll-snap-type: x mandatory;
}
.cs__track { display: flex; width: 400%; }
.cs__slide {
  flex: 0 0 25%;
  scroll-snap-align: start;
}

02Chấm ::scroll-marker

Chỉ chấm đang xem được tô đầy

Dưới slide có mấy chấm nhỏ, chỉ chấm của tấm ảnh bạn đang xem mới được tô màu và to lên một chút.

Chấm ::scroll-marker
Mở trong tab mới

02 · Chấm ::scroll-marker — Diện tích đo được 49.997% · khung hình chuyển động 20/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

::scroll-markerscroll-marker-group:target-current

Trình duyệt tự vẽ chấm

Chấm không phải do JavaScript vẽ, trình duyệt tự tạo cho mỗi slide một cái bằng ::scroll-marker.

.viewport { scroll-marker-group: after; }
.slide::scroll-marker {
  content: "";
  width: 8px; height: 8px;
  border-radius: 50%;
}
.slide::scroll-marker:target-current {
  background: $color;
  transform: scale(1.35);
}

03Mũi tên ::scroll-button

Mũi tên đưa qua từng tấm

Hai bên slide có nút mũi tên trước, sau, bấm vào thì ảnh trượt mượt qua từng tấm một.

Mũi tên ::scroll-button
Mở trong tab mới

03 · Mũi tên ::scroll-button — Diện tích đo được 56.113% · khung hình chuyển động 20/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

::scroll-button(*):disabledscroll-snap-type

Kiểu mờ đi ở hai đầu

Nút mũi tên cũng do trình duyệt tự tạo bằng ::scroll-button(), và được tạo kiểu để mờ đi khi chạm tới hai đầu.

.viewport::scroll-button(left)  { content: "‹"; }
.viewport::scroll-button(right) { content: "›"; }
.viewport::scroll-button(left):disabled,
.viewport::scroll-button(right):disabled {
  opacity: 0;
  pointer-events: none;
}

04Thumbnail làm marker

Thumbnail khớp với ảnh lớn

Dưới ảnh lớn là dãy thumbnail vuông nhỏ, chỉ thumbnail cùng màu với ảnh lớn đang xem mới hiện viền.

Marker hình thu nhỏ
Mở trong tab mới

04 · Marker hình thu nhỏ — Diện tích đo được 49.928% · khung hình chuyển động 19/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

::scroll-markerCSS 변수 상속:target-current

Cách kế thừa màu

Thumbnail cũng chỉ là một ::scroll-marker, còn màu thì kế thừa thẳng biến CSS ghi sẵn trên slide gốc.

.slide { background: var(--thumb); }
.slide::scroll-marker {
  content: "";
  background: var(--thumb);
  border: 2px solid transparent;
}
.slide::scroll-marker:target-current {
  border-color: $color;
}

05Autoplay chỉ bằng CSS

Cho carousel tự quay

Bạn không bấm gì cả mà cứ vài giây một lần ảnh lại tự chuyển sang tấm kế tiếp.

Tự động phát chỉ bằng CSS
Mở trong tab mới

05 · Tự động phát chỉ bằng CSS — Diện tích đo được 62.373% · khung hình chuyển động 20/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

@keyframesanimation-play-stateJS 0줄

Rê chuột vào là dừng

Rê chuột lên thì nó dừng một lát, đủ để bạn xem kỹ tấm mình muốn.

.ap__track {
  display: flex;
  width: 400%;
  animation: ap-cycle $duration $easing infinite;
}
.ap:hover .ap__track { animation-play-state: paused; }
@keyframes ap-cycle {
  0%, 17%  { transform: translateX(0%); }
  25%, 42% { transform: translateX(-25%); }
  50%, 67% { transform: translateX(-50%); }
  75%, 92% { transform: translateX(-75%); }
  100%     { transform: translateX(0%); }
}

06Slider đánh giá

Đổi từng tấm bằng fade

Card đánh giá của khách hàng đổi từng tấm bằng fade rồi tự xoay vòng liên tục.

Slider đánh giá
Mở trong tab mới

06 · Slider đánh giá — Diện tích đo được 3.304% · khung hình chuyển động 12/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

opacitygrid 겹침@keyframes

Vì sao chiều cao không rung

Các card xếp chồng đúng một chỗ nên dù chữ đánh giá dài ngắn khác nhau, chiều cao vẫn không rung lên xuống.

.ts__card { grid-area: 1 / 1; opacity: 0; }
.ts__card--1 { animation: ts-1 $duration $easing infinite; }
.ts__card--2 { animation: ts-2 $duration $easing infinite; }
.ts__card--3 { animation: ts-3 $duration $easing infinite; }
@keyframes ts-1 { 0%, 4%   { opacity: 1; } 29%, 100% { opacity: 0; } }
@keyframes ts-2 { 0%, 29%  { opacity: 0; } 33%, 62%  { opacity: 1; } 66%, 100% { opacity: 0; } }
@keyframes ts-3 { 0%, 62%  { opacity: 0; } 66%, 96%  { opacity: 1; } 100%     { opacity: 0; } }

07Phóng to slide đang xem

Giữa to lên, hai bên mờ đi

Tấm ở giữa được phóng to hơn chút, hai tấm bên cạnh nhỏ lại và mờ đi, nên bạn nắm ngay mình đang xem tấm nào.

Phóng to slide đang xem
Mở trong tab mới

07 · Phóng to slide đang xem — Diện tích đo được 46.955% · 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

:checkedtransform: scaleopacity

Bấm vào là ra giữa

Bấm vào tấm bạn muốn thì tấm đó ra giữa và to trở lại.

.slide {
  transform: scale(.82);
  opacity: .5;
  transition: transform .3s, opacity .3s;
}
#as-2:checked ~ .as__track .as__slide--2 {
  transform: scale(1.08);
  opacity: 1;
}

08Marker tiến độ liên động

Thanh đầy theo số tấm đã qua

Phía trên slide có một thanh tiến độ mỏng, ảnh chuyển tới đâu thanh đầy tới đó.

Đồng bộ marker tiến trình
Mở trong tab mới

08 · Đồng bộ marker tiến trình — Diện tích đo được 51.043% · khung hình chuyển động 21/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

:checkedwidth 전환형제결합자

Bấm thanh để nhảy thẳng

Bấm thẳng vào thanh tiến độ cũng nhảy ngay tới tấm ảnh ở chỗ bạn bấm.

.pm__fill {
  width: calc(100% / 3);
  transition: width .3s;
}
#pm-2:checked ~ .pm__bar .pm__fill { width: calc(200% / 3); }
#pm-3:checked ~ .pm__bar .pm__fill { width: 100%; }

09Coverflow 3D

Như lật bìa đĩa nhạc

Tấm giữa đứng thẳng mặt và to, các tấm hai bên nghiêng chéo lùi ra sau và nhỏ lại, trông như bạn đang lật bìa đĩa nhạc bằng tay.

Coverflow 3D
Mở trong tab mới

09 · Coverflow 3D — Diện tích đo được 36.764% · 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

perspectiverotateY + translateZ:checked

Bấm tấm nào, tấm đó quay ra trước

Bấm vào tấm bạn muốn thì tấm đó quay ra chính giữa, đối diện người xem.

.cf__stage { perspective: 720px; }
.cf__slide { transform-style: preserve-3d; }
#cf-2:checked ~ .cf__stage .cf__slide--2 {
  transform: translateX(-50%) rotateY(0deg) scale(1);
}
#cf-2:checked ~ .cf__stage .cf__slide--3 {
  transform: translateX(8%) translateZ(-120px) rotateY(-38deg) scale(.74);
}

Carousel dễ hỏng ở đâu — một cái bẫy

Khi bạn gắn transform lên hộp cuộn

Cái bẫy lớn nhất trong chín mục rơi vào lúc dựng bản xem trước của autoplay. Gắn translateX thẳng lên hộp cuộn (phần tử mang overflow-x:auto) thì toàn bộ thẻ biến mất khỏi màn hình. Con số 100% trong translateX(-100%) luôn tính theo bề rộng của chính phần tử đó, nên nó chỉ đẩy đi đúng bề rộng hộp cuộn (bằng một thẻ) chứ không phải bề rộng nội dung bốn thẻ nằm trong hộp. Đo trực tiếp bằng getBoundingClientRect thì lượng dịch mong muốn là bề rộng bốn thẻ cộng lại (quanh 1600px), còn thực tế chỉ nhích đúng bề rộng một thẻ (400px), và chênh lệch ấy đẩy thẻ ra hẳn ngoài viewport, màn hình chỉ còn trơ nền sân khấu. Vậy nên phải để nguyên hộp cuộn và chỉ gắn transform lên hàng nội dung (track) bên trong thì kéo tay và snap vẫn sống nguyên mà autoplay cũng chạy. Mật khẩu 9jk4bv6c của file nén này mở chín file đã sửa đúng theo cách ấy.

:disabled của scroll-button chưa thật gọn

Trạng thái :disabled của ::scroll-button() — tức mũi tên mờ đi ở hai đầu — thử ra thì ngay cả trên nhánh Chromium cũng chưa hoàn toàn gọn ghẽ, nên dù mũi tên còn hiện nhạt, chín bản này vẫn chặn để nó không bấm thêm được nữa ở biên cuộn.

Khả năng tiếp cận

Cái gì dừng lại khi giảm chuyển động

Cả chín mục đều tắt animation autoplay dưới prefers-reduced-motion: reduce và đứng yên ở slide bạn đang xem. Mục 07·08·09 dựng bằng radio là form control gốc nên Tab và phím mũi tên điều khiển được ngay, còn 02·03·04 dùng ::scroll-marker với ::scroll-button thì chỉ cần focus tới là phím mũi tên chuyển slide. Mỗi thẻ của 07·09 mang aria-label="slide N" để trình đọc màn hình phân biệt được các thẻ với nhau.

Bài đọc tiếp

Các hiệu ứng CSS khác dùng cho portfolio hay landing page thì tìm thêm ở mục CSS animation, còn cách trang này vận hành thì ghi ở trang giới thiệu. Hành vi chính xác của scroll-snap-type xem tại tài liệu MDN về scroll-snap-type.

FAQ

::scroll-marker của css only carousel chạy trên mọi trình duyệt chứ?

Tại thời điểm viết bài, hành vi này đã được xác nhận trực tiếp trên nhánh Chromium. Trình duyệt chưa hỗ trợ thì chỉ mất chấm với mũi tên thôi, còn scroll-snap-type vẫn hoạt động bình thường, nên nó lùi êm về dạng cơ bản (01) đẩy bằng ngón tay hay con lăn.

Người dùng kéo tay giữa lúc autoplay thì sao?

Ngay khoảnh khắc chạm vào slide (pointerdown), class autoplay bị gỡ liền, từ đó về sau chỉ còn thao tác cuộn và bấm của người dùng có tác dụng. Không có chuyện autoplay giành nhau với thao tác thật.

Bản React có nguyên lý khác không?

Mục 01~04 chép nguyên scroll-snap-type, ::scroll-marker, ::scroll-button() sang CSS module nên nguyên lý y hệt. Chỉ mấy mục vốn chọn bằng radio input như 07~09 mới đổi sang một useState, còn duration, easing, color thì nhận qua props — ai cần css carousel scroll snap cho dự án React thì dùng thẳng phần 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.