Bộ nhận diện
Vui Cười Lên
Nguồn chuẩn duy nhất cho màu sắc, font chữ, logo và component của mọi sản phẩm mang thương hiệu VCL. Người thiết kế, lập trình viên, đối tác và AI agent đều đọc chung tài liệu này.
Dùng tài liệu này thế nào
Tài liệu tồn tại ở bốn dạng, cùng một nội dung, khác định dạng để hợp với từng người đọc:
| Dạng | Đường dẫn | Dành cho |
|---|---|---|
| Trang này | /brand | Người xem: designer, team nội dung, khách hàng |
| Token CSS | /css/brand-tokens.css | Lập trình viên: copy vào dự án, dùng var(--token) |
| Token JSON | /data/brand.json | AI agent, script build, Figma plugin |
| Markdown | /docs/brand-guidelines.md | AI agent đọc toàn văn, dán vào context |
brand-tokens.css không chỉ có biến. Ngoài toàn bộ token, file này còn kèm sẵn bốn thứ áp dụng ngay: thanh cuộn gradient hồng, màu bôi đen chữ, chặn trang trượt ngang và khối prefers-reduced-motion. Nạp một file là có đủ, không phải chép lại từng dự án.Bắt đầu một dự án mới trong 30 giây
<!-- 1. Font -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<!-- 2. Icon -->
<script src="https://unpkg.com/lucide@0.460.0"></script>
<!-- 3. Token thương hiệu, nạp TRƯỚC mọi CSS khác -->
<link rel="stylesheet" href="https://vuicuoilen.vn/css/brand-tokens.css">
<link rel="stylesheet" href="css/app.css">
<!-- 4. Favicon -->
<link rel="icon" type="image/png" href="https://raw.githubusercontent.com/trinhleminhan/img-all-site/main/vuicuoilen.com/favicon.png">
Thương hiệu
Vui Cười Lên là công ty truyền thông, marketing và công nghệ tại TP.HCM. Tính cách thương hiệu: vui vẻ, gần gũi, chuyên nghiệp. Mọi lựa chọn thiết kế đều phục vụ ba tính từ đó, hồng ấm thay vì xanh doanh nghiệp, chữ bo tròn thay vì chữ vuông, chuyển động nhún nhảy thay vì trượt phẳng.
| Tên thương hiệu | Vui Cười Lên |
| Viết tắt | VCL |
| Pháp nhân | CÔNG TY TNHH TRUYỀN THÔNG, CÔNG NGHỆ VÀ GIẢI TRÍ, VUI CƯỜI LÊN |
| Mã số thuế | 0319374592 |
| Thành lập | 2026 |
| Website | vuicuoilen.vn |
| hi@vuicuoilen.vn | |
| Hotline | 0981875139 |
| Địa chỉ | E10/211, ấp 9, Xã Tân Nhựt, Bình Chánh, TP. Hồ Chí Minh |
| Tagline | Đồng hành cùng bạn trong kinh doanh |
Cách viết tên
Đúng
- Vui Cười Lên (viết hoa 3 chữ đầu, đủ dấu)
- VCL (viết tắt trong văn nói, tên nội bộ)
vuicuoilen(tên file, slug, biến code)- Tên pháp nhân viết hoa toàn bộ chỉ trong hợp đồng và hoá đơn
Sai
- Vui cười lên (thiếu viết hoa)
- VUI CƯỜI LÊN trong bài marketing
- Vui Cuoi Len (mất dấu, trừ khi bắt buộc kỹ thuật)
- VuiCuoiLen, Vui-Cuoi-Len
Logo
Logo là lockup ba chữ SVG V, C, L xếp ngang căn đáy, cách nhau 2px, kèm wordmark "Vui Cười Lên" bên phải dùng gradient hồng clip vào chữ. Ba chữ luôn đi cùng nhau, đúng thứ tự.
<a href="/">. Người dùng đã quen bấm logo để về trang chủ, một logo không bấm được là lỗi trải nghiệm. Chi tiết ở phần Quy tắc link bên dưới.Rê chuột vào logo để xem hiệu ứng chữ ký "Happy Jump"
Logo trên nền tối và nền hồng phớt
#FFFFFF và bỏ gradient clip.File nguồn
Markup chuẩn
<a href="/" class="logo">
<div class="logo-letters">
<img src=".../V-VCL.svg" alt="V" class="logo-letter">
<img src=".../C-VCL.svg" alt="C" class="logo-letter">
<img src=".../L-VCL.svg" alt="L" class="logo-letter">
</div>
<span class="logo-text">Vui Cười Lên</span>
</a>
<style>
.logo-letters { display:flex; align-items:flex-end; gap:2px; height:45px; }
.logo-letter { height:40px; width:auto; transform-origin:bottom center;
transition: transform .3s var(--ease-bounce); }
.logo-text { font-size:1.5rem; font-weight:800;
background: var(--gradient-primary);
-webkit-background-clip:text; background-clip:text;
-webkit-text-fill-color:transparent; }
</style>
Quy tắc link: logo luôn dẫn về trang chủ
Bất kỳ chỗ nào chèn khối logo để điều hướng, phần tử bao ngoài phải là thẻ <a> trỏ về /. Chỉ hai chỗ được miễn, xem bảng bên dưới.
| Vị trí | Đích đến | Ghi chú |
|---|---|---|
| Header | / | Bắt buộc. Kèm aria-label="Vui Cười Lên, về trang chủ" |
| Footer | / | Bắt buộc, dùng chung markup với header |
| Màn hình loading | không link | Ngoại lệ. Loading tự biến mất, người dùng không kịp bấm |
| Khối trưng bày logo | không link | Ngoại lệ. Là mẫu vật để xem, như ô màu trong bảng màu. Chỉ áp dụng trong chính trang này |
| Trang lỗi 404 | / | Quan trọng nhất, đây là lối thoát duy nhất của người dùng |
| Email, tài liệu PDF | https://vuicuoilen.vn | Dùng URL tuyệt đối, không dùng đường dẫn tương đối |
| Trên chính trang chủ | / | Vẫn giữ link. Bấm vào sẽ tải lại trang, đó là hành vi người dùng mong đợi |
<!-- ĐÚNG: cả khối logo nằm trong một thẻ a -->
<a href="/" class="logo" aria-label="Vui Cười Lên, về trang chủ">
<div class="logo-letters">
<img src=".../V-VCL.svg" alt="" class="logo-letter">
<img src=".../C-VCL.svg" alt="" class="logo-letter">
<img src=".../L-VCL.svg" alt="" class="logo-letter">
</div>
<span class="logo-text">Vui Cười Lên</span>
</a>
<!-- SAI: logo trơ, không bấm được -->
<div class="logo">
<img src=".../vuicuoilen.png" alt="Vui Cười Lên">
</div>
<!-- SAI: mỗi chữ một link, screen reader đọc thành ba link rời -->
<a href="/"><img src=".../V-VCL.svg" alt="V"></a>
<a href="/"><img src=".../C-VCL.svg" alt="C"></a>
<a href="/"><img src=".../L-VCL.svg" alt="L"></a>
- Một link duy nhất bọc cả khối, không tách mỗi chữ một link. Tách ra thì trình đọc màn hình đọc thành ba link riêng lẻ tên "V", "C", "L".
- Ảnh bên trong để
alt=""và đặtaria-labelở thẻa. Ba chữ cái là hình trang trí, tên thương hiệu đã nằm ở wordmark. - Vùng bấm tối thiểu 44px chiều cao. Logo footer chỉ cao 28px nên cần thêm padding dọc cho đủ vùng chạm.
- Con trỏ phải là
pointerkhi rê vào, để người dùng biết bấm được.
Kích thước và khoảng trống bảo vệ
- Chiều cao chữ tối thiểu 28px. Nhỏ hơn thì ba chữ dính vào nhau, không đọc được.
- Chuẩn header: 40px chữ, container 45px. Footer: 28px chữ.
- Khoảng trống xung quanh tối thiểu bằng chiều cao chữ V. Không đặt chữ hay ảnh chen vào vùng này.
Hiệu ứng chữ ký: Happy Jump
Khi hover logo, ba chữ nhún lên lần lượt với độ trễ 0s, 0.08s, 0.16s, dùng easing cubic-bezier(0.34, 1.56, 0.64, 1). Đây là chuyển động đặc trưng của thương hiệu, dùng lại được cho icon feature và card nổi bật.
@keyframes logoJump {
0% { transform: translateY(0) scaleY(1) scaleX(1); }
20% { transform: translateY(0) scaleY(.85) scaleX(1.08); }
40% { transform: translateY(-12px) scaleY(1.1) scaleX(.95); }
60% { transform: translateY(-14px) scaleY(1.05) scaleX(1); }
75% { transform: translateY(-4px) scaleY(1) scaleX(1); }
85% { transform: translateY(0) scaleY(.95) scaleX(1.03); }
100% { transform: translateY(0) scaleY(1) scaleX(1); }
}
.logo:hover .logo-letter:nth-child(1){ animation: logoJump .5s var(--ease-bounce) forwards; }
.logo:hover .logo-letter:nth-child(2){ animation: logoJump .5s var(--ease-bounce) .08s forwards; }
.logo:hover .logo-letter:nth-child(3){ animation: logoJump .5s var(--ease-bounce) .16s forwards; }
Được và không được
Được
- Dùng SVG cho web, PNG cho nơi không hỗ trợ vector
- Đặt trên nền trắng, nền
#FFF0F5hoặc nền tối#2D2D2D - Chỉ dùng ba chữ V C L khi không đủ chỗ cho wordmark
- Phủ lớp tối lên ảnh nền trước khi đặt logo
Không được
- Đổi màu logo sang màu ngoài bộ brand
- Xoay, nghiêng, kéo giãn làm méo tỉ lệ
- Thêm đổ bóng, viền, outline vào logo
- Đặt lên ảnh nền rối mà không có lớp phủ
- Dùng logo dưới 28px chiều cao
- Tách rời hoặc đổi thứ tự ba chữ
Favicon
Favicon là bản rút gọn tối đa của logo. Ở 16px chỉ còn nhận ra khối hồng, đó là chủ ý, không thêm chi tiết để "cho rõ hơn".
<link rel="icon" type="image/png"
href="https://raw.githubusercontent.com/trinhleminhan/img-all-site/main/vuicuoilen.com/favicon.png">
<link rel="apple-touch-icon"
href="https://raw.githubusercontent.com/trinhleminhan/img-all-site/main/vuicuoilen.com/favicon.png">
- File nguồn tối thiểu 512x512px. Bản hiện tại dùng nền tối, giữ nguyên để logo hồng nổi trên thanh tab sáng.
- Khai báo cả hai
rel="icon"vàrel="apple-touch-icon". - Vùng an toàn: chừa 10% viền quanh, iOS bo góc sẽ cắt mất phần rìa.
- Không nhét chữ "Vui Cười Lên" vào favicon, ở 16px không ai đọc được.
Màu sắc
Hồng lấy từ logo là màu chủ đạo, tím là màu bổ trợ. Bấm vào ô màu để copy mã hex.
#FF69B4 đẹp làm nền nhưng chỉ đạt 2.65:1 với nền trắng, không đủ để làm màu chữ. Khi cần chữ hồng, dùng --primary-deep #C2185B (5.87:1). Mỗi ô màu bên dưới đều ghi rõ tỉ lệ tương phản và có dùng làm chữ được hay không.Hồng thương hiệu
Tím bổ trợ
Màu trạng thái
Trung tính
Tỉ lệ dùng màu: 60 / 30 / 10
Hồng là điểm nhấn, không phải nền chính. Một trang phủ hồng toàn bộ là sai chuẩn.
Trạng thái, cách dùng đúng
| Trạng thái | Nền badge | Màu chữ | Icon Lucide |
|---|---|---|---|
| Thành công | rgba(46,204,113,.12) | #1E8449 | check-circle |
| Chờ xử lý | rgba(243,156,18,.12) | #B35C00 | clock |
| Lỗi / huỷ | rgba(231,76,60,.12) | #C0392B | x-circle |
| Thông tin | rgba(52,152,219,.12) | #1F6391 | info |
Gradient
Ba gradient chính thức, lấy cảm hứng từ chuyển sắc trong logo.
linear-gradient(180deg, #FFAEC9 0%, #FF69B4 100%)linear-gradient(135deg, #FF69B4 0%, #FFAEC9 100%)linear-gradient(135deg, #9B59B6 0%, #FF69B4 100%)Font chữ
Nunito là font duy nhất. Bo tròn, thân thiện, hỗ trợ đầy đủ dấu tiếng Việt, khớp với tính cách vui vẻ của thương hiệu. Không thêm font thứ hai vào bất kỳ dự án VCL nào.
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;500;600;700;800&display=swap" rel="stylesheet">
--font-primary: 'Nunito', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
Năm độ đậm
Thang chữ
3rem / 48px
weight 800 · LH 1.2
mobile 2rem
2.5rem / 40px
weight 800 · LH 1.3
mobile 1.75rem
1.5rem / 24px
weight 700 · LH 1.4
mobile 1.25rem
1.15rem / 18px
weight 700 · LH 1.4
1rem / 16px
weight 400 · LH 1.8
0.9rem / 14px
weight 400 · LH 1.7
màu #666666
0.85rem / 13.6px
weight 600 · LS .09em
Quy tắc riêng cho tiếng Việt
- Line-height đoạn văn là 1.8, không phải 1.5. Dấu thanh tiếng Việt chiếm chiều cao, chữ sát nhau sẽ chạm dấu của dòng trên.
- Bật
text-wrap: pretty, tắthyphens. Tiếng Việt không gạch nối từ. - Không viết HOA TOÀN BỘ quá 4 từ. Chữ hoa có dấu rất khó đọc.
- Độ dài dòng 60 đến 75 ký tự. Đặt
max-width: 65chcho khối văn bản dài. - Tiêu đề dùng
text-wrap: balanceđể không bị mồ côi một chữ ở dòng cuối.
p, li { line-height: 1.8; text-wrap: pretty; hyphens: none; }
h1, h2, h3, h4 { text-wrap: balance; }
.prose { max-width: 65ch; }
Layout & spacing
| Token | Giá trị | Ghi chú |
|---|---|---|
--container-max | 1200px | Chiều rộng tối đa của container |
--container-pad | 20px | Padding ngang, giữ nguyên mọi breakpoint |
--section-pad-y | 80px | Padding dọc section, mobile giảm còn 50px |
| grid gap | 30px | Khoảng cách giữa các card |
Thang spacing
Mọi khoảng cách lấy từ thang này, không tự chế số lẻ như 17px hay 23px.
Breakpoints
| Tên | Điều kiện | Thay đổi chính |
|---|---|---|
| Mobile | ≤ 768px | 1 cột, menu thu gọn, section padding 50px, H1 2rem |
| Tablet | 769px - 1024px | 2 cột, nav thu nhỏ font |
| Desktop | ≥ 1025px | Layout đầy đủ, 3 đến 4 cột |
Bo góc, đổ bóng, chuyển động
Bo góc
sm
nút, input
card, ảnh
badge, tag
nút icon
Đổ bóng
Bóng luôn là đen trong suốt. Không dùng bóng màu hồng.
nút, header
card
card hover
modal
Chuyển động
- Mặc định:
transition: all .3s ease - Chữ ký thương hiệu:
cubic-bezier(0.34, 1.56, 0.64, 1), có độ nảy nhẹ, dùng cho hiệu ứng nhún - Hover nút:
translateY(-2px)· Hover card:translateY(-8px) - Thời lượng tối đa 400ms. Dài hơn thì cảm giác chậm chạp.
prefers-reduced-motion. Một số người bị chóng mặt vì animation.@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: .01ms !important;
animation-iteration-count: 1 !important;
transition-duration: .01ms !important;
scroll-behavior: auto !important;
}
}
Thanh cuộn & vùng bôi đen
Hai chi tiết nhỏ mà người dùng nhìn thấy suốt phiên duyệt web. Thanh cuộn mặc định của trình duyệt màu xám, đặt cạnh giao diện hồng ấm trông rất lạc quẻ. VCL thay bằng thanh cuộn gradient hồng chuyển sang tím.
brand-tokens.css và có sẵn.brand-tokens.css đã kèm sẵn thanh cuộn, màu bôi đen, chặn trượt ngang và khối prefers-reduced-motion. Nạp file token là có đủ. Đoạn CSS bên dưới chỉ để bạn biết bên trong nó là gì.Thông số
| Thành phần | Giá trị | Ghi chú |
|---|---|---|
| Bề rộng | 12px | Đủ dày để kéo bằng chuột, không lấn nội dung |
| Rãnh (track) | #FFF0F5 | Hồng phớt, bo góc 10px |
| Con trượt (thumb) | gradient hồng nhạt → hồng → tím | Bo 10px, viền 2px màu rãnh tạo cảm giác nổi |
| Thumb hover | gradient đậm hơn một bậc | Viền đổi sang trắng |
| Thumb active | #E91E8C đặc | Bỏ gradient khi đang kéo |
| Firefox | scrollbar-width: thin | Firefox không hỗ trợ gradient, dùng màu đặc |
| Bôi đen chữ | nền #FFAEC9, chữ #C2185B | Tỉ lệ tương phản 3.4:1, đọc được thoải mái |
Vui Cười Lên đồng hành cùng bạn trong kinh doanh. Bôi đen dòng này để thấy nền hồng nhạt và chữ hồng đậm, đúng chuẩn thương hiệu.
/* ===== Thanh cuộn chuẩn VCL ===== */
/* Chrome, Safari, Edge */
::-webkit-scrollbar {
width: 12px;
height: 12px;
}
::-webkit-scrollbar-track {
background: var(--light-color);
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: linear-gradient(180deg,
var(--primary-light) 0%,
var(--primary-color) 50%,
var(--secondary-color) 100%);
border-radius: 10px;
border: 2px solid var(--light-color); /* viền cùng màu rãnh, tạo cảm giác nổi */
transition: all .3s ease;
}
::-webkit-scrollbar-thumb:hover {
background: linear-gradient(180deg,
var(--primary-color) 0%,
var(--primary-dark) 50%,
var(--secondary-dark) 100%);
border-color: var(--white);
}
::-webkit-scrollbar-thumb:active {
background: var(--primary-dark); /* đang kéo thì bỏ gradient */
}
::-webkit-scrollbar-corner {
background: var(--light-color);
}
/* Firefox: không hỗ trợ gradient, dùng màu đặc */
* {
scrollbar-width: thin;
scrollbar-color: var(--primary-color) var(--light-color);
}
/* ===== Màu bôi đen chữ ===== */
::selection {
background: var(--primary-light);
color: var(--primary-dark);
}
Thanh cuộn ngang trong khối nội dung
Bảng rộng, khối code, lưới thẻ ngang đều phải cuộn bên trong khung của nó, không được đẩy cả trang trượt ngang. Trang bị trượt ngang trên điện thoại là lỗi nghiêm trọng.
/* Bọc bảng rộng trong khối này */
.table-wrap {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
/* Chặn tuyệt đối trang trượt ngang */
body { overflow-x: hidden; }
Nên
- Giữ bề rộng 12px, đủ để kéo bằng chuột
- Bảng rộng bọc trong khối
overflow-x: auto - Trên macOS thanh cuộn tự ẩn, đó là hành vi hệ điều hành, không cố ép hiện
Không nên
- Ẩn hẳn thanh cuộn bằng
::-webkit-scrollbar { display:none }, người dùng mất mốc vị trí - Làm thanh cuộn mảnh dưới 8px, rất khó trúng khi kéo
- Dùng thư viện cuộn giả lập, chậm và hỏng trên bàn phím
- Để trang trượt ngang trên điện thoại
Trạng thái tải
Người dùng chờ mà không thấy gì sẽ tưởng trang hỏng. VCL có ba mức trạng thái tải, chọn theo thời gian chờ dự kiến, không dùng lẫn lộn.
| Mức | Khi nào | Hiển thị | Thời lượng |
|---|---|---|---|
| 1. Màn hình tải | Vào trang lần đầu | Toàn màn hình, ba chữ V C L nhún nhảy trên nền gradient hồng | Tối đa 1.5 giây |
| 2. Khung xương | Gọi dữ liệu về, chưa có nội dung | Khối xám nhấp nháy đúng hình dạng nội dung sắp hiện | Tới khi có dữ liệu |
| 3. Nút đang xử lý | Gửi form, thêm giỏ hàng | Vòng xoay nhỏ trong nút, nút bị khoá | Tới khi có phản hồi |
Mức 1: màn hình tải toàn trang
Chỉ dùng khi vào trang lần đầu. Ba chữ V C L nhún lần lượt trên nền gradient hồng, có bóng đổ dưới chân co giãn theo nhịp nhảy và các đốm sáng bay lên. Đây là phiên bản phóng to của hiệu ứng Happy Jump ở logo.
| Thành phần | Thông số |
|---|---|
| Nền | var(--gradient-primary), phủ kín màn hình, z-index: 9999 |
| Chiều cao chữ | 120px desktop, 80px dưới 480px |
| Nhịp nhảy | happyJump .8s cubic-bezier(.28,.84,.42,1) infinite, độ trễ 0s / 0.15s / 0.3s |
| Bóng dưới chân | Ellipse mờ, co lại còn 50% khi chữ ở đỉnh nhảy |
| Đốm sáng | 5 chấm trắng 8px bay lên rồi mờ dần, chu kỳ 2s |
| Chữ | "Đang tải niềm vui...", trắng, weight 600, nhấp nháy nhẹ 2s |
| Cách tắt | Thêm class .hidden, mờ dần 0.5s rồi biến mất |
<div class="loading-screen">
<div class="loading-logo-container">
<div class="letter-wrapper">
<img src=".../V-VCL.svg" alt="" class="loading-letter letter-v">
<div class="letter-shadow"></div>
</div>
<div class="letter-wrapper">
<img src=".../C-VCL.svg" alt="" class="loading-letter letter-c">
<div class="letter-shadow"></div>
</div>
<div class="letter-wrapper">
<img src=".../L-VCL.svg" alt="" class="loading-letter letter-l">
<div class="letter-shadow"></div>
</div>
<div class="sparkles">
<span class="sparkle"></span><span class="sparkle"></span>
<span class="sparkle"></span><span class="sparkle"></span>
<span class="sparkle"></span>
</div>
</div>
<p class="loading-text">Đang tải niềm vui...</p>
</div>
.loading-screen {
position: fixed; inset: 0;
background: var(--gradient-primary);
display: flex; flex-direction: column;
justify-content: center; align-items: center;
z-index: 9999; overflow: hidden;
transition: opacity .5s ease, visibility .5s ease;
}
.loading-screen.hidden { opacity: 0; visibility: hidden; }
.loading-logo-container {
display: flex; align-items: flex-end; justify-content: center;
gap: 8px; margin-bottom: 40px; position: relative; height: 180px;
}
.letter-wrapper { display: flex; flex-direction: column; align-items: center; }
.loading-letter {
height: 120px; width: auto; transform-origin: bottom center;
filter: drop-shadow(0 8px 25px rgba(255,255,255,.4));
}
.letter-v { animation: happyJump .8s cubic-bezier(.28,.84,.42,1) infinite; }
.letter-c { animation: happyJump .8s cubic-bezier(.28,.84,.42,1) .15s infinite; }
.letter-l { animation: happyJump .8s cubic-bezier(.28,.84,.42,1) .3s infinite; }
@keyframes happyJump {
0%, 100% { transform: translateY(0) scaleY(1) scaleX(1); }
10% { transform: translateY(0) scaleY(.9) scaleX(1.05); }
30% { transform: translateY(-50px) scaleY(1.1) scaleX(.95); }
50% { transform: translateY(-55px) scaleY(1.05) scaleX(1); }
70% { transform: translateY(-20px) scaleY(1) scaleX(1); }
80% { transform: translateY(0) scaleY(.92) scaleX(1.04); }
90% { transform: translateY(-8px) scaleY(1.02) scaleX(.98); }
}
/* Bóng dưới chân, co lại khi chữ lên đỉnh */
.letter-shadow {
width: 60px; height: 12px; margin-top: 8px; border-radius: 50%;
background: radial-gradient(ellipse, rgba(0,0,0,.25) 0%, transparent 70%);
animation: shadowPulse .8s cubic-bezier(.28,.84,.42,1) infinite;
}
.letter-wrapper:nth-child(2) .letter-shadow { animation-delay: .15s; }
.letter-wrapper:nth-child(3) .letter-shadow { animation-delay: .3s; }
@keyframes shadowPulse {
0%, 100% { transform: scaleX(1); opacity: .6; }
30%, 50% { transform: scaleX(.5); opacity: .2; }
80% { transform: scaleX(1.1); opacity: .7; }
}
.loading-text {
color: #fff; font-size: 1.1rem; font-weight: 600;
animation: textPulse 2s ease-in-out infinite;
}
@keyframes textPulse { 0%,100% { opacity:.7 } 50% { opacity:1 } }
@media (max-width: 480px) {
.loading-letter { height: 80px; }
.loading-logo-container { gap: 5px; height: 130px; }
.letter-shadow { width: 40px; height: 8px; }
}
function initLoading() {
const screen = document.querySelector('.loading-screen');
if (!screen) return;
const hide = () => {
screen.classList.add('hidden');
document.body.style.overflow = 'auto';
};
// Tắt ngay khi trang sẵn sàng, KHÔNG chờ hết 1.5 giây
if (document.readyState === 'complete') hide();
else window.addEventListener('load', hide, { once: true });
// Chốt chặn: dù có gì kẹt cũng không giữ quá 1.5 giây
setTimeout(hide, 1500);
}
document.addEventListener('DOMContentLoaded', initLoading);
Mức 2: khung xương
Dùng khi đang gọi dữ liệu về: danh sách dịch vụ, bài viết, đơn hàng. Khung xương phải đúng hình dạng nội dung sắp hiện, nếu không trang sẽ nhảy khi dữ liệu về.
.skeleton {
background: linear-gradient(90deg,
var(--light-color) 25%,
#FFE4EF 50%,
var(--light-color) 75%);
background-size: 200% 100%;
border-radius: 8px;
animation: skelShimmer 1.4s ease-in-out infinite;
}
@keyframes skelShimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
- Dùng tông hồng phớt
#FFF0F5đến#FFE4EF, không dùng xám trung tính, để khung xương vẫn thuộc về thương hiệu. - Số khối bằng số nội dung thật hoặc ít hơn. Hiện 9 khung xương rồi chỉ về 2 kết quả là làm người dùng hụt hẫng.
- Không lồng khung xương quá 3 cấp, càng chi tiết càng dễ lệch với nội dung thật.
Mức 3: nút đang xử lý
Dùng khi gửi form, thêm giỏ hàng, lưu dữ liệu. Nút phải bị khoá ngay lập tức để người dùng không bấm hai lần tạo hai đơn hàng.
.btn-spinner {
width: 16px; height: 16px; border-radius: 50%;
border: 2px solid rgba(255,255,255,.35);
border-top-color: #fff;
animation: btnSpin .7s linear infinite;
}
@keyframes btnSpin { to { transform: rotate(360deg); } }
async function submitForm(btn, payload) {
const original = btn.innerHTML;
btn.disabled = true; // khoá NGAY, chống bấm hai lần
btn.innerHTML = '<span class="btn-spinner"></span>Đang gửi...';
try {
const res = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!res.ok) throw new Error('Gửi thất bại');
showToast('Đã gửi, chúng tôi sẽ liên hệ trong 24 giờ');
} catch (err) {
showToast('Không gửi được, vui lòng thử lại hoặc gọi 0981875139');
} finally {
btn.disabled = false; // LUÔN mở khoá, kể cả khi lỗi
btn.innerHTML = original;
}
}
finally. Khi API lỗi, nút kẹt ở trạng thái "Đang gửi..." vĩnh viễn, người dùng không gửi lại được và bỏ đi.Quy tắc chung cho mọi trạng thái tải
- Dưới 300ms thì đừng hiện gì. Chớp nháy một cái loading rồi tắt ngay còn khó chịu hơn là chờ im lặng.
- Từ 300ms đến 1 giây dùng khung xương hoặc vòng xoay trong nút.
- Trên 3 giây phải nói rõ đang làm gì: "Đang tải danh sách dịch vụ...", không để vòng xoay trống trơn.
- Luôn có đường thoát khi lỗi. Báo lỗi phải kèm cách khắc phục và số hotline.
- Tôn trọng
prefers-reduced-motion: tắt nhún nhảy và hiệu ứng loang, giữ lại chữ trạng thái.
@media (prefers-reduced-motion: reduce) {
.loading-letter, .letter-shadow,
.sparkle, .skeleton, .btn-spinner { animation: none; }
.loading-letter { transform: none; }
}
<!-- Báo cho trình đọc màn hình biết đang tải -->
<div class="loading-screen" role="status" aria-live="polite">
<p class="loading-text">Đang tải niềm vui...</p>
</div>
Nút
Mỗi màn hình chỉ một nút primary. Nút phụ dùng secondary hoặc ghost.
#C2185B · trắng trên nền này đạt 5.87:1
| Loại | Khi nào dùng | Số lượng cho phép |
|---|---|---|
| primary | Hành động chính: Liên hệ ngay, Đặt hàng, Gửi form | 1 mỗi màn hình |
| secondary | Hành động phụ: Xem bảng giá, Tìm hiểu thêm | 1 đến 2 |
| ghost | Hành động nhẹ: Huỷ, Quay lại, Bỏ qua | không giới hạn |
| danger | Xoá, huỷ đơn | chỉ khi thật sự phá huỷ dữ liệu |
CSS đầy đủ
.btn {
display:inline-flex; align-items:center; justify-content:center; gap:8px;
padding:12px 24px; font-size:1rem; font-weight:600;
border-radius: var(--border-radius); transition: var(--transition);
min-height:44px; cursor:pointer; border:none; font-family:var(--font-primary);
}
.btn-primary { background: var(--gradient-primary); color:#fff; box-shadow: var(--shadow-sm); }
.btn-primary:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
/* Biến thể đặc, đạt WCAG AA cho chữ nhỏ */
.btn-solid { background: var(--primary-deep); color:#fff; box-shadow: var(--shadow-sm); }
.btn-solid:hover { background:#AD1457; transform: translateY(-2px); }
.btn-secondary { background:#fff; color: var(--primary-deep); border:2px solid var(--primary-color); }
.btn-secondary:hover{ background: var(--primary-color); color:#fff; }
.btn-ghost { background:transparent; color: var(--text-light); }
.btn-ghost:hover { color: var(--primary-deep); background: var(--light-color); }
.btn-danger { background: var(--danger-color); color:#fff; }
.btn-icon { width:45px; height:45px; border-radius:50%; padding:0; }
.btn:disabled { opacity:.5; cursor:not-allowed; transform:none; box-shadow:none; }
Nhãn nút
Động từ cộng tân ngữ, 2 đến 4 từ. Người dùng phải biết chuyện gì xảy ra sau khi bấm.
Badge & tag
.section-tag {
display:inline-block; background: var(--gradient-primary); color:#fff;
padding:8px 20px; border-radius:20px;
font-size:.85rem; font-weight:600;
text-transform:uppercase; letter-spacing:1px;
}
Card
Rê chuột để xem hiệu ứng nâng và dải gradient chạy ở cạnh trên.
Truyền thông MXH
Quản trị fanpage trọn gói, nội dung và hình ảnh hằng tuần, báo cáo hiệu quả rõ ràng.
Quảng cáo
Chạy Facebook Ads và Google Ads theo mục tiêu doanh số, tối ưu ngân sách mỗi tuần.
Thương mại điện tử
Vận hành gian hàng Shopee và TikTok Shop, từ hình ảnh tới chăm sóc khách hàng.
.card {
background:#fff; border-radius: var(--border-radius-lg); padding:35px;
box-shadow: var(--shadow-sm); transition: var(--transition);
position:relative; overflow:hidden;
}
.card::before {
content:''; position:absolute; top:0; left:0; right:0; height:4px;
background: var(--gradient-primary);
transform: scaleX(0); transform-origin:left; transition: var(--transition);
}
.card:hover { transform: translateY(-8px); box-shadow: var(--shadow-lg); }
.card:hover::before { transform: scaleX(1); }
Spotlight Card: quầng sáng đi theo con trỏ
Đây là hiệu ứng đặc trưng thứ hai của VCL, dùng cho khối "Đội ngũ chuyên gia" và danh sách kênh dịch vụ. Con trỏ đi tới đâu, quầng sáng theo tới đó, và cả cụm card cùng sáng chứ không chỉ card đang bị hover. Rê chuột qua lại giữa ba card bên dưới để thấy.
Đội ngũ Marketing
Thành thạo quảng cáo đa nền tảng, luôn cập nhật chính sách mới nhất và nghiên cứu từ khoá tiềm năng.
Đội ngũ Pháp lý & Kế toán
Chuyên nghiệp, nghiệp vụ vững vàng. Cập nhật sớm nhất các chính sách và quy định nhà nước.
Đội ngũ Hỗ trợ Kinh doanh
Sử dụng thành thạo các phần mềm quản lý bán hàng hàng đầu, giúp bạn vận hành trơn tru.
Rê chuột vào cụm card để xem quầng sáng và viền phát sáng chạy theo con trỏ
Hiệu ứng gồm hai lớp chồng nhau
| Lớp | Vai trò | Bán kính | Độ đậm màu |
|---|---|---|---|
::before | Viền phát sáng. Chỉ hiện một vòng dày 2px quanh mép card | 400px | 0.5 đến 0.6 |
::after | Quầng sáng nền. Vệt màu mờ loang trên mặt card, nằm dưới nội dung | 300px | 0.08 |
Bốn kỹ thuật làm nên hiệu ứng
- 1. Toạ độ con trỏ truyền qua biến CSS. JS ghi
--mouse-xvà--mouse-ylên từng card, CSS đọc vào tâm củaradial-gradient. Giá trị mặc định-999pxđẩy quầng sáng ra ngoài màn hình khi chưa có chuột. - 2. Mẹo mask để chỉ chừa lại viền. Lớp
::beforephủ kín card, nhưngpadding: 2pxcộng hai lớp mask vớimask-composite: excludesẽ khoét rỗng phần ruột, chỉ còn lại vòng viền 2px. Đây là cách tạo viền gradient màborder-imagekhông làm được với bo góc. - 3. Proximity: cả cụm cùng sáng. Điều kiện hiện quầng sáng là
.teams-grid:hover, tức con trỏ ở bất kỳ đâu trong lưới, chứ không phải.team-card:hover. Nhờ vậy card bên cạnh đã bắt đầu sáng dần khi con trỏ mới tới gần, tạo cảm giác cả cụm phản ứng cùng nhau. - 4. Fade vào nhanh, fade ra chậm. Hiện dùng
.15s ease-in, tắt dùng.4s ease-out. Bất đối xứng có chủ đích: vào nhanh cho cảm giác nhạy, ra chậm để không bị giật khi con trỏ lướt qua mép lưới.
opacity và background-position, hai thuộc tính GPU xử lý được, không gây reflow. Không có thuộc tính nào làm trình duyệt tính lại layout khi chuột di chuyển.CSS đầy đủ
.team-card {
background:#fff; border-radius: var(--border-radius-lg); padding:30px;
box-shadow: var(--shadow-sm); position:relative; overflow:hidden;
border:1px solid rgba(255,255,255,.1);
transition: transform .3s ease, box-shadow .3s ease;
/* Tối ưu hiệu năng, xem phần dưới */
contain: layout style paint;
will-change: transform;
transform: translate3d(0,0,0);
}
/* Lớp 1: viền phát sáng, chỉ chừa lại vòng 2px */
.team-card::before {
content:''; position:absolute; inset:0; border-radius:inherit;
padding:2px;
background: radial-gradient(
400px circle at var(--mouse-x, -999px) var(--mouse-y, -999px),
var(--card-glow-color, rgba(255,107,107,.5)),
transparent 40%
);
/* Mẹo mask: phủ kín rồi khoét rỗng ruột, chỉ còn viền */
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
opacity:0; transition: opacity .4s ease-out; pointer-events:none;
}
/* Lớp 2: quầng sáng nền, nằm dưới nội dung */
.team-card::after {
content:''; position:absolute; inset:0; border-radius:inherit;
background: radial-gradient(
300px circle at var(--mouse-x, -999px) var(--mouse-y, -999px),
var(--card-glow-color, rgba(255,107,107,.08)),
transparent 40%
);
opacity:0; transition: opacity .4s ease-out; pointer-events:none;
z-index:-1;
}
/* Proximity: con trỏ ở BẤT KỲ ĐÂU trong lưới thì CẢ CỤM cùng sáng.
Đây là chi tiết quan trọng nhất, đừng đổi thành .team-card:hover */
.teams-grid:hover .team-card::before,
.teams-grid:hover .team-card::after {
opacity:1;
transition: opacity .15s ease-in; /* vào nhanh, ra chậm */
}
.team-card:hover { transform: translateY(-5px); box-shadow: var(--shadow-lg); }
/* Màu quầng sáng riêng cho từng nhóm */
.team-card:has(.team-icon.marketing) { --card-glow-color: rgba(255,107,107,.6); }
.team-card:has(.team-icon.legal) { --card-glow-color: rgba(74,144,164,.6); }
.team-card:has(.team-icon.business) { --card-glow-color: rgba(102,126,234,.6); }
.team-card:has(.team-icon.dev) { --card-glow-color: rgba(17,153,142,.6); }
.team-card:has(.team-icon.hr) { --card-glow-color: rgba(255,105,180,.6); }
.team-card:has(.team-icon.it) { --card-glow-color: rgba(52,152,219,.6); }
JavaScript theo dõi con trỏ
Ba nguyên tắc bắt buộc: một listener trên lưới chứ không phải mỗi card một cái, gộp cập nhật vào một khung hình bằng requestAnimationFrame, và passive: true để trình duyệt không phải chờ xem có preventDefault hay không.
function initCardGlowEffect() {
const grids = document.querySelectorAll('.teams-grid, .channel-grid');
grids.forEach(grid => {
if (grid.dataset.glowInit) return; // chống gắn listener hai lần
grid.dataset.glowInit = 'true';
const cards = Array.from(grid.querySelectorAll('.team-card, .channel-card'));
if (!cards.length) return;
let rafId = null;
let mouseX = 0, mouseY = 0;
// MỘT listener cho cả lưới, không phải mỗi card một cái
grid.addEventListener('mousemove', (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
// Bỏ qua nếu đã có khung hình đang chờ.
// mousemove bắn ~120 lần/giây, màn hình chỉ vẽ 60 lần/giây.
if (rafId) return;
rafId = requestAnimationFrame(() => {
for (let i = 0; i < cards.length; i++) {
const rect = cards[i].getBoundingClientRect();
cards[i].style.setProperty('--mouse-x', (mouseX - rect.left) + 'px');
cards[i].style.setProperty('--mouse-y', (mouseY - rect.top) + 'px');
}
rafId = null;
});
}, { passive: true });
});
}
document.addEventListener('DOMContentLoaded', initCardGlowEffect);
setTimeout(initCardGlowEffect, 1000); // chạy lại cho card render bằng JS
Vì sao mượt, bốn tối ưu hiệu năng
| Kỹ thuật | Tác dụng |
|---|---|
requestAnimationFrame | Sự kiện mousemove bắn khoảng 120 lần mỗi giây, màn hình chỉ vẽ 60 lần. Gộp lại còn đúng một lần cập nhật mỗi khung hình, cắt một nửa công việc thừa. |
{ passive: true } | Báo trước cho trình duyệt là handler không gọi preventDefault, nhờ đó việc cuộn trang không bị chặn chờ. |
contain: layout style paint | Khoá vùng ảnh hưởng của card. Trình duyệt biết chắc thay đổi bên trong card không lan ra ngoài nên không tính lại layout cả trang. |
translate3d(0,0,0) + will-change | Đẩy card lên lớp riêng của GPU, quầng sáng được vẽ bằng card đồ hoạ thay vì CPU. |
will-change tốn bộ nhớ video, chỉ đặt cho card thật sự có hiệu ứng. Đặt will-change lên hàng loạt phần tử sẽ làm chậm ngược lại.Màu quầng sáng đến từ đâu
Biến --card-glow-color quyết định màu quầng sáng của từng card. Có hai cách cấp giá trị cho nó, chọn theo việc card dùng icon hay dùng ảnh.
| Cách | Khi nào dùng | Nguồn màu |
|---|---|---|
| A. Gán cứng | Card dùng icon Lucide trên nền gradient | Bảng gán sẵn theo nhóm, xem bên dưới |
| B. Trích từ ảnh | Card dùng ảnh đại diện: avatar nhân sự, logo đối tác, logo kênh | JS đọc màu chủ đạo của chính tấm ảnh đó |
Cách A: gán cứng theo nhóm, dùng cho card icon
Đây là ngoại lệ có kiểm soát của bộ màu thương hiệu. Sáu màu này chỉ dùng cho icon và quầng sáng của card đội ngũ, để người xem phân biệt nhanh từng nhóm chuyên môn. Không mang đi dùng cho nút, chữ hay nền ở nơi khác.
Cách B: trích màu chủ đạo từ ảnh đại diện
Khi card dùng ảnh thay cho icon, không gán màu cứng nữa. JS đọc màu chủ đạo của chính tấm ảnh rồi ghi vào --card-glow-color. Viền phát sáng nhờ vậy tự khớp với ảnh, thêm thành viên mới hay đối tác mới không phải sửa CSS.
Ba card bên dưới dùng ba logo khác màu hẳn nhau. Rê chuột vào để thấy viền sáng mỗi card một màu, toàn bộ tính ra từ ảnh chứ không có dòng CSS nào gán màu.
Kênh Facebook
Quản trị fanpage, nội dung hằng tuần, quảng cáo theo mục tiêu doanh số.
Kênh Shopee
Vận hành gian hàng, tối ưu hình ảnh sản phẩm, chăm sóc khách hàng.
Kênh YouTube
Sản xuất video, tối ưu tiêu đề và thumbnail, phát triển kênh dài hạn.
Đang đọc màu từ ảnh...
Thuật toán lấy màu chủ đạo
Vẽ ảnh xuống canvas 32x32 rồi gom pixel vào các ô màu. Điểm mấu chốt là lọc bỏ pixel vô nghĩa trước khi đếm. Ảnh đại diện thường có nền trắng và viền tối, nếu đếm tất thì màu chủ đạo luôn ra trắng hoặc xám.
- Bỏ pixel trong suốt (
alpha < 200), logo PNG có nền trong suốt rất nhiều. - Bỏ pixel quá tối và quá sáng, đây là nền và viền, không phải màu thương hiệu.
- Bỏ pixel xám (chênh lệch kênh màu dưới 25), xám không tạo được quầng sáng có ý nghĩa.
- Gom theo ô 16 giá trị (dịch bit
>> 4) để các sắc độ gần nhau tính chung, rồi lấy trung bình thật của ô đông nhất.
/**
* Đọc màu chủ đạo của một ảnh đã tải xong.
* Trả về [r, g, b], hoặc null nếu không đọc được.
* Ảnh BẮT BUỘC có crossorigin="anonymous" và server phải trả
* header Access-Control-Allow-Origin, nếu không canvas sẽ bị "nhiễm bẩn"
* và getImageData ném lỗi bảo mật.
*/
function getDominantColor(img) {
const S = 32; // 32x32 là đủ, nhanh hơn ảnh gốc hàng trăm lần
const canvas = document.createElement('canvas');
canvas.width = canvas.height = S;
const ctx = canvas.getContext('2d', { willReadFrequently: true });
ctx.drawImage(img, 0, 0, S, S);
let data;
try {
data = ctx.getImageData(0, 0, S, S).data;
} catch (e) {
return null; // thiếu CORS, giữ màu mặc định
}
const buckets = new Map();
for (let i = 0; i < data.length; i += 4) {
const r = data[i], g = data[i+1], b = data[i+2], a = data[i+3];
if (a < 200) continue; // bỏ pixel trong suốt
const max = Math.max(r, g, b), min = Math.min(r, g, b);
if (max < 45 || min > 220) continue; // bỏ quá tối và quá sáng
if (max - min < 25) continue; // bỏ pixel xám
// Gom sắc độ gần nhau vào chung một ô
const key = (r >> 4) + '|' + (g >> 4) + '|' + (b >> 4);
const acc = buckets.get(key) || { n: 0, r: 0, g: 0, b: 0 };
acc.n++; acc.r += r; acc.g += g; acc.b += b;
buckets.set(key, acc);
}
let best = null;
buckets.forEach(v => { if (!best || v.n > best.n) best = v; });
if (!best) return null; // ảnh toàn xám hoặc toàn trắng
return [
Math.round(best.r / best.n),
Math.round(best.g / best.n),
Math.round(best.b / best.n)
];
}
/** Gán màu quầng sáng cho card theo ảnh đại diện bên trong nó. */
function applyAvatarGlow(card) {
const img = card.querySelector('.team-avatar');
if (!img) return; // card dùng icon, giữ màu gán cứng
const run = () => {
const rgb = getDominantColor(img);
if (!rgb) return; // đọc lỗi thì giữ màu mặc định
card.style.setProperty('--card-glow-color', `rgba(${rgb.join(',')},.6)`);
};
// Ảnh có thể đã nằm sẵn trong cache
if (img.complete && img.naturalWidth) run();
else img.addEventListener('load', run, { once: true });
}
document.querySelectorAll('.team-card, .channel-card').forEach(applyAvatarGlow);
crossorigin="anonymous" trên thẻ img thì getImageData ném lỗi bảo mật, dù server có gửi header CORS. Hai, ảnh từ CDN ngoài phải trả Access-Control-Allow-Origin, CDN ảnh của VCL đã có sẵn. Ba, luôn giữ giá trị mặc định trong CSS (var(--card-glow-color, rgba(255,107,107,.5))) để card vẫn đẹp khi đọc màu thất bại.Quy chuẩn ảnh đại diện
- Kích thước: 55x55px hiển thị, file nguồn tối thiểu 160x160px cho màn hình retina.
- Bo góc 12px giống ô icon, để card icon và card ảnh đứng chung lưới vẫn đồng bộ.
- Nền: ảnh chân dung dùng nền trắng hoặc
#FFF0F5. Logo đối tác giữ nền trong suốt. - Ảnh chân dung cắt vuông, mặt nằm ở một phần ba trên, dùng
object-fit: cover. - Luôn có
widthvàheighttrên thẻimgđể trang không bị nhảy layout khi ảnh tải xong. - Ảnh quá nhạt (logo trắng, ảnh nền sáng) sẽ không trích được màu, hàm trả
nullvà card rơi về màu hồng mặc định. Đó là hành vi đúng, không cần xử lý thêm.
Khi nào dùng, khi nào không
Dùng cho
- Lưới giới thiệu đội ngũ, năng lực, đối tác
- Danh sách kênh dịch vụ trên trang chi tiết
- Bảng giá gói, nơi cần người xem so sánh cả cụm
- Lưới từ 3 card trở lên, hiệu ứng proximity mới có ý nghĩa
Không dùng cho
- Card đơn lẻ, không có cụm thì không có gì để lan sáng
- Danh sách dài trên 12 card, mỗi lần di chuột phải đọc
getBoundingClientRectcủa tất cả - Bảng dữ liệu, form, khu vực người dùng cần tập trung nhập liệu
- Trang có sẵn nhiều animation khác, thêm nữa thành rối
Hai điều bắt buộc kèm theo
- Thiết bị cảm ứng. Điện thoại không có sự kiện
mousemove, quầng sáng đơn giản là không hiện. Card vẫn phải đọc được đầy đủ khi không có hiệu ứng, đây là lớp trang trí chứ không phải phần mang thông tin. - Tôn trọng
prefers-reduced-motion. Tắt hẳn hai lớp phát sáng cho người đã bật giảm chuyển động trong hệ điều hành.
@media (prefers-reduced-motion: reduce) {
.team-card::before,
.team-card::after { display: none; }
.team-card:hover { transform: none; }
}
Form
Label luôn hiển thị, không dùng placeholder thay label. Khi người dùng gõ vào, placeholder biến mất và họ quên mất ô đó hỏi gì.
.form-group label {
display:block; font-weight:600; color: var(--text-color); margin-bottom:8px;
}
.form-group input, .form-group textarea, .form-group select {
width:100%; padding:15px;
border:2px solid var(--light-color); border-radius: var(--border-radius);
font-family:inherit; font-size:1rem; transition: var(--transition);
}
.form-group input:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(255,105,180,.15);
outline: 2px solid transparent; /* giữ outline cho chế độ tương phản cao */
}
.form-group .error { color:#C0392B; font-size:.9rem; margin-top:6px; }
.form-group input[aria-invalid="true"] { border-color: var(--danger-color); }
- Ô nhập cao tối thiểu 44px, đủ để chạm bằng ngón tay.
- Lỗi hiển thị ngay dưới field, kèm icon
alert-circlevà chữ mô tả cách sửa. - Trường bắt buộc đánh dấu bằng chữ, không chỉ bằng dấu sao đỏ.
- Nút submit ở trạng thái đang gửi phải
disabledvà đổi nhãn thành "Đang gửi...".
Icon & hình ảnh
Icon: Lucide, chỉ dùng dạng nét
Bộ icon duy nhất là Lucide, phiên bản 0.460.0. Chỉ dùng dạng nét, stroke-width: 2, không dùng icon tô đặc.
- Kích thước: 16px inline trong chữ · 18px trong nút · 24 đến 32px cho feature
- Màu: kế thừa
currentColor, hoặcvar(--primary-color)khi làm điểm nhấn - Không trộn nhiều bộ icon trong cùng một trang
- Không dùng emoji thay icon trong UI. Emoji chỉ xuất hiện trong nội dung marketing
Hình ảnh
Nên
- Người thật, ánh sáng ấm, có nụ cười
- Bối cảnh Việt Nam, dễ nhận ra
- Bo góc 20px cho mọi ảnh trong card
- WebP, có
widthvàheight,loading="lazy"trừ ảnh hero - Ảnh nền có chữ đè phải phủ
rgba(45,45,45,.55)
Không nên
- Stock "doanh nhân bắt tay" chung chung
- Ảnh AI có tay lỗi, chữ lỗi
- Tông xám lạnh, không khớp thương hiệu ấm
- Ảnh vuông góc, không bo
- Chữ trắng đè lên ảnh sáng mà không có lớp phủ
OG image: 1200x630px, logo góc trái, chữ tối đa 8 từ, nền dùng --gradient-warm hoặc ảnh có lớp phủ tối.
Giọng nói & cách viết
Tính cách: vui vẻ, gần gũi, chuyên nghiệp, rõ ràng, thực tế. Xưng hô "chúng tôi" với "bạn" trong nội dung marketing, "chúng tôi" với "quý khách" trong hợp đồng, hoá đơn và tài liệu pháp lý.
Nên
- Nói lợi ích trước, tính năng sau
- Dùng số cụ thể: "từ 2 triệu/tháng" thay vì "giá tốt"
- Câu ngắn, thể chủ động
- Kết bằng lời mời hành động rõ ràng
- Emoji tối đa 1 cái mỗi đoạn, chỉ trong marketing
Không nên
- Dùng dấu gạch dài, thay bằng dấu phẩy hoặc hai chấm
- Hứa hẹn tuyệt đối: "cam kết top 1", "đảm bảo x10 doanh thu"
- Từ sáo rỗng: "giải pháp toàn diện", "đột phá", "tiên phong"
- Viết HOA TOÀN BỘ quá 4 từ
- Emoji trong hợp đồng, hoá đơn, chính sách
Ví dụ đối chiếu
| Sai | Đúng |
|---|---|
| "Giải pháp marketing toàn diện, đột phá doanh thu!" | "Quản trị fanpage trọn gói từ 2 triệu/tháng. Có báo cáo hằng tuần." |
| "SUBMIT" | "Gửi yêu cầu tư vấn" |
| "Chúng tôi cam kết đưa bạn lên TOP 1 Google" | "Chúng tôi tối ưu SEO theo bộ từ khoá bạn chọn, báo cáo thứ hạng mỗi tháng." |
| "Liên hệ ngay hôm nay 🔥🔥🔥" | "Liên hệ ngay để nhận tư vấn miễn phí 🎉" |
| "Dịch vụ của chúng tôi được thiết kế nhằm mang lại hiệu quả tối ưu cho doanh nghiệp của quý vị" | "Bạn tập trung bán hàng. Phần fanpage và quảng cáo để chúng tôi lo." |
Accessibility
Mục tiêu WCAG 2.1 mức AA cho mọi sản phẩm VCL. Đây là yêu cầu bắt buộc, không phải tuỳ chọn.
- Tương phản: chữ thường ≥ 4.5:1, chữ ≥ 24px hoặc ≥ 19px bold ≥ 3:1
- Không dùng
#FF69B4hay#FFAEC9làm màu chữ trên nền trắng - Vùng chạm tối thiểu 44x44px
- Focus ring luôn nhìn thấy:
outline: 2px solid var(--primary-deep); outline-offset: 2px - Không truyền tải thông tin chỉ bằng màu, luôn kèm icon hoặc chữ
- Mọi ảnh có
altmô tả, ảnh trang trí dùngalt="" - Thứ tự heading không nhảy cấp, mỗi trang đúng một
h1 - Có khối
prefers-reduced-motion
.btn-primary đang đặt chữ trắng trên gradient hồng #FF69B4, tỉ lệ 2.65:1, chưa đạt AA. Với dự án mới, chọn một trong hai cách: dùng nền var(--primary-deep) #C2185B cho nút có chữ nhỏ, hoặc giữ gradient nhưng ép font-size ≥ 18px; font-weight: 700. Trang này minh hoạ cả hai ở phần Nút.
Bảng tương phản nhanh
| Cặp màu | Tỉ lệ | Kết luận |
|---|---|---|
#333333 trên trắng | 12.63:1 | AAA |
#666666 trên trắng | 5.74:1 | AA |
#C2185B trên trắng | 5.87:1 | AA |
#8E44AD trên trắng | 5.87:1 | AA |
#E91E8C trên trắng | 4.18:1 | Chỉ chữ lớn |
#999999 trên trắng | 2.85:1 | Không đạt |
Trắng trên #FF69B4 | 2.65:1 | Không đạt |
Trắng trên #2D2D2D | 13.77:1 | AAA |
Dành cho AI agent
Phần này viết cho mô hình ngôn ngữ và công cụ sinh code. Nếu bạn là AI agent đang được giao việc liên quan tới thương hiệu Vui Cười Lên, đọc kỹ và tuân thủ.
Chỉ dẫn bắt buộc
Khi tạo bất cứ thứ gì mang thương hiệu Vui Cười Lên: website, landing page, slide, banner, email, tài liệu hoặc app.
- Nạp
brand-tokens.csstrước mọi stylesheet khác. Dùngvar(--token), không hardcode hex. - Không tự sinh màu mới. Cần sắc độ khác thì dùng
rgba()của màu brand có sẵn. - Font duy nhất là
Nunito. Không thêm font thứ hai. - Trước khi đặt chữ lên nền màu, tra trường
text_safetrongbrand.json. - Viết tiếng Việt theo mục Giọng nói: không dùng dấu gạch dài, không hứa hẹn tuyệt đối.
- Mỗi màn hình đúng một nút primary, dùng class
.btn.btn-primary. - Icon dùng Lucide dạng nét, không dùng emoji trong UI.
- Đoạn văn tiếng Việt luôn
line-height: 1.8. - Link nội bộ, canonical và sitemap không có đuôi
.html. - Khối logo điều hướng luôn bọc trong
<a href="/">. Chỉ màn hình loading và khối trưng bày logo được miễn. - Cần nhắc tới dịch vụ, bài viết hay bất kỳ nội dung nào của site thì phải đọc
llms.txttrước. Xem mục bên dưới. - Nếu yêu cầu của người dùng lệch chuẩn này, cứ làm theo người dùng nhưng nói rõ chỗ lệch.
Cần nội dung của site thì đọc llms.txt, đừng đoán
Tài liệu này quy định hình thức: màu, font, component. Nó không chứa nội dung: có bao nhiêu dịch vụ, giá bao nhiêu, bài viết nào đang đăng. Dữ liệu đó thay đổi liên tục và chỉ có một nguồn đúng.
| Bạn cần | Đọc ở đâu |
|---|---|
| Màu, font, component, quy tắc thiết kế | /data/brand.json |
| Danh sách dịch vụ, giá, mô tả | /llms.txt |
| Bài viết blog, chuyên mục, tiêu đề | /llms.txt |
| Cấu trúc URL, biểu mẫu, chính sách | /llms.txt |
| Toàn bộ URL đang có | /sitemap.xml |
llms.txt được sinh động từ dữ liệu thật trong KV, luôn đúng tại thời điểm đọc. Bịa ra một mức giá sai là làm hại khách hàng thật.llms.txt có sẵn những gì
- Giới thiệu công ty và thông tin liên hệ chính thức
- Cấu trúc URL đầy đủ, dùng để đặt link nội bộ cho đúng
- Danh sách dịch vụ kèm mô tả và giá từng gói, lấy trực tiếp từ KV
- Danh sách bài viết blog kèm tiêu đề, chuyên mục và đường dẫn
- Biểu mẫu đang mở cho khách điền
- Chính sách, đội ngũ, thống kê
- Tóm tắt bộ nhận diện và link tới bốn file brand
Quy trình đúng
# 1. Luôn đọc cả hai trước khi bắt tay
curl -s https://vuicuoilen.vn/llms.txt # NỘI DUNG: dịch vụ, bài viết, URL
curl -s https://vuicuoilen.vn/data/brand.json # HÌNH THỨC: màu, font, component
# 2. Cần biết toàn bộ URL đang tồn tại
curl -s https://vuicuoilen.vn/sitemap.xml
# 3. Cần chi tiết một trang cụ thể thì đọc chính trang đó
curl -s https://vuicuoilen.vn/services/social-media
llms.txt, lấy đúng tên gói và giá đang niêm yết, rồi mới dựng trang.Prompt mẫu để dán vào agent khác
Bạn đang làm việc cho thương hiệu Vui Cười Lên (VCL).
BẮT BUỘC đọc trước khi viết bất kỳ dòng code hoặc nội dung nào:
https://vuicuoilen.vn/llms.txt (NỘI DUNG: dịch vụ, giá, bài viết, URL)
https://vuicuoilen.vn/data/brand.json (HÌNH THỨC: token máy đọc)
https://vuicuoilen.vn/docs/brand-guidelines.md (hướng dẫn đầy đủ)
https://vuicuoilen.vn/css/brand-tokens.css (biến CSS)
Ràng buộc về nội dung:
- KHÔNG bịa tên dịch vụ, giá, tiêu đề bài viết hay đường dẫn.
Mọi thứ đó lấy từ llms.txt, được sinh động từ dữ liệu thật.
- Link nội bộ không có đuôi .html, đối chiếu với sitemap.xml.
Ràng buộc về thiết kế:
- Màu: chỉ dùng token trong brand.json. Hồng chủ đạo #FF69B4 chỉ làm nền,
chữ hồng dùng #C2185B.
- Font: Nunito, weight 400/500/600/700/800. Không thêm font khác.
- Đoạn văn tiếng Việt: line-height 1.8.
- Icon: Lucide dạng nét. Không emoji trong UI.
- Mỗi màn hình chỉ một nút primary.
- Khối logo luôn bọc trong <a href="/">.
- Thanh cuộn dùng gradient hồng chuẩn VCL, không để mặc định xám.
- Có trạng thái tải: khung xương khi gọi dữ liệu, spinner trong nút khi gửi form.
- Đạt WCAG 2.1 AA: tương phản chữ >= 4.5:1, vùng chạm >= 44px.
- Có khối prefers-reduced-motion.
Ràng buộc về câu chữ:
- Không dùng dấu gạch dài, không hứa hẹn tuyệt đối.
Checklist trước khi bàn giao
- Đã nạp
brand-tokens.css, không còn hex hardcode ngoài file token - Font Nunito đã load, không có font thứ hai
- Mọi cặp chữ trên nền đạt tương phản ≥ 4.5:1, hoặc ≥ 3:1 với chữ lớn
- Mỗi màn hình đúng một nút primary
- Icon toàn bộ là Lucide dạng nét, không lẫn emoji trong UI
- Logo cao ≥ 28px, có khoảng trống bảo vệ, không bị bóp méo
- Mọi khối logo bọc trong
<a href="/">, bấm được về trang chủ - Thanh cuộn dùng gradient hồng VCL, không để mặc định xám
- Có trạng thái tải: khung xương khi gọi dữ liệu, spinner trong nút khi gửi form
- Nút submit có khối
finallyđể luôn mở khoá kể cả khi API lỗi - Nội dung dịch vụ, giá, bài viết lấy từ
llms.txt, không bịa - Đoạn văn tiếng Việt dùng
line-height: 1.8 - Có khối
prefers-reduced-motion - Vùng chạm ≥ 44px, focus ring nhìn thấy được
- Nội dung không dùng dấu gạch dài, không hứa hẹn tuyệt đối
- Link nội bộ không có đuôi
.html - OG image 1200x630, favicon khai báo đủ hai thẻ
