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.

18 màu chuẩn Nunito, 5 weight Token CSS + JSON WCAG 2.1 AA
Bắt đầu

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ẫnDành cho
Trang này/brandNgười xem: designer, team nội dung, khách hàng
Token CSS/css/brand-tokens.cssLập trình viên: copy vào dự án, dùng var(--token)
Token JSON/data/brand.jsonAI agent, script build, Figma plugin
Markdown/docs/brand-guidelines.mdAI agent đọc toàn văn, dán vào context
Quy tắc vàng: mọi màu, font, khoảng cách trong dự án VCL phải lấy từ file token. Nếu bạn phải gõ một mã hex bằng tay, khả năng cao là đang làm sai chuẩn.
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">
Nền tảng

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ệuVui Cười Lên
Viết tắtVCL
Pháp nhânCÔ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ập2026
Websitevuicuoilen.vn
Emailhi@vuicuoilen.vn
Hotline0981875139
Đị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
Nhận diện

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".

Favicon 16px16px · tab
Favicon 32px32px · tab retina
Favicon 64px64px · bookmark
Favicon 180px180px · apple-touch
<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"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.
Hệ thống thiết kế

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.

Đọc trước khi dùng màu làm chữ. Hồng #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.

60% nền trung tính
30% chữ & cấu trúc
10% hồng

Trạng thái, cách dùng đúng

Trạng tháiNền badgeMàu chữIcon Lucide
Thành côngrgba(46,204,113,.12)#1E8449check-circle
Chờ xử lýrgba(243,156,18,.12)#B35C00clock
Lỗi / huỷrgba(231,76,60,.12)#C0392Bx-circle
Thông tinrgba(52,152,219,.12)#1F6391info
Không bao giờ truyền tải trạng thái chỉ bằng màu. Luôn kèm icon hoặc chữ, vì khoảng 8% nam giới bị mù màu đỏ lục.
Hệ thống thiết kế

Gradient

Ba gradient chính thức, lấy cảm hứng từ chuyển sắc trong logo.

Primary
--gradient-primarylinear-gradient(180deg, #FFAEC9 0%, #FF69B4 100%)
Mặc định: nút primary, badge, section tag
Warm
--gradient-warmlinear-gradient(135deg, #FF69B4 0%, #FFAEC9 100%)
Khối lớn: hero, banner, nền section
Purple
--gradient-purplelinear-gradient(135deg, #9B59B6 0%, #FF69B4 100%)
Nhấn đặc biệt, dùng tiết chế
Chữ trên gradient hồng phải là trắng, weight ≥ 600, size ≥ 16px. Chữ nhỏ hoặc chữ mảnh đặt lên gradient sẽ không đọc được ở đầu sáng.
Hệ thống thiết kế

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

Vui Cười Lên
400 Regular · body
Vui Cười Lên
500 Medium · chữ phụ
Vui Cười Lên
600 SemiBold · nút, nav
Vui Cười Lên
700 Bold · H3, H4
Vui Cười Lên
800 ExtraBold · H1, H2

Thang chữ

Đồng hành cùng bạn
H1
3rem / 48px
weight 800 · LH 1.2
mobile 2rem
Dịch vụ nổi bật
H2
2.5rem / 40px
weight 800 · LH 1.3
mobile 1.75rem
Quản trị fanpage
H3
1.5rem / 24px
weight 700 · LH 1.4
mobile 1.25rem
Gói cơ bản
H4
1.15rem / 18px
weight 700 · LH 1.4
Chúng tôi nhận quản trị fanpage trọn gói từ 2 triệu mỗi tháng, có báo cáo hằng tuần và cam kết thời gian phản hồi trong 4 giờ làm việc.
Body
1rem / 16px
weight 400 · LH 1.8
Giá chưa bao gồm chi phí quảng cáo trả cho nền tảng.
Small
0.9rem / 14px
weight 400 · LH 1.7
màu #666666
Dịch vụ
Caption / tag
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ắt hyphens. 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: 65ch cho 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; }
Sai · line-height 1.4
Chúng tôi nhận quản trị fanpage trọn gói, đầy đủ nội dung, hình ảnh và báo cáo hiệu quả mỗi tuần cho khách hàng.
Đúng · line-height 1.8
Chúng tôi nhận quản trị fanpage trọn gói, đầy đủ nội dung, hình ảnh và báo cáo hiệu quả mỗi tuần cho khách hàng.
Hệ thống thiết kế

Layout & spacing

TokenGiá trịGhi chú
--container-max1200pxChiều rộng tối đa của container
--container-pad20pxPadding ngang, giữ nguyên mọi breakpoint
--section-pad-y80pxPadding dọc section, mobile giảm còn 50px
grid gap30pxKhoả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ệnThay đổi chính
Mobile≤ 768px1 cột, menu thu gọn, section padding 50px, H1 2rem
Tablet769px - 1024px2 cột, nav thu nhỏ font
Desktop≥ 1025pxLayout đầy đủ, 3 đến 4 cột
Hệ thống thiết kế

Bo góc, đổ bóng, chuyển động

Bo góc

8px
sm
12px
nút, input
20px
card, ảnh
999px
badge, tag
50%
nút icon

Đổ bóng

Bóng luôn là đen trong suốt. Không dùng bóng màu hồng.

shadow-sm
nút, header
shadow-md
card
shadow-lg
card hover
shadow-xl
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.
Mọi dự án bắt buộc có khối 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;
  }
}
Hệ thống thiết kế

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.

