Skip to content

UI style

Harry Nguyen edited this page Oct 17, 2026 · 6 revisions

UI style

Trang này ghi lại style của Zhesen: màu, chữ, khoảng cách, animation và các luật owner đã chốt. Mọi trang mới hoặc trang được làm lại theo style này. Người sửa UI bằng Claude Code đọc trang này trước. Việc đưa các trang còn lại sang style này nằm ở issue #87.

Các trang đã dùng style này:

Trang Issue
Header, footer, trang / (landing page và homepage khi đã đăng nhập) #86
Trang Dịch /dictionary và trang chi tiết từ /dictionary/<lang>/<id> #87
Sổ tay /wordlist #87
Khu admin /admin và /rescue #87
Lý thuyết /theory và mọi trang con #87
Luyện tập /practice và 6 chế độ luyện #87
/account, /login, /register, trang 404 và trang lỗi #87

Mọi trang của Zhesen đã dùng style này. Tính năng mới cũng dùng style này, không cần owner duyệt lại giao diện.

Luật owner đã chốt

Luật Nguồn
Toàn bộ Zhesen dùng trắng, đen và một dải xanh 8 bậc Owner, 2026-09-29
Không dùng #03045a: "hơi đậm, đừng dùng nữa nhé, dùng các màu sáng sáng, nhẹ nhẹ pastel một tí" Owner, 2026-09-29
Nền các section sáng, pastel. Ngoại lệ duy nhất là 3 cột ngôn ngữ khi quả cầu mở phẳng, màu đậm của chúng đã được owner duyệt cùng quả cầu Owner, 2026-09-29
Trang cho người học không ghi số entry, licence hay cách đăng nhập Owner
Không có khối độn: dải kêu gọi cuối trang, câu trấn an, câu liệt kê tính năng, tiêu đề lặp lại nội dung Owner
Không viết cứng chi tiết dễ đổi khi Zhesen phát triển, như tên kỳ thi hay số lượng mục Owner, 2026-09-29
Mọi tính năng mới theo style này: "sau này có dev feature/function mới thì cũng cứ áp dụng theo design style có sẵn là được" Owner, 2026-09-30
Dùng đa dạng dải xanh, không chỉ 2 màu: "palette có khá nhiều màu xanh mà, sử dụng đa dạng lên". Hai đoạn chữ khác vai trò đặt cạnh nhau, như câu ví dụ và câu dịch, phải phân biệt được ngay Owner, 2026-09-30
Copy tiếng Việt theo .claude/rules/copy.md: không "bạn", không "Hãy", dùng từ trong glossary Owner, issue #68
Không placeholder bịa như ban@vidu.com, không câu dịch word-for-word từ tiếng Anh như "phiên ôn", "lượt giữ chuỗi" Owner, 2026-10-13
Logo là chữ Z gấp từ dải băng, xem mục Logo Owner, 2026-10-17
Logo đứng thay chữ Z trong tên, chữ "hesen" chuyển màu qua 3 màu logo, header chỉ hiện logo, icon tab không nền Owner, 2026-10-18

Màu

Token nằm trong app/globals.css. Component dùng token, không viết mã màu trực tiếp.

Token Sáng Dùng cho
--sea-50 #c3e7ef Nền chip, nền nhạt
--sea-100 #a7dfea Viền, nền thẻ
--sea-200 #8bd6e8 Nền thẻ, cột biểu đồ
--sea-300 #45c2db Màu tiếng Tây Ban Nha
--sea-400 #0cafd2 Điểm nhấn
--sea-500 #0090bf Chữ lớn, icon
--sea-600 #0172b6 Nút chính, màu tiếng Trung
--sea-700 #023c85 Chữ tiêu đề, màu tiếng Anh

Dải --sea-* giữ nguyên ở theme tối. Thứ gì phải đổi theo theme thì dùng token ngữ nghĩa, mỗi token có giá trị tối riêng trong khối :root[data-theme="dark"]:

