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

9 CSS Border Animations — dán 1 dòng là chạy

CSS border animation là hiệu ứng viền của card hay ô nhập di chuyển, phát sáng hoặc đổi màu bằng thuần CSS để báo một trạng thái đang diễn ra.

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

Thứ tự chín viền dưới đây đi theo loại tín hiệu mà chúng đang báo: 01 đến 04 báo có gì đó đang xử lý hoặc đang tải, 05 và 06 đánh dấu chỗ bạn vừa chọn hoặc vừa đưa focus tới, 07 chỉ phản hồi khi bạn hover vào, 08 dành cho một khoảnh khắc đặc biệt, còn 09 là vòng focus dành riêng cho bàn phím. Mỗi phần dưới đây chỉ trích đoạn SCSS quyết định chuyển động, còn phần thiết lập HTML đầy đủ của cả chín file nằm gọn trong bản nén ở cuối bài.

01Viền quét sáng chéo

Bạn thấy gì

Một vòng mỏng cắt đúng hình viền mang gradient sọc chéo, rồi trượt background-position không ngừng khiến cả khung trông như đang chảy sáng cùng lúc.

Viền quét sáng chéo
Mở trong tab mới

01 · Viền quét sáng chéo — Diện tích đo được 1.167% · khung hình chuyển động 23/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

mask-compositebackground-position2초

Dùng ở đâu

Bạn đặt kiểu này ở viền ô input trong lúc AI đang trả lời, hoặc quanh card gói cước cao cấp.

.frame::before {
  content: "";
  position: absolute; inset: 0; padding: 3px;
  background: repeating-linear-gradient(115deg, $color 0 10px, $subject-cream 10px 14px, transparent 14px 34px);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  animation: sweep $duration linear infinite;
}
@keyframes sweep { to { background-position: 220px 0; } }

02Vẽ viền bằng SVG

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

Viền card được vẽ bằng thẻ rect trong SVG với pathLength="100", nên bạn luôn làm việc với thang 0–100 bất kể chu vi thật của card là bao nhiêu pixel.

Vẽ viền bằng SVG
Mở trong tab mới

02 · Vẽ viền bằng SVG — Diện tích đo được 1.148% · 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

stroke-dashoffsetSVG0.6초

Chỗ dùng

Giảm stroke-dashoffset từ 100 về 0 khiến đường viền nối lại như một cây bút đang vẽ, hợp với khoảnh khắc thẻ vừa được phát hành.

// pathLength="100" trên rect — làm việc theo thang 0–100, không quan tâm chu vi thật là bao nhiêu px
.card__rect {
  stroke-dasharray: 100;
  stroke-dashoffset: 100;
  animation: draw-loop $dur-loop $easing infinite;
}
@keyframes draw-loop {
  0%, 5%   { stroke-dashoffset: 100; }
  35%, 60% { stroke-dashoffset: 0; }
  100%     { stroke-dashoffset: -100; }
}

03Nét đứt chạy quanh viền

Chuyển động thế nào

Mỗi cạnh trong số bốn cạnh mang một nền gradient nét đứt của riêng nó, và trượt background-position theo đúng hướng cạnh đó khiến nét đứt trông như đang chạy vòng theo chiều kim đồng hồ.

Nét đứt chạy quanh viền
Mở trong tab mới

03 · Nét đứt chạy quanh viền — Diện tích đo được 1.1% · khung hình chuyển động 23/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

repeating-linear-gradientbackground-position2초

Dùng khi nào

Đặt nó ở viền của card đang tải lên, hoặc ở bất kỳ đâu cần báo trạng thái chờ.

.frame__edge--top, .frame__edge--bottom {
  background-image: repeating-linear-gradient(90deg, $color 0 8px, transparent 8px 16px);
}
.frame__edge--left, .frame__edge--right {
  background-image: repeating-linear-gradient(180deg, $color 0 8px, transparent 8px 16px);
}
.frame.is-demo .frame__edge--top    { animation: run-x $duration linear infinite; }
.frame.is-demo .frame__edge--bottom { animation: run-x-rev $duration linear infinite; }
@keyframes run-x     { to { background-position: 32px 0; } }
@keyframes run-x-rev { to { background-position: -32px 0; } }

04Viền phát sáng nhấp nháy

Bạn thấy gì

Một giá trị kiểu <length> mang tên --glow được đăng ký qua @property rồi animate từ 0 lên 16px, nên bán kính mờ của box-shadow lớn dần và nhỏ lại mượt mà thay vì nhảy khựng.

Viền phát sáng nhấp nháy
Mở trong tab mới

