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

9 hiệu ứng css skeleton loading copy-paste

Css skeleton loading là kỹ thuật hiện khối xám giữ đúng hình dạng của nội dung thật trong lúc chờ dữ liệu, thay cho màn hình trắng hoặc spinner đơn điệu.

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

Thứ tự chín kiểu đi theo đúng luồng của một trang thật khi dữ liệu chưa về kịp. Từ 01 đến 04 là bốn khối tĩnh đứng yên chờ — dòng chữ, card, avatar, bảng — mỗi khối ứng với một vùng giao diện quen thuộc. Mục 05 thêm chuyển động để làm rõ dần một tấm ảnh thay vì chỉ đứng im. Mục 06 giải quyết việc cùng một file skeleton phải vừa với nhiều khung chứa rộng hẹp khác nhau trên cùng một trang. Mục 07 là khoảnh khắc bàn giao — lúc skeleton nhường chỗ cho nội dung thật. Mục 08 và mục 09 là hai biến thể nâng cao hơn: độ trễ theo hàng, và màu tự đổi theo theme của trình duyệt.

01Dòng chữ lấp lánh

Bạn thấy gì

Thanh xám thay chỗ cho dòng chữ, còn ánh sáng quét qua nhờ một lớp gradient di chuyển bằng transform chứ không phải đổi màu nền trực tiếp.

Dòng chữ lấp lánh
Mở trong tab mới

01 · Dòng chữ lấp lánh — Diện tích đo được 11.078% · 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

1.4s느리게 2s빛 없이

Dùng ở đâu

Dùng cho danh sách bài viết và bình luận, nơi mỗi dòng dài ngắn khác nhau nên các thanh cũng cần rộng hẹp khác nhau để trông giống chữ thật.

.line {
  position: relative;
  overflow: hidden;
  height: clamp(12px, 4vw, 16px);
  border-radius: $r-control;
  background: rgba($color, .14);

  &::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(90deg, transparent, rgba($color, .45), transparent);
    transform: translateX(-120%);
    animation: shimmer $duration $easing infinite;
  }
}

@keyframes shimmer {
  to { transform: translateX(120%); }
}

02Skeleton dạng card

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

Một card có sẵn chỗ trống cho ảnh, tiêu đề và mô tả, dùng chung một placeholder %bar cho cả ba khối để chúng lấp lánh cùng kiểu nhưng lệch nhịp nhau đôi chút.

Skeleton dạng card
Mở trong tab mới

02 · Skeleton dạng card — Diện tích đo được 28.717% · 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

3장가로형둥글게

Chỗ dùng

Hợp cho card sản phẩm và card bài viết, những nơi người dùng lướt qua rất nhanh và cần thấy ngay bố cục sắp hiện ra.

%bar {
  position: relative;
  overflow: hidden;
  border-radius: $r-control;
  background: rgba($color, .16);

  &::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(90deg, transparent, rgba($stage-paper, .85), transparent);
    transform: translateX(-120%);
    animation: shimmer $duration $easing infinite;
  }
}

.thumb { @extend %bar; aspect-ratio: 2 / 1; }
.title { @extend %bar; height: clamp(12px, 3.6vw, 16px); width: 70%; }
.desc  { @extend %bar; height: clamp(9px, 3vw, 12px); }

03Danh sách avatar

Chuyển động thế nào

Một avatar tròn cùng hai dòng chữ lặp lại theo từng hàng, còn ánh sáng thì quét một lần cho cả danh sách thay vì cho từng thanh riêng lẻ như hai kiểu trước.

Danh sách avatar
Mở trong tab mới

03 · Danh sách avatar — Diện tích đo được 44.317% · khung hình chuyển động 17/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

5줄작게글 한 줄

Dùng khi nào

Dùng cho khung chat và danh sách người theo dõi, nơi nhiều hàng giống nhau xếp liền kề.

.avatar {
  flex: none;
  width: clamp(20px, 6.4vw, 32px);
  height: clamp(20px, 6.4vw, 32px);
  border-radius: $r-pill;
  background: rgba($color, .22);
}

.sheen {
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, rgba($stage-paper, .4), transparent);
  transform: translateX(-120%);
  animation: shimmer $duration $easing infinite;
  pointer-events: none;
}

04Skeleton bảng

Bạn thấy gì

Header của bảng vẫn giữ nguyên chữ thật, chỉ các ô trong thân bảng biến thành thanh xám, nhờ hàng thân và hàng header dùng chung một grid-template-columns nên độ rộng cột không nhảy khi dữ liệu thật thế chỗ.

Skeleton bảng
Mở trong tab mới

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

6행열 폭 유지줄무늬

Dùng ở đâu

Dùng cho bảng trong dashboard, nơi người dùng cần thấy ngay số cột từ giây đầu tiên.