Nhóm Token Vai trò
Khung trang --zs-bg, --zs-ink, --zs-soft, --zs-line, --zs-field, --zs-pen, --zs-chip Nền, chữ, chữ phụ, đường kẻ, ô nhập, link, chip
Nút chính --zs-btn, --zs-btn-ink, --zs-btn-hover Sáng: chữ trắng trên #0172b6, hover #023c85. Tối: nút trắng chữ xanh
Dấu tô --zs-mark, --zs-mark-ink Nền và chữ của phần được tô, như dạng từ bất quy tắc trên trang chi tiết từ
Nền section --tint-1, --tint-2, --tint-3, --edge, --lift 3 mức nền pastel, viền thẻ, bóng đổ
Ngôn ngữ --l-en, --l-zh, --l-es và bản nhạt --l-*-t Mỗi thứ tiếng một màu, dùng chung cho quả cầu và homepage
Quả cầu --g-* Đại dương, đất liền, viền nước, Việt Nam, bóng đêm

Tailwind có sẵn class bg-sea-50 tới bg-sea-700. Lưu ý là class bg-white và text-white của repo này đổi thành màu tối ở theme tối, nên chỗ cần trắng thật thì dùng token --zs-*.

Bảng màu không có đỏ, nên trạng thái lỗi và thao tác xoá được đánh dấu bằng hình dạng:

Trạng thái Cách vẽ Mẫu
Lỗi Chữ màu ink kèm icon cảnh báo aria-hidden, giữ nguyên role="alert" nếu có components/search/ErrorLine.tsx, components/lookup/FeedbackButton.tsx
Nút xoá Viền 1.5 px màu ink, nền trong suốt, tô đặc khi hover. Nút chính vẫn tô đặc màu xanh components/wordlist/Wordlist.module.css, class .danger
Link xoá trong dòng Chữ màu ink có gạch chân, khác hẳn các link bên cạnh ngay cả khi chưa hover components/wordlist/Wordlist.module.css, .act[data-danger]

Hai màu dùng để phân biệt hai thứ, như chấm chú thích hay trạng thái bật tắt, phải khác nhau ở cả hai theme. Ở theme tối --zs-pen là --sea-300, nên một cặp --zs-pen và sea-300 khác nhau ở theme sáng sẽ trùng nhau ở theme tối.

Không làm mờ chữ bằng hậu tố độ mờ trên token, như text-(--zs-pen)/60. Test test/text-contrast.test.ts chỉ bắt text-black/<n>, nên chữ mờ kiểu này lọt qua test. Dùng một token đặc đạt 4.5:1 ở cả hai theme.

Trong một nhóm lựa chọn như LayoutPicker, bộ chọn chiều tra hay nút bật tắt ngôn ngữ, mục đang chọn tô đặc bằng --zs-btn với chữ --zs-btn-ink. Nền trắng trên nền pastel không đủ 3:1 để phân biệt mục đang chọn.

Rule :focus-visible chỉ thêm outline, không đổi border-radius, để nút pill giữ hình khi được focus bằng bàn phím.

Chữ nhỏ trên nền #0cafd2 hoặc #0090bf phải màu đen. Chữ #023c85 trên hai nền này dưới mức tương phản 4.5:1. Biến --ink của repo vẫn là đen, vì chữ độ mờ 55% bằng #023c85 chỉ còn khoảng 3.2:1 và test tương phản test/text-contrast.test.ts sẽ fail.

Logo

Logo là chữ Z gấp từ một dải băng, nhạt ở dải trên và đậm dần xuống dải dưới. Owner chọn ngày 2026-10-17 từ các phác thảo tạo bằng AI, sau đó phác thảo được vẽ lại thành vector.

Logo Zhesen

Phần Theme sáng Theme tối
Dải trên --sea-300 #45c2db --sea-200 #8bd6e8
Dải chéo --sea-500 #0090bf --sea-400 #0cafd2
Dải dưới --sea-700 #023c85 --sea-600 #0172b6
Chữ "hesen" Chuyển màu từ trái sang phải qua 3 màu trên Như theme sáng, dùng 3 màu theme tối

Hai tam giác bóng màu đen, độ mờ 16% và 26%, cho thấy dải chéo luồn dưới dải trên rồi đè lên dải dưới. Trên web, màu ba dải là token --zs-logo-1 tới --zs-logo-3, component vẽ logo là ZMark (components/layout/ZMark.tsx), và component vẽ cả tên là BrandMark (components/layout/SiteHeader.tsx).