04 · Viền phát sáng nhấp nháy — Diện tích đo được 1.765% · khung hình chuyển động 8/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

@propertybox-shadow2초

Dùng ở đâu

Đây là kiểu bạn gắn cho badge thông báo hay card đang cập nhật theo thời gian thực.

@property --glow {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}
.badge {
  box-shadow: 0 0 var(--glow) 0 rgba($color, .55);
  animation: glow $duration $easing infinite;
}
@keyframes glow { 0%, 100% { --glow: 0px; } 50% { --glow: 16px; } }

05Ngoặc góc

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

Bốn ngoặc chữ L đặt riêng ở bốn góc, mỗi cái mở ra theo đúng hướng chéo của góc đó khi hover hoặc focus, tạo cảm giác giống khung lấy nét của máy ảnh đang mở ra.

Ngoặc góc
Mở trong tab mới

05 · Ngoặc góc — Diện tích đo được 1.503% · khung hình chuyển động 15/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

transform: translate4개 요소0.3초

Chỗ dùng

Card trong thư viện ảnh dùng kiểu này để đánh dấu ảnh bạn vừa chọn.

.frame__bracket--tl { top: -3px; left: -3px; border-right: 0; border-bottom: 0; }
.frame__bracket--tr { top: -3px; right: -3px; border-left: 0; border-bottom: 0; }
.frame.is-demo .frame__bracket--tl { animation: open-tl $dur-loop $easing infinite; }
@keyframes open-tl {
  0%, 15%  { transform: translate(0, 0); }
  50%, 70% { transform: translate(-6px, -6px); }
  100%     { transform: translate(0, 0); }
}

06Gạch chân mở rộng

Chuyển động thế nào

Thanh chỉ báo dưới ô nhập giữ left: 50%; right: 50% để gập lại ở giữa, và khi ô nhập nhận focus cả hai giá trị chuyển về 0 cùng lúc nên thanh nở ra đều về hai bên.

Gạch chân mở rộng
Mở trong tab mới

06 · Gạch chân mở rộng — Diện tích đo được 0.306% · 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

left/right:focus0.22초

Dùng khi nào

Dùng left/right thay vì scaleX vì cách này cho phép mở rộng đúng tâm mà không cần khai riêng transform-origin.

.field__bar {
  position: absolute;
  left: 50%; right: 50%; bottom: -2px;
  background: $color;
  transition: left $duration $easing, right $duration $easing;
}
.field__input:focus + .field__bar { left: 0; right: 0; }

07Viền vòng đôi

Bạn thấy gì

box-shadow giữ một vòng trong cố định, còn ::after dựng thêm vòng thứ hai vốn ẩn đi lúc bình thường, khi hover hay focus nó scale lớn dần trong lúc opacity tăng dần nên trông như gợn sóng lan ra.

Viền vòng đôi
Mở trong tab mới

07 · Viền vòng đôi — Diện tích đo được 2.954% · 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

box-shadow 2겹transform: scale0.3초

Dùng ở đâu

Đây là cách nhấn hover cho nút bấm hay icon thông báo.

.btn::after {
  content: "";
  position: absolute; inset: -3px;
  border: 2px solid $color;
  opacity: 0;
  transform: scale(1);
  transition: transform $duration $easing, opacity $duration $easing;
}
.btn:hover::after, .btn:focus-visible::after { opacity: 1; transform: scale(1.3); }

08Viền cầu vồng

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

border-image-source giữ nguyên một gradient cầu vồng đứng yên, chỉ mỗi giá trị filter: hue-rotate quay từ 0deg đến 360deg khiến màu viền tuần hoàn tại chỗ mà gradient không hề dịch chuyển — khác với cú trượt nền ở mục 01.

Viền cầu vồng
Mở trong tab mới

08 · Viền cầu vồng — Diện tích đo được 0.761% · khung hình chuyển động 23/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

border-imagefilter: hue-rotate2초

Chỗ dùng

Banner sự kiện hay thiệp chúc mừng hợp với sắc màu xoay vòng này.

.badge {
  border: 4px solid;
  border-image-slice: 1;
  border-image-source: linear-gradient(90deg, $error, $stage-orange, $stage-yellow, $subject-blue, $error);
  animation: hue $duration linear infinite;
}
@keyframes hue { to { filter: hue-rotate(360deg); } }

09Vòng focus có animation

Chuyển động thế nào

outline giữ trong suốt lúc bình thường, chỉ khi :focus-visible mới đổi màu và kéo outline-offset từ 0 lên 4px cùng lúc nên vòng trông như bật ra rồi dừng lại.

