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

9 css glow effect — nút chữ phát sáng như neon

css glow effect là lớp trang trí tạo vầng sáng lan ra quanh nút hoặc dòng chữ, trông như biển neon nhưng chỉ làm bằng vài dòng code, không cần ảnh hay video.

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

Chín mục không xếp theo độ nổi tiếng mà theo trình tự một vầng sáng thật sự trải qua trên màn hình: chữ cần nổi (01), nút để nhấn (02), thẻ trạng thái đang chờ (03), thẻ cần làm nổi (04), icon thông báo (05), menu điều hướng (06), rồi huy hiệu phải liên tục bắt mắt (07), huy hiệu hòa theo dark mode (08), và thẻ sáng lên khi bạn cuộn vào trang (09).

Số Tên Cái gì phát sáng JS
01 Chữ neon nhấp nháy 5 lớp text-shadow không
02 Nút glow khi hover box-shadow nở bán kính không
03 Viền glow thở vòng box-shadow quanh viền không
04 Đèn rọi trên thẻ tâm radial-gradient theo --x --y 8 dòng
05 Vòng glow quanh icon hai vòng scale lệch nửa nhịp không
06 Gạch chân glow vệt translateX dưới menu không
07 Glow quay màu --hue chạy 0 tới 360 không
08 Glow nhận light-dark() một dòng thay hai nền không
09 Glow đậm theo cuộn --intensity từ IntersectionObserver vài dòng

01Chữ neon nhấp nháy

Trên màn hình trông thế nào

Năm lớp text-shadow chồng lên dòng chữ loe sáng như ống neon thật, và vì bật tắt đều đặn sẽ lộ vẻ giả, đoạn dừng của @keyframes được chia thành 13 mốc để nhấp nháy lệch nhịp.

Chữ neon chớp tắt
Mở trong tab mới

01 · Chữ neon chớp tắt — Diện tích đo được 6.513% · khung hình chuyển động 14/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

text-shadow@keyframes flicker다중 그림자

Mấu chốt nằm ở đâu

.neon {
  text-shadow:
    0 0 $sp-1 $color,
    0 0 $sp-2 $color,
    0 0 $sp-4 rgba($color, .8),
    0 0 $sp-8 rgba($color, .6),
    0 0 $sp-12 rgba($color, .35);
}
.stage.is-demo .neon { animation: flicker $duration $easing infinite; }
@keyframes flicker {
  0%   { opacity: 1; }
  6%   { opacity: .3; }
  14%  { opacity: 1; }
  20%  { opacity: 1; }
  26%  { opacity: .4; }
  34%  { opacity: 1; }
  50%  { opacity: 1; }
  56%  { opacity: .25; }
  64%  { opacity: 1; }
  80%  { opacity: 1; }
  86%  { opacity: .35; }
  94%  { opacity: 1; }
  100% { opacity: 1; }
}

02Nút glow khi hover

Nút nào nên dùng css glow effect on hover

Kiểu css glow effect button kinh điển này để vệt box-shadow mờ nằm sau nút lúc bình thường rất nhạt; khi bạn rê chuột hoặc focus bằng bàn phím thì bán kính và độ sáng nở ra, soi cả vùng không gian quanh nút.

Nút phát sáng khi hover
Mở trong tab mới

02 · Nút phát sáng khi hover — Diện tích đo được 4.303% · khung hình chuyển động 11/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

box-shadow:hover · :focus-visible@keyframes

Điểm cần nhớ

.btn {
  box-shadow: 0 0 8px rgba(0, 229, 255, .3),
              0 0 16px rgba(0, 229, 255, .12);
  transition: box-shadow .3s ease-out, transform .3s ease-out;
}
.btn:hover,
.btn:focus-visible {
  box-shadow: 0 0 12px #00e5ff,
              0 0 32px rgba(0, 229, 255, .65);
  transform: scale(1.06);
}

03Viền glow thở

Đó là trạng thái nào của thẻ

Vòng box-shadow ôm lấy viền thẻ, co dãn bán kính và độ đậm như một nhịp thở, báo rằng thẻ này đang hoạt động ngay lúc này.

Viền glow đập nhịp
Mở trong tab mới

03 · Viền glow đập nhịp — Diện tích đo được 10.727% · 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 펄스border-color@keyframes

Cách làm ra nhịp thở đó

.card { animation: pulse 2s ease-in-out infinite; }

@keyframes pulse {
  0%, 100% {
    box-shadow: 0 0 8px rgba(57, 255, 136, .18);
    border-color: rgba(57, 255, 136, .35);
  }
  50% {
    box-shadow: 0 0 4px rgba(57, 255, 136, .7),
                0 0 32px rgba(57, 255, 136, .55);
    border-color: rgba(57, 255, 136, .9);
  }
}

04Đèn rọi glow trên thẻ

Cái gì đang chuyển động