Luật dùng:

  • Logo đứng thay chữ Z trong tên. Phần còn lại "hesen" dùng Be Vietnam Pro 800, letter-spacing -0.02em. Logo cao bằng chiều cao chữ hoa và đứng trên baseline.
  • Header chỉ hiện logo. Khi rê chuột hoặc focus bằng bàn phím vào logo, chữ "hesen" hiện ra bên phải. Footer và ảnh chia sẻ luôn hiện đủ tên.
  • Icon trên tab trình duyệt là logo không nền, đổi sang 3 màu theme tối khi trình duyệt dùng theme tối. Icon app, icon PWA và avatar giữ ô nền trắng, vì launcher tô góc trong suốt thành màu đen.
  • Khoảng trống quanh logo ít nhất bằng bề dày một dải, tức 30% chiều cao logo.
  • Cỡ nhỏ nhất là 16 px. Ở cỡ đó favicon bỏ hai tam giác bóng.
  • Không đổi màu ra ngoài dải xanh, không xoay, không kéo méo, không thêm viền hay bóng đổ.
  • Trên nền tối hoặc nền ảnh thì dùng bản tối. In một màu thì dùng bản mono.

Bộ file nằm ở public/brand/, tải tại https://zhesen.net/brand/<tên file>:

File Dùng cho
zhesen-logo.svg, zhesen-logo.png Cả tên, logo thay chữ Z, nền sáng
zhesen-logo-dark.svg, zhesen-logo-dark.png Cả tên, logo thay chữ Z, nền tối
zhesen-mark.svg, zhesen-mark.png Chỉ hình chữ Z, nền sáng
zhesen-mark-dark.svg Chỉ hình chữ Z, nền tối
zhesen-mark-mono.svg Một màu --sea-700, cho bản in một màu
zhesen-icon.png Ô vuông nền trắng 1024 px, cho avatar mạng xã hội

Trên site, logo nằm ở app/icon.svg, app/favicon.ico (16, 32 và 48 px), app/apple-icon.png (180 px) và 3 icon PWA trong public/. Icon maskable giữ logo trong vòng tròn an toàn 80% của Android. Mọi trang có ảnh chia sẻ chung app/opengraph-image.tsx, riêng trang chi tiết từ có ảnh chia sẻ của từng từ.

Script scripts/brand-build.js tạo lại mọi file trên từ một bộ toạ độ. Chạy ở gốc repo: npm i --no-save opentype.js@1.3.4, rồi node <wiki>/scripts/brand-build.js.

Chữ

Font Dùng cho Khai báo
Be Vietnam Pro 400, 600, 700, 800 Chữ giao diện của mọi trang đã làm lại app/layout.tsx, class font-ui
Newsreader Từ tiếng Anh và tiếng Tây Ban Nha. Tiếng Tây Ban Nha in nghiêng components/home/fonts.ts cho / và /wordlist; components/lookup/fonts.ts cho mọi trang dưới /dictionary, gắn ở app/dictionary/layout.tsx
Serif có sẵn của máy Chữ Hán Không tải font, vì Noto Serif SC không có subset tiếng Trung để preload
Geist Phần thân các trang chưa làm lại app/layout.tsx

Cỡ chữ theo tỉ lệ 1.333: 12, 16, 21, 28, 37.5, 50, 66.6 px. Tiêu đề đậm 800, letter-spacing âm từ -0.02em tới -0.04em. Câu dài không được xuống dòng giữa cụm như "tiếng Anh," hay "tiếng Tây Ban Nha": bọc cụm đó trong span nowrap.

Bố cục

Mọi trang và header dùng chung một độ rộng --container-page và class mx-auto max-w-page px-6, để mép trái phải thẳng hàng. Ở 390 px lề hai bên là 16 tới 24 px và không có thanh cuộn ngang. Chiều cao header nằm ở biến --header-h: 64 px trên màn rộng, 96 px dưới 640 px. Anchor #id phải hiện dưới header.