Bạn đang nhìn thấy nó ngay lúc này. Thanh cuộn bên phải trang này chính là thanh cuộn chuẩn VCL. Trang này không tự viết CSS thanh cuộn, nó chỉ nạp brand-tokens.css và có sẵn.
Không cần chép đoạn CSS bên dưới. Từ phiên bản này, 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ầnGiá trịGhi chú
Bề rộng12pxĐủ dày để kéo bằng chuột, không lấn nội dung
Rãnh (track)#FFF0F5Hồng phớt, bo góc 10px
Con trượt (thumb)gradient hồng nhạt → hồng → tímBo 10px, viền 2px màu rãnh tạo cảm giác nổi
Thumb hovergradient đậm hơn một bậcViền đổi sang trắng
Thumb active#E91E8C đặcBỏ gradient khi đang kéo
Firefoxscrollbar-width: thinFirefox không hỗ trợ gradient, dùng màu đặc
Bôi đen chữnền #FFAEC9, chữ #C2185BTỉ lệ tương phản 3.4:1, đọc được thoải mái
Thử bôi đen đoạn chữ này để xem màu selection

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
Hệ thống thiết kế

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ứcKhi nàoHiển thịThời lượng
1. Màn hình tảiVào trang lần đầuToàn màn hình, ba chữ V C L nhún nhảy trên nền gradient hồngTối đa 1.5 giây
2. Khung xươngGọi dữ liệu về, chưa có nội dungKhối xám nhấp nháy đúng hình dạng nội dung sắp hiệnTới khi có dữ liệu
3. Nút đang xử lýGửi form, thêm giỏ hàngVò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.

Đang tải niềm vui...

Thành phầnThông số
Nềnvar(--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ảyhappyJump .8s cubic-bezier(.28,.84,.42,1) infinite, độ trễ 0s / 0.15s / 0.3s
Bóng dưới chânEllipse mờ, co lại còn 50% khi chữ ở đỉnh nhảy
Đốm sáng5 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ắtThêm class .hidden, mờ dần 0.5s rồi biến mất
Trần 1.5 giây, không hơn. Màn hình tải là để che khoảnh khắc trang chưa vẽ xong, không phải để khoe animation. Bắt người dùng ngồi xem logo nhảy 3 giây là làm chậm trang một cách cố ý. Nếu trang tải xong sớm hơn, tắt ngay chứ đừng chờ hết thời gian.
<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ề.

Khung xương cho card dịch 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.

Bấm thử để xem trạng thái xử lý
.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;
  }
}
Lỗi hay gặp nhất: quên khối 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>
Component

Nút

Mỗi màn hình chỉ một nút primary. Nút phụ dùng secondary hoặc ghost.

Trạng thái mặc định · rê chuột để xem hover
Biến thể đặc, dùng khi chữ nhỏ hoặc cần đạt tương phản AA
nền #C2185B · trắng trên nền này đạt 5.87:1
LoạiKhi nào dùngSố lượng cho phép
primaryHành động chính: Liên hệ ngay, Đặt hàng, Gửi form1 mỗi màn hình
secondaryHành động phụ: Xem bảng giá, Tìm hiểu thêm1 đến 2
ghostHành động nhẹ: Huỷ, Quay lại, Bỏ quakhông giới hạn
dangerXoá, huỷ đơnchỉ 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.

Sai
"SUBMIT" · "Click here" · "OK" · "Gửi"
Đúng
"Gửi yêu cầu tư vấn" · "Nhận báo giá" · "Đặt lịch tư vấn" · "Tải hồ sơ năng lực"
Component

Badge & tag