Tâm của radial-gradient phủ trên thẻ do hai biến --x --y dời chỗ. Vì cặp biến này đăng ký qua @property theo kiểu phần trăm chứ không phải góc, @keyframes nội suy mượt mà; khi không có con trỏ thì chính keyframes kéo hai biến chạy quanh.

Card glow đèn rọi
Mở trong tab mới

04 · Card glow đèn rọi — Diện tích đo được 20.233% · khung hình chuyển động 16/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

@propertyradial-gradient@keyframes sweep

Đọc code như thế nào

Vị trí chuột thật do một đoạn JS 8 dòng ghi vào đúng hai biến đó.

@property --x { syntax: '<percentage>'; inherits: false; initial-value: 50%; }
@property --y { syntax: '<percentage>'; inherits: false; initial-value: 50%; }

.card::before {
  content: '';
  position: absolute; inset: 0;
  background: radial-gradient(circle at var(--x) var(--y),
              rgba(179, 107, 255, .55), transparent 55%);
  animation: sweep 2s ease-in-out infinite;
}
@keyframes sweep {
  0%   { --x: 18%; --y: 20%; }
  25%  { --x: 85%; --y: 22%; }
  50%  { --x: 78%; --y: 80%; }
  75%  { --x: 20%; --y: 76%; }
  100% { --x: 18%; --y: 20%; }
}

05Vòng glow quanh icon

Dùng ở đâu

Hai vòng tròn ôm quanh icon thay nhau phình ra và mờ dần rồi biến mất, đọc như một tín hiệu đang lan tỏa từ trung tâm.

Vòng glow quanh icon
Mở trong tab mới

05 · Vòng glow quanh icon — Diện tích đo được 6.007% · 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

animation-delaybox-shadow 링scale · opacity

Điểm chính của code

Chỉ transform và opacity thay đổi nên đây là mục chạy nhẹ nhất trong cả bài.

.ring {
  position: absolute; inset: 0;
  border-radius: 50%;
  border: 2px solid #ffb020;
  opacity: 0;
  animation: ring 2s ease-out infinite;
}
.ring--b { animation-delay: 1s; }

@keyframes ring {
  0%   { transform: scale(.7); opacity: .8; }
  70%  { transform: scale(1.7); opacity: 0; }
  100% { transform: scale(1.7); opacity: 0; }
}

06Gạch chân glow

Bạn nhìn thấy gì

Gạch chân phết gradient neon trượt đúng dưới mục menu đang được chọn.

Gạch chân phát sáng
Mở trong tab mới

06 · Gạch chân phát sáng — Diện tích đo được 3.618% · 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

translateXbox-shadowlinear-gradient

Nguyên lý chạy thế nào

Vì hai tab cố định bề rộng 96px, quãng đường di chuyển (rộng 96px cộng khoảng hở 16px) tính bằng số là chính xác, không cần đo lại kết quả render.

.tab { flex: 0 0 auto; width: $tab-w; }