.row {
  display: grid;
  grid-template-columns: 2fr 1fr 1fr 1fr;
  gap: clamp(6px, 2vw, 10px);
  padding: clamp(6px, 2vw, 9px) clamp(10px, 3vw, 14px);
  align-items: center;
}

.body {
  position: relative;
  overflow: hidden;
}

.cell {
  height: clamp(9px, 2.6vw, 11px);
  border-radius: $r-control;
  background: rgba($color, .13);
}

05Ảnh mờ dần rõ

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

Một phiên bản ảnh cực nhỏ được làm mờ bằng filter: blur() rồi phóng to nhẹ để che viền, hiện ra trước tiên; sau đó ảnh thật chồng lên trên và nét dần bằng opacity chứ không đổi kích thước đột ngột.

Ảnh mờ dần rõ
Mở trong tab mới

05 · Ảnh mờ dần rõ — Diện tích đo được 49.817% · khung hình chuyển động 22/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

20px400ms확대 섞기

Chỗ dùng

Hợp cho ảnh hero và gallery, nơi một tấm ảnh trắng trơn trông trống trải hơn nhiều so với một vệt màu mờ đúng dáng ảnh.

.placeholder, .real {
  position: absolute;
  inset: 0;
  background-image:
    repeating-linear-gradient(135deg, rgba($subject-cream, .85) 0 6px, rgba($color, .85) 6px 12px),
    radial-gradient(circle at 30% 30%, rgba($subject-cream, .5), transparent 55%);
  background-color: $stage-ink;
}

.placeholder {
  filter: blur(20px);
  transform: scale(1.15);
}

.real {
  opacity: 0;
  transform: scale(1.04);
  animation: reveal $duration $easing infinite;
}

06Skeleton responsive

Chuyển động thế nào

Số cột của lưới đổi theo độ rộng thật của khung chứa nó, không phải theo độ rộng màn hình, nhờ một container query gắn trên chính .wrap.

Skeleton responsive
Mở trong tab mới

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

3열2열 @ 480px1열 @ 320px

Dùng khi nào

Cùng một file này đặt trong sidebar hẹp sẽ tự co xuống 2 cột rồi 1 cột, còn đặt trong nội dung chính vẫn giữ nguyên 3 cột, mà không cần viết hai bộ CSS khác nhau.

.wrap {
  container-type: inline-size;
  container-name: skeleton;
  max-width: 420px;
}

.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(6px, 2vw, 10px);
}

@container skeleton (max-width: 260px) {
  .grid { grid-template-columns: repeat(2, 1fr); }
}
@container skeleton (max-width: 150px) {
  .grid { grid-template-columns: 1fr; }
}

07Chuyển sang nội dung

Bạn thấy gì

Các thanh skeleton và nội dung thật nằm chồng lên nhau trong cùng một ô grid, rồi lần lượt fade-out và fade-in chứ không dịch chuyển hay đổi kích thước, nên không có cú giật nào khi nội dung thế chỗ.

Chuyển sang nội dung
Mở trong tab mới

07 · Chuyển sang nội dung — Diện tích đo được 8.035% · 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

300ms위로 살짝페이드만

Dùng ở đâu

Đây là khung hình cuối của mọi skeleton, chỗ quyết định người dùng có cảm thấy trang "mượt" hay không.

.skeleton {
  animation: fade-out $duration $easing infinite;
}

@keyframes fade-out {
  0%, 40%   { opacity: 1; }
  55%, 85%  { opacity: 0; }
  100%      { opacity: 1; }
}

.content {
  animation: fade-in $duration $easing infinite;
}

@keyframes fade-in {
  0%, 40%   { opacity: 0; transform: translateY(6px); }
  55%, 85%  { opacity: 1; transform: translateY(0); }
  100%      { opacity: 0; transform: translateY(6px); }
}

08Lấp lánh theo hàng

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

Mỗi hàng lấp lánh trễ hơn hàng trước nó đúng 60ms, dùng vòng lặp SCSS @for để gán animation-delay âm cho từng hàng thay vì chép tay chín dòng CSS, nên toàn bộ trông như một con sóng chạy dọc danh sách.

Lấp lánh theo hàng
Mở trong tab mới

08 · Lấp lánh theo hàng — Diện tích đo được 13.818% · khung hình chuyển động 17/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

60ms역방향2열

Chỗ dùng

Hợp cho danh sách dài, nơi một lần lấp lánh đồng loạt trông đơn điệu hơn nhiều so với nhịp so le.

.bar {
  position: relative;
  overflow: hidden;
  height: clamp(11px, 3.2vw, 14px);
  border-radius: $r-control;
  background: rgba($color, .12);

  &::after {
    content: "";
    position: absolute;
    inset: 0;
    animation: shimmer $duration $easing infinite;
  }
}