Badge trạng thái · luôn có icon kèm màu
Đã thanh toán Chờ xử lý Đã huỷ Bản nháp Mới
Section tag · nhãn đặt phía trên tiêu đề section
Dịch vụ Bảng giá
.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;
}
Component

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.

Google AdsFacebook Ads TikTok BusinessShopee Seller
Độ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.

Tư vấn pháp lýKế toán thuế Thành lập DNGiấy phép
Độ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.

HaravanSapo KiotVietTpos

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ớpVai tròBán kínhĐộ đậm màu
::beforeViền phát sáng. Chỉ hiện một vòng dày 2px quanh mép card400px0.5 đến 0.6
::afterQuầng sáng nền. Vệt màu mờ loang trên mặt card, nằm dưới nội dung300px0.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-x--mouse-y lên từng card, CSS đọc vào tâm của radial-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 ::before phủ kín card, nhưng padding: 2px cộng hai lớp mask với mask-composite: exclude sẽ 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-image khô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.
Vì sao mượt. Toàn bộ chuyển động nằm ở opacitybackground-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ậtTác dụng
requestAnimationFrameSự 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 paintKhoá 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áchKhi nào dùngNguồn màu
A. Gán cứngCard dùng icon Lucide trên nền gradientBảng gán sẵn theo nhóm, xem bên dưới
B. Trích từ ảnhCard dùng ảnh đại diện: avatar nhân sự, logo đối tác, logo kênhJS đọ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.

Marketingrgba(255,107,107,.6)
Pháp lý & Kế toánrgba(74,144,164,.6)
Hỗ trợ Kinh doanhrgba(102,126,234,.6)
Lập trìnhrgba(17,153,142,.6)
Nhân sựrgba(255,105,180,.6)
IT Helpdeskrgba(52,152,219,.6)

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.

Facebook
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ố.

FanpageFacebook Ads
Shopee
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.

Gian hàngShopee Ads
YouTube
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.

VideoSEO kênh

Đ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);
Ba điều dễ sai. Một, thiếu 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ó widthheight trê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ả null và 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 getBoundingClientRect củ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; }
}
Component

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ì.

Số điện thoại cần đủ 10 chữ số
.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-circle và 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 disabled và đổi nhãn thành "Đang gửi...".
Component

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.

phone
mail
megaphone
trending-up
shopping-cart
check-circle
clock
heart
target
sparkles
  • Kích thước: 16px inline trong chữ · 18px trong nút · 24 đến 32px cho feature
  • Màu: kế thừa currentColor, hoặc var(--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ó widthheight, 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.

Nội dung

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."
Chuẩn kỹ thuật

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 #FF69B4 hay #FFAEC9 là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ó alt mô tả, ảnh trang trí dùng alt=""
  • Thứ tự heading không nhảy cấp, mỗi trang đúng một h1
  • Có khối prefers-reduced-motion
Điểm cần lưu ý trên vuicuoilen.vn hiện tại: .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àuTỉ lệKết luận
#333333 trên trắng12.63:1AAA
#666666 trên trắng5.74:1AA
#C2185B trên trắng5.87:1AA
#8E44AD trên trắng5.87:1AA
#E91E8C trên trắng4.18:1Chỉ chữ lớn
#999999 trên trắng2.85:1Không đạt
Trắng trên #FF69B42.65:1Không đạt
Trắng trên #2D2D2D13.77:1AAA
Tự động hoá

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.

  1. Nạp brand-tokens.css trước mọi stylesheet khác. Dùng var(--token), không hardcode hex.
  2. 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.
  3. Font duy nhất là Nunito. Không thêm font thứ hai.
  4. Trước khi đặt chữ lên nền màu, tra trường text_safe trong brand.json.
  5. 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.
  6. Mỗi màn hình đúng một nút primary, dùng class .btn.btn-primary.
  7. Icon dùng Lucide dạng nét, không dùng emoji trong UI.
  8. Đoạn văn tiếng Việt luôn line-height: 1.8.
  9. Link nội bộ, canonical và sitemap không có đuôi .html.
  10. 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.
  11. 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.txt trước. Xem mục bên dưới.
  12. 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
Không được bịa nội dung site. Không tự nghĩ ra tên dịch vụ, không tự đặt giá, không tự chế tiêu đề bài viết, không tự tạo đường dẫn. 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
Sai
Viết landing page rồi tự đặt "Gói Premium 5.000.000đ/tháng" vì nghe hợp lý, trong khi bảng giá thật không có gói đó.
Đúng
Đọc 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.
Bàn giao

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ẻ
Đã copy