.indicator {
  position: absolute; left: 0; bottom: -$sp-2;
  width: $tab-w; height: 3px;
  background: linear-gradient(90deg, transparent, $color, transparent);
  box-shadow: 0 0 $sp-3 $color, 0 0 $sp-6 rgba($color, .6);
}
// chỉ translateX di chuyển — bề rộng là hằng số nên điểm đến cũng là hằng số
.stage.is-demo .indicator { animation: slide $duration $easing infinite; }
@keyframes slide {
  0%, 40%  { transform: translateX(0); }
  60%, 90% { transform: translateX(#{$tab-w + $tab-gap}); } // rộng 96px + hở 16px
  100%     { transform: translateX(0); }
}

07@property quay vòng màu glow

Đó là tình huống gì

Đăng ký --hue qua @property theo kiểu số rồi quay từ 0 tới 360, màu hsl(var(--hue) 90% 60%) chuyển liền mạch không đứt đoạn.

Xoay màu glow bằng @property
Mở trong tab mới

07 · Xoay màu glow bằng @property — Diện tích đo được 4.603% · 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

@propertyhsl(var(--hue))무한 색상 순환

Cách tạo ra nó

Custom property chưa đăng ký thì không nội suy được, nên màu sẽ nhảy bậc thay vì chuyển mượt.

@property --hue { syntax: '<number>'; inherits: false; initial-value: 262; }

.badge {
  box-shadow: 0 0 16px hsl(var(--hue) 90% 62%),
              0 0 40px hsl(var(--hue) 90% 55% / .55);
  animation: cycle 2s linear infinite;
}
@keyframes cycle {
  from { --hue: 0; }
  to   { --hue: 360; }
}

08Glow nhận light-dark()

Điều gì đang diễn ra

Bên trong phần tử đã khai báo color-scheme, viết màu glow bằng light-dark() thì một dòng duy nhất tự chia nhánh — nhạt trên nền sáng, đậm trên nền tối — hoàn toàn không cần media query.

Glow tương thích light-dark()
Mở trong tab mới

08 · Glow tương thích light-dark() — Diện tích đo được 4.275% · 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

light-dark()color-schemebox-shadow

Vì sao chọn cách này

.pane--light { color-scheme: light; }
.pane--dark  { color-scheme: dark; }

.badge {
  --glow: light-dark(rgba(124, 92, 255, .3), #7c5cff);
  box-shadow: 0 0 4px var(--glow);
  animation: breathe 2s ease-in-out infinite;
}
@keyframes breathe {
  0%, 100% { box-shadow: 0 0 4px var(--glow); }
  50%      { box-shadow: 0 0 16px var(--glow); }
}

09Glow đậm dần theo cuộn

Trên màn hình có gì

Khi phần tử tiến lại gần tâm viewport, IntersectionObserver ghi tỷ lệ giao vào biến --intensity và vầng glow theo đó mà đậm thêm.

Cường độ glow theo scroll
Mở trong tab mới

09 · Cường độ glow theo scroll — Diện tích đo được 1.083% · 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

IntersectionObserver(JS)@propertybox-shadow

Cốt lõi khi triển khai

Ở chỗ không có cuộn thật, @keyframes quay thử cùng biến đó để hiệu ứng vẫn trình diễn được.

@property --intensity { syntax: '<number>'; inherits: false; initial-value: 0; }

.card {
  box-shadow: 0 0 calc(var(--intensity) * 12px) #3ec9ff,
              0 0 calc(var(--intensity) * 40px) rgba(62, 201, 255, .55);
  opacity: calc(.45 + var(--intensity) * .55);
  transition: box-shadow .3s linear, opacity .3s linear;
}

Khi nào dễ vỡ — light-dark() chỉ dùng cho màu

Vấn đề nằm ở đâu

Chỗ vấp đầu tiên khi làm hiệu ứng biển neon là mục 08 — mình từng viết cả bán kính mờ của box-shadow thành light-dark(4px, 8px) hòng cho mỗi nền một độ nở. Kết quả là không render nổi: đo thực tế ra cường độ 0.0, trong 24 khung hình không khung nào di chuyển. light-dark() chỉ hợp lệ với giá trị màu (<color>); đặt vào độ dài thì cả khai báo box-shadow đó bị vô hiệu, lặng lẽ biến mất khỏi trang.

Tránh như thế nào

Chỉ đưa màu qua light-dark(), bán kính để nguyên là số thuần. Mật khẩu được hỏi lúc giải nén file zip là azk2eev6, nhập đúng nguyên văn như file nhận được thì mở được. Kèm thêm một điều về chi phí: animation box-shadow là khoản paint tô lại mỗi khung hình, không phải tổng hợp GPU như transform hay opacity, vượt quá ba lớp là máy yếu nhìn ra ngay — toàn bộ demo trong bài gói trong hai lớp.

Khả năng tiếp cận

Còn lại gì khi bật reduced-motion

Cả chín mục đều rơi về animation: none khi gặp prefers-reduced-motion: reduce. Chỉ phần chuyển động bị gỡ bỏ, bản thân vầng glow (màu, bóng) vẫn ở lại, nên người khó chịu với nhấp nháy, pulse hay đổi màu vẫn nhận ra đối tượng đang được nhấn mạnh.

Ngoài ra nên chú ý

Mục 04 đèn rọi và mục 09 glow theo cuộn đứng ở giá trị khởi tạo của @property nên không bao giờ bật lên đột ngột. Nền tảng của quy tắc này nằm trong hướng dẫn reduced-motion trên MDN.

Hỏi đáp (FAQ)

Chỉ dùng CSS có làm được hiệu ứng biển neon không?

Có, bảy trên chín mục là CSS thuần. Riêng mục 04 (đèn rọi) và mục 09 (glow theo cuộn) mượn khoảng 8 dòng JS để đọc tọa độ thật và vị trí cuộn, mà ngay khi thiếu JS, hai mục đó vẫn tự chạy như một vòng lặp. Muốn tìm thêm hiệu ứng CSS-only, vào trang tìm kiếm của site và gõ từ glow.

Vì sao có vầng glow lại bị giật?

Glow làm bằng box-shadow hoặc filter phải tô lại mỗi khung hình, bán kính mờ càng lớn và lớp càng nhiều thì càng nặng máy. Nhóm này đắt hơn animation chỉ chạm transform và opacity, nên giữ số lớp trong hai là an toàn.

@property chạy được trên trình duyệt nào?

Chrome và Edge hỗ trợ từ lâu, Safari bắt đầu từ 16.4. Tài liệu @property trên MDN có bảng hỗ trợ đầy đủ; trên trình duyệt chưa hỗ trợ, ba demo 04, 07, 09 chỉ hiện vầng glow tĩnh không kèm animation — bố cục không vỡ. Nếu bạn mới đến site, trang giới thiệu kể rõ blog này dựng demo theo cách nào.

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.