@for $i from 1 through 6 {
  .bar:nth-child(#{$i})::after { animation-delay: -($i - 1) * 60ms; }
}

.bar:nth-child(even)::after { animation-name: shimmer-reverse; }

09Tự đổi màu dark mode

Chuyển động thế nào

Màu nền và màu thanh skeleton tự đổi theo theme của người dùng chỉ nhờ hàm light-dark(), miễn là color-scheme đã khai báo cả hai chế độ trên gốc trang.

Tự đổi màu dark mode
Mở trong tab mới

09 · Tự đổi màu dark mode — Diện tích đo được 15.289% · 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

light-dark()대비 높게회색 대신 파랑

Dùng khi nào

Hợp cho mọi trang có dark mode, vì không cần viết riêng một khối @media (prefers-color-scheme: dark) chỉ để đổi vài màu skeleton.

:root {
  color-scheme: light dark;
}

.card {
  background: light-dark(#ffffff, #1a1a1a);
  box-shadow: $shadow-raised;
}

.bar {
  background: light-dark(rgba(#000, .16), rgba(#fff, .22));
  height: clamp(11px, 3.2vw, 14px);
  border-radius: $r-control;
}

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

Container query trên trình duyệt cũ

Cái bẫy đầu tiên nằm ở mục 06: container query chỉ chạy được từ Chrome 105, Safari 16 và Firefox 110 trở lên, nên trên một trình duyệt cũ hơn thì .grid giữ nguyên 3 cột kể cả khi khung chứa đã hẹp lại, tràn ra ngoài thay vì co gọn. Cái bẫy thứ hai nằm ở mục 09: light-dark() cần Chrome 123, Safari 17.5 hoặc Firefox 120 trở lên, còn thiếu color-scheme: light dark trên :root thì hàm này luôn trả về giá trị sáng dù hệ điều hành đang bật dark mode. Cái bẫy thứ ba nằm ngay ở phần xem trước phía trên: lưới đó đặt trong một khung iframe không cuộn được, nên mỗi ô tự lặp lại vòng chuyển động của nó cứ mỗi hai giây một lần thay vì chờ dữ liệu thật tải xong — bạn chỉ xem chuyển động lặp ở đây thôi, không có gì để bấm cả. Những mốc trình duyệt và số mili giây trong chín kiểu này đã canh theo đúng bản demo thật, và nếu bạn muốn dừng đọc để mở file ngay thì mật khẩu của zip là a3rjf9rd.

Khả năng tiếp cận

Cách tôn trọng prefers-reduced-motion

Chín demo đều tôn trọng prefers-reduced-motion, nhưng không phải lúc nào cũng bằng cách tắt hẳn chuyển động. Từ 01 đến 04 và mục 08 giữ nguyên opacity ở mức thấy rõ và bỏ animation lấp lánh, để khối skeleton vẫn đứng yên đúng hình dạng chứ không biến mất. Mục 05 và mục 07 khoá thẳng vào trạng thái cuối — ảnh thật hiện rõ, nội dung thật hiện ra — vì một vòng mờ-rõ hay một vòng phai-hiện lặp mãi không đủ tính trang trí để giữ khi người dùng đã tắt chuyển động. Mục 06 giữ nguyên chuyển động co giãn cột vì đó là phản hồi bố cục thật chứ không phải hiệu ứng trang trí, còn mục 09 dừng ở bản dark, không còn phai qua phai lại giữa hai bản màu.

Xem thêm các hiệu ứng CSS khác trong chuyên mục CSS, và trang giới thiệu nói rõ site này thực chất là gì.

FAQ

Css skeleton loading có cần JavaScript không?

Không nhất thiết. Cả chín kiểu ở đây chỉ dùng SCSS thuần — animation, gradient, container query, light-dark() — không có dòng JS nào điều khiển chuyển động. JavaScript chỉ cần khi bạn muốn tự tay bật/tắt skeleton lúc dữ liệu về, còn bản thân hiệu ứng chạy độc lập với việc đó.

Vì sao dùng transform thay vì đổi vị trí left hay width khi lấp lánh?

Vì transform: translateX() chạy trên luồng riêng của trình duyệt (compositor) mà không bắt trang tính lại layout ở mỗi khung hình, còn đổi left hay width thì có. Với một danh sách skeleton dài, đổi layout liên tục dễ làm chuyển động giật, trong khi transform giữ được nhịp mượt suốt lúc chờ tải.

Container query và light-dark() có chạy được trên mọi trình duyệt không?

Chưa phải mọi trình duyệt. Container query cần Chrome 105+, Safari 16+ hoặc Firefox 110+, còn light-dark() cần Chrome 123+, Safari 17.5+ hoặc Firefox 120+. Với site cần hỗ trợ bản trình duyệt cũ hơn, mục 06 vẫn giữ được 3 cột mặc định an toàn còn mục 09 vẫn hiện đúng bản sáng nếu không có color-scheme, nên cả hai không vỡ hẳn, chỉ mất phần nâng cao.

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.