Header gồm logo, 4 link Dịch, Lý thuyết, Luyện tập, Sổ tay và góc tài khoản. Góc tài khoản chỉ hiện chữ cái đầu trong vòng tròn, email nằm ở tooltip và trang /account. Footer gồm logo, nhóm Học, nhóm Tài khoản và dòng ©. Cả hai là components/layout/SiteHeader.tsx và components/layout/SiteFooter.tsx.

Trang có nhiều bố cục thì dùng chung component LayoutPicker (components/ui/LayoutPicker.tsx), như trang chi tiết từ và homepage. Lựa chọn nhớ theo trình duyệt. Script chạy trước khi vẽ trang đặt lựa chọn lên thẻ <html>, nên trang không nháy sang bố cục khác lúc tải.

Animation

Easing nằm trong app/globals.css, dùng được như class Tailwind:

Token Đường cong Dùng cho
--ease-enter cubic-bezier(0.05, 0.7, 0.1, 1) Phần tử hiện vào
--ease-std cubic-bezier(0.2, 0, 0, 1) Đổi trạng thái, hover
--ease-spring linear(...) hơi nảy Nút, thẻ, mũi tên

Luật chuyển động:

  • Mỗi animation có bản thay thế khi người dùng bật giảm chuyển động (prefers-reduced-motion). Khi đó trang hiện trạng thái cuối, không bỏ nội dung. Hook đọc thiết lập này là useReducedMotion (lib/hooks/useReducedMotion.ts).
  • Thứ gì chuyển động quá 5 giây, như quả cầu quay, phải có nút dừng (WCAG 2.2.2).
  • Khối nội dung hiện dần một lần khi cuộn tới, lệch nhau theo thứ tự, bằng component Reveal (components/home/Reveal.tsx) và attribute data-reveal.
  • Số đếm lên, cột biểu đồ mọc lên và thẻ lật chỉ chạy một lần và dưới 5 giây.
  • Canvas dừng vẽ khi ra khỏi màn hình hoặc khi tab bị ẩn.
  • Hover đổi màu trong 150 ms với --ease-std. Dịch chuyển và phóng to dùng --ease-spring, từ 160 tới 520 ms tùy độ lớn của phần tử.

Viết CSS

  • CSS riêng của một khu vực nằm trong CSS Module cạnh component, như components/home/Landing.module.css và Home.module.css.
  • Biến CSS riêng cho từng phần tử, như thứ tự hiện --i, đặt qua rule [data-*] trong CSS Module. Type CSSProperties của React không nhận --x nếu không ép kiểu.
  • Kiểm tra trên bản đã build ở 1440x900, 1280x720 và 390x844, theme sáng và tối, có và không có giảm chuyển động. Skill /verify-ui chứa các bước này.

Hình mẫu

Owner duyệt các bản demo HTML ngày 2026-09-29. Ảnh dưới chụp bản build đọc database production.

Landing page cho người chưa đăng nhập. Quả cầu quay, tô màu các nước nói từng thứ tiếng, và từ vừa tra bay từ Hà Nội tới từng nơi.

Landing page, phần đầu trang

Cuộn xuống thì quả cầu mở phẳng thành bản đồ, rồi tách thành 3 cột, mỗi cột một thứ tiếng và một con số về thứ tiếng đó.

Quả cầu mở phẳng

Ba cột ngôn ngữ

Theme tối dùng cùng bố cục, nền đen và dải xanh giữ nguyên.

Landing page ở theme tối

Homepage khi đã đăng nhập có 3 bố cục. Bàn học là mặc định.

Bố cục Bàn học

Bố cục Ôn ngay

Bố cục Quả cầu của tôi

Ở 390 px mọi thứ xếp một cột.

Landing page ở 390 px

Trang Dịch. Mỗi chiều tra có một ô riêng, kết quả của từng thứ tiếng nằm trên nền pastel riêng.

Trang Dịch

Trang chi tiết từ ở bố cục Tổng quan. Headword tiếng Anh dùng Newsreader, dạng bất quy tắc được tô bằng --zs-mark.

Trang chi tiết từ

Sổ tay ở chế độ lưới. Mỗi card có dải màu ngôn ngữ ở mép trái, nút xoá là link có gạch chân.

Sổ tay, chế độ lưới

Clone this wiki locally