Vòng focus có animation
Mở trong tab mới

09 · Vòng focus có animation — Diện tích đo được 2.264% · khung hình chuyển động 15/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

outline-offset:focus-visible0.22초

Dùng khi nào

Nó không phản ứng với cú click chuột, chỉ phản ứng khi focus đến từ bàn phím.

.field__input {
  outline: 2px solid transparent;
  outline-offset: 0;
  transition: outline-color $duration $easing, outline-offset $duration $easing;
}
.field__input:focus-visible { outline-color: $color; outline-offset: 4px; }

Bẫy nằm ở đâu?

Vùng hỗ trợ mask-composite

01 dựa vào mask-composite để chỉ giữ lại phần viền và khoét bỏ phần giữa. Không kèm tiền tố, thuộc tính này chạy được từ Chrome/Edge 120, Safari 15.4 và Firefox 53 (trang mask-composite của MDN); Chrome cũ hơn mốc đó chỉ đọc được bản có tiền tố -webkit-mask-composite: xor. Bỏ sót một trong hai dòng thì bản demo bạn tự dựng và test trên máy chạy đẹp, nhưng trên trình duyệt cũ hơn cả khối viền lại hiện ra như một ô vuông đặc, không khoét được phần giữa như mong đợi.

Đăng ký kiểu cho @property

Còn một điểm khác đáng nhớ ở 04, thuộc tính @property. Nếu chỉ animate một custom property mà không đăng ký kiểu dữ liệu, trình duyệt không nội suy được giá trị giữa hai mốc nên bước nhảy diễn ra tức thì thay vì mượt. Khai syntax: "<length>" qua @property mới khiến bán kính mờ của 04 lớn nhỏ trơn tru, và cú đăng ký đó chạy từ Chrome/Edge 85, Safari 16.4, Firefox 128 (trang @property của MDN). Bộ file đã gộp sẵn cả hai điểm vừa nói nằm trong bản nén, và mật khẩu mở nó là 4vruzyhz, gõ đúng như bạn thấy, không thêm khoảng trắng.

Chi phí vẽ lại của filter

Điểm thứ ba nằm ở 08, thuộc tính filter. filter vẽ lại toàn bộ nội dung con bên trong phần tử, nên nếu badge chứa nhiều chữ hay icon thì mỗi vòng quay hue-rotate sẽ buộc trình duyệt hợp thành lại cả vùng đó. Viền cầu vồng nên dừng ở kích thước badge nhỏ, đừng gắn lên cả một card lớn hay khung chứa rộng.

Khả năng tiếp cận

Giảm chuyển động theo từng viền

Chín viền đều lắng nghe prefers-reduced-motion: reduce, nhưng không phải viền nào cũng chỉ đơn giản tắt animation. 01, 03 và 08 đứng lại ở một khung hình tĩnh thay vì tiếp tục trượt hay xoay, còn 04 giữ nguyên bán kính mờ ở giá trị giữa. 09 là ngoại lệ có chủ đích: vòng focus bàn phím không phải trang trí mà là tín hiệu "bạn đang ở đây", nên chỉ transition bị tắt còn màu vẫn giữ nguyên.

@media (prefers-reduced-motion: reduce) {
  .frame.is-demo::before, .badge.is-demo, .field__bar { animation: none; transition: none; }
}

Bạn 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 bán gì.

FAQ

Có thể lấy đúng một hiệu ứng trong chín cái, bỏ phần còn lại không?

Được. Mỗi hiệu ứng đóng gói trong một khối class riêng như .frame, .badge, .btn hay .field, không chia sẻ trạng thái với mục khác, nên copy phần SCSS của một mục cùng khung HTML đi kèm là chạy độc lập ngay. Mục 06 và 09 là hai bản gọn nhất nếu bạn chỉ cần một chỉ báo focus đơn giản.

mask-composite thay bằng border-image được không?

Được một phần. 08 chỉ cần màu đứng yên nên border-image là đủ gọn, nhưng 01 cần trượt nền theo thời gian thực, mà border-image không có thuộc tính tương đương background-position để làm việc đó, nên buộc bạn quay lại mask-composite hoặc SVG như 02.

Bản React trong zip nhận biến CSS qua đâu?

Thư mục react/ trong file nén nhận duration, easing, color làm props rồi ghi vào style inline dưới dạng custom property. Các module SCSS chỉ đọc var(--duration) như vậy, nên đổi tốc độ hay màu chỉ cần sửa props ở nơi gọi component.

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.