Thiết lập và duy trì tùy chọn phối màu bằng CSS và một chút JavaScript

theanh

Administrator
Nhân viên
Nhiều trang web hiện đại cung cấp cho người dùng khả năng thiết lập tùy chọn phối màu cụ thể cho trang web. Một triển khai cơ bản rất đơn giản với JavaScript: lắng nghe khi người dùng thay đổi hộp kiểm hoặc nhấp vào nút, chuyển đổi lớp (hoặc thuộc tính) trên phần tử để phản hồi và viết kiểu cho lớp đó để ghi đè thiết kế bằng phối màu khác.

Pseudo-class :has() mới của CSS, được các trình duyệt chính hỗ trợ kể từ tháng 12 năm 2023, mở ra nhiều cánh cửa cho các nhà phát triển giao diện người dùng. Tôi đặc biệt hào hứng khi tận dụng nó để sửa đổi giao diện người dùng để phản hồi tương tác của người dùng mà không cần JavaScript. Trước đây chúng ta sử dụng JavaScript để chuyển đổi các lớp hoặc thuộc tính (hoặc để thiết lập trực tiếp các kiểu), giờ đây chúng ta có thể ghép nối các bộ chọn :has() với các phần tử tương tác gốc của HTML.

Hỗ trợ tùy chọn phối màu, như "Chế độ tối", là một trường hợp sử dụng tuyệt vời. Chúng ta có thể sử dụng phần tử ở bất kỳ đâu để chuyển đổi các phối màu dựa trên đã chọn — không cần JavaScript, ngoại trừ một lần rắc để lưu lựa chọn của người dùng, mà chúng ta sẽ tìm hiểu thêm ở phần sau.

Tôn trọng tùy chọn hệ thống​

Đầu tiên, chúng ta sẽ hỗ trợ tùy chọn phối màu trên toàn hệ thống của người dùng bằng cách áp dụng phương pháp "Chế độ sáng" trước. Nói cách khác, chúng tôi bắt đầu với một bảng màu sáng theo mặc định và đổi sang bảng màu tối cho những người dùng thích.

Tính năng phương tiện prefers-color-scheme phát hiện tùy chọn hệ thống của người dùng. Gói các kiểu "tối" trong truy vấn phương tiện prefers-color-scheme: dark.
Mã:
selector { /* light styles */ @media (prefers-color-scheme: dark) { /* dark styles */ }}
Tiếp theo, đặt thuộc tính color-scheme để khớp với bảng màu ưa thích. Thiết lập color-scheme: dark sẽ chuyển trình duyệt sang chế độ tối tích hợp sẵn, bao gồm nền mặc định màu đen, văn bản mặc định màu trắng, kiểu "tối" cho thanh cuộn và các thành phần khác khó nhắm mục tiêu bằng CSS, v.v. Tôi đang sử dụng các biến CSS để gợi ý rằng giá trị là động — và vì tôi thích trải nghiệm công cụ dành cho nhà phát triển của trình duyệt — nhưng color-scheme: lightcolor-scheme: dark đơn giản sẽ hoạt động tốt.
Mã:
:root { /* kiểu sáng tại đây */ color-scheme: var(--color-scheme, light); /* tùy chọn hệ thống là "dark" */ @media (prefers-color-scheme: dark) { --color-scheme: dark; /* bất kỳ kiểu tối bổ sung nào ở đây */ }}

Cung cấp cho người dùng quyền kiểm soát​

Bây giờ, để hỗ trợ ghi đè tùy chọn hệ thống, hãy cho phép người dùng chọn giữa các lược đồ màu sáng (mặc định) và tối ở cấp độ trang.

HTML có các thành phần gốc để xử lý tương tác của người dùng. Sử dụng một trong các điều khiển đó, thay vì, chẳng hạn, một lồng nhau, sẽ cải thiện khả năng người dùng công nghệ hỗ trợ có trải nghiệm tốt. Tôi sẽ sử dụng menu với các tùy chọn cho "hệ thống", "sáng" và "tối". Một nhóm cũng sẽ hiệu quả nếu bạn muốn các tùy chọn ngay trên bề mặt thay vì menu thả xuống.
Mã:
 System Light Dark
Trước khi CSS có :has(), việc phản hồi cho đã chọn của người dùng yêu cầu JavaScript, ví dụ, thiết lập trình lắng nghe sự kiện trên để chuyển đổi một lớp hoặc thuộc tính trên hoặc .

Nhưng bây giờ chúng ta đã có :has(), chúng ta có thể thực hiện điều này chỉ bằng CSS! Bạn sẽ tiết kiệm được chi phí cho bất kỳ ngân sách hiệu suất nào của mình cho một tập lệnh chế độ tối, cộng với khả năng kiểm soát sẽ hoạt động ngay cả với những người dùng đã tắt JavaScript. Và bất kỳ người dùng "không có JS" nào trong dự án sẽ hài lòng.

Những gì chúng ta cần là một bộ chọn áp dụng cho trang khi nó :has() một menu select với [value]:checked cụ thể. Hãy dịch điều đó thành CSS:
Mã:
:root:has(select option[value="dark"]:checked)
Chúng ta đang mặc định sử dụng một bảng màu sáng, do đó, chỉ cần tính đến hai trường hợp bảng màu tối có thể xảy ra là đủ:
  1. Tùy chọn màu cấp trang là "hệ thống" và tùy chọn cấp hệ thống là "tối".
  2. Tùy chọn màu cấp trang là "tối".
Đầu tiên là một phiên bản lặp lại có nhận thức về tùy chọn trang của trường hợp prefers-color-scheme: dark của chúng tôi. Tùy chọn cấp hệ thống "dark" không còn đủ để đảm bảo các kiểu tối nữa; chúng ta cần tùy chọn cấp hệ thống "dark" và "follow the system-level preference" ở tùy chọn cấp trang. Chúng ta sẽ gói các kiểu lược đồ tối của truy vấn phương tiện prefers-color-scheme bằng bộ chọn :has() mà chúng ta vừa viết:
Mã:
:root { /* kiểu sáng tại đây */ color-scheme: var(--color-scheme, light); /* tùy chọn trang là "system", và tùy chọn hệ thống là "dark" */ @media (prefers-color-scheme: dark) { &:has(#color-scheme option[value="system"]:checked) { --color-scheme: dark; /* bất kỳ kiểu tối bổ sung nào, một lần nữa */ } }}
Lưu ý rằng tôi đang sử dụng CSS Nesting trong đoạn trích cuối cùng đó. Baseline 2023 đã gắn thẻ là "Mới có trên các trình duyệt chính" có nghĩa là hỗ trợ tốt, nhưng tại thời điểm viết bài, hỗ trợ trên các trình duyệt Android không được bao gồm trong bộ trình duyệt cốt lõi của Baselinecó giới hạn. Bạn có thể có được kết quả tương tự mà không cần lồng nhau.
Mã:
:root { /* light styles */ color-scheme: var(--color-scheme, light); /* page preference is "dark" */ &:has(#color-scheme option[value="dark"]:checked) { --color-scheme: dark; /* any additional dark styles */ }}
Đối với kịch bản chế độ tối thứ hai, chúng ta sẽ sử dụng gần như chính xác cùng một bộ chọn :has() như chúng ta đã làm đối với kịch bản đầu tiên, lần này kiểm tra xem tùy chọn "dark" — thay vì tùy chọn "system" — có được chọn hay không:
Mã:
:root { /* light styles */ color-scheme: var(--color-scheme, light); /* tùy chọn trang là "tối" */ &:has(#color-scheme option[value="dark"]:checked) { --color-scheme: dark; /* bất kỳ kiểu tối bổ sung nào */ } /* tùy chọn trang là "hệ thống" và tùy chọn hệ thống là "tối" */ @media (prefers-color-scheme: dark) { &:has(#color-scheme option[value="system"]:checked) { --color-scheme: dark; /* bất kỳ kiểu tối bổ sung nào, một lần nữa */ } }}
Bây giờ, kiểu của trang phản hồi với cả những thay đổi trong cài đặt hệ thống của người dùng tương tác của người dùng với giao diện người dùng tùy chọn màu của trang — tất cả đều bằng CSS!

Nhưng màu sắc thay đổi ngay lập tức. Hãy làm cho quá trình chuyển đổi trở nên mượt mà hơn.

Tôn trọng tùy chọn chuyển động​

Trong một số trường hợp, việc thay đổi kiểu tức thời có thể trở nên kém thanh lịch và đây là một trong số đó. Vì vậy, hãy áp dụng chuyển đổi CSS trên :root để "làm dễ dàng" việc chuyển đổi giữa các bảng màu. (Các kiểu chuyển đổi tại :root sẽ lan tỏa xuống phần còn lại của trang, điều này có thể cần phải thêm transition: none hoặc các ghi đè chuyển đổi khác.)

Lưu ý rằng thuộc tính color-scheme của CSS không hỗ trợ chuyển đổi.
Mã:
:root { transition-duration: 200ms; transition-property: /* các thuộc tính đã thay đổi theo kiểu sáng/tối của bạn */;}
Không phải tất cả người dùng đều coi việc thêm chuyển đổi là một cải tiến đáng hoan nghênh. Truy vấn tính năng phương tiện prefers-reduced-motion cho phép chúng tôi tính đến sở thích chuyển động của người dùng. Nếu giá trị được đặt thành reduce, thì chúng tôi sẽ xóa transition-duration để loại bỏ chuyển động không mong muốn.
Mã:
:root { transition-duration: 200ms; transition-property: /* các thuộc tính thay đổi theo kiểu sáng/tối của bạn */; @media screen and (prefers-reduced-motion: reduce) { transition-duration: none; }}
Chuyển tiếp cũng có thể tạo ra trải nghiệm người dùng kém trên các thiết bị hiển thị thay đổi chậm, ví dụ như các thiết bị có màn hình e-ink. Chúng ta có thể mở rộng truy vấn phương tiện “điều kiện không chuyển động” để tính đến điều đó bằng tính năng phương tiện update. Nếu giá trị của nó là slow, thì chúng ta sẽ xóa transition-duration.
Mã:
:root { transition-duration: 200ms; transition-property: /* các thuộc tính thay đổi theo kiểu sáng/tối của bạn */; @media screen and (prefers-reduced-motion: reduce), (update: slow) { transition-duration: 0s; }}
Chúng ta hãy thử những gì chúng ta đã có trong bản demo sau đây. Lưu ý rằng, để giải quyết vấn đề thiếu hỗ trợ chuyển tiếp của color-scheme, tôi đã định kiểu rõ ràng cho các thuộc tính cần chuyển tiếp trong quá trình thay đổi chủ đề.

Xem Bút [Công cụ chuyển đổi chủ đề chỉ dành cho CSS (yêu cầu :has()) [phân nhánh]](https://codepen.io/smashingmag/pen/YzMVQja) của Henry.
Xem Bút Công cụ chuyển đổi chủ đề chỉ dành cho CSS (yêu cầu :has()) [phân nhánh] của Henry.
Không tệ! Nhưng điều gì sẽ xảy ra nếu người dùng làm mới các trang hoặc điều hướng đến một trang khác? Việc tải lại sẽ xóa sạch lựa chọn biểu mẫu của người dùng, buộc người dùng phải thực hiện lại lựa chọn. Điều đó có thể chấp nhận được trong một số bối cảnh, nhưng có khả năng sẽ đi ngược lại mong đợi của người dùng. Hãy đưa JavaScript vào để có một chút cải tiến dần dần dưới dạng…

Persistence​

Đây là một triển khai JavaScript thuần túy. Đây là một điểm khởi đầu ngây thơ — các hàm và biến không được đóng gói mà thay vào đó là các thuộc tính trên window. Bạn sẽ muốn điều chỉnh theo cách phù hợp với các quy ước, khuôn khổ, thư viện, v.v. của trang web.

Khi người dùng thay đổi bảng màu từ menu , chúng tôi sẽ lưu trữ giá trị đã chọn trong một mục localStorage mới có tên là "preferredColorScheme". Trong các lần tải trang tiếp theo, chúng tôi sẽ kiểm tra localStorage để tìm mục "preferredColorScheme". Nếu mục này tồn tại và nếu giá trị của mục này tương ứng với một trong các tùy chọn điều khiển biểu mẫu, chúng tôi sẽ khôi phục tùy chọn của người dùng bằng cách cập nhật theo chương trình lựa chọn menu.
Mã:
/* * Nếu tùy chọn bảng màu đã được lưu trữ trước đó, * hãy chọn tùy chọn tương ứng trong Giao diện người dùng tùy chọn bảng màu * trừ khi tùy chọn đó đã được chọn. */function restoreColorSchemePreference() { const colorScheme = localStorage.getItem(colorSchemeStorageItemName); if (!colorScheme) { // Không có tùy chọn nào được lưu trữ để khôi phục return; } const option = colorSchemeSelectorEl.querySelector(`[value=${colorScheme}]`); if (!option) { // Tùy chọn được lưu trữ không có tùy chọn tương ứng trong UI. localStorage.removeItem(colorSchemeStorageItemName); return; } if (option.selected) { // Tùy chọn menu tương ứng của tùy chọn được lưu trữ đã được chọn return; } option.selected = true;}/* * Lưu trữ giá trị của mục tiêu sự kiện trong localStorage theo colorSchemeStorageItemName */function storeColorSchemePreference({ target }) { const colorScheme = target.querySelector(":checked").value; localStorage.setItem(colorSchemeStorageItemName, colorScheme);}// Tên mà tùy chọn phối màu của người dùng sẽ được lưu trữ.const colorSchemeStorageItemName = "preferredColorScheme";// Giao diện người dùng giao diện người dùng giao diện người dùng giao diện người dùng giao diện người dùng.const colorSchemeSelectorEl = document.querySelector("#color-scheme");if (colorSchemeSelectorEl) { restoreColorSchemePreference(); // Khi người dùng thay đổi tùy chọn phối màu của họ thông qua Giao diện người dùng, // lưu trữ tùy chọn mới. colorSchemeSelectorEl.addEventListener("input", storeColorSchemePreference);}
Chúng ta hãy thử xem. Mở bản demo này (có thể trong một cửa sổ mới), sử dụng menu để thay đổi bảng màu, sau đó làm mới trang để xem tùy chọn của bạn có được duy trì hay không:

Xem Pen [Công cụ chuyển đổi chủ đề chỉ dành cho CSS (yêu cầu :has()) với JS persistence [phân nhánh]](https://codepen.io/smashingmag/pen/GRLmEXX) của Henry.
Xem Pen Công cụ chuyển đổi chủ đề chỉ dành cho CSS (yêu cầu :has()) với JS persistence [phân nhánh] của Henry.
Nếu tùy chọn phối màu hệ thống của bạn là "sáng" và bạn đặt phối màu của bản demo thành "tối", bạn có thể nhận được các kiểu chế độ sáng trong giây lát ngay sau khi tải lại trang trước khi các kiểu chế độ tối có hiệu lực. Đó là vì CodePen tải JavaScript của riêng nó trước các tập lệnh của bản demo. Điều đó nằm ngoài tầm kiểm soát của tôi, nhưng bạn có thể cẩn thận cải thiện tính bền bỉ này trên các dự án của mình.

Cân nhắc về hiệu suất bền bỉ​

Mọi thứ có thể trở nên khó khăn khi khôi phục tùy chọn của người dùng ngay lập tức sau khi trang tải xong. Nếu tùy chọn phối màu trong localStorage khác với tùy chọn phối màu cấp hệ thống của người dùng, thì có khả năng người dùng sẽ thấy phối màu tùy chọn của hệ thống trước khi tùy chọn cấp trang được khôi phục. (Những người dùng đã chọn tùy chọn "Hệ thống" sẽ không bao giờ thấy đèn nháy đó; những người có cài đặt hệ thống khớp với tùy chọn đã chọn trong điều khiển biểu mẫu cũng sẽ không thấy.)

Nếu triển khai của bạn hiển thị "đèn nháy chủ đề màu không chính xác", thì vấn đề xảy ra ở đâu? Nói chung, các tập lệnh xuất hiện càng sớm trên trang thì rủi ro càng thấp. Tất nhiên, "lựa chọn tốt nhất" dành cho bạn sẽ phụ thuộc vào ngăn xếp cụ thể của bạn.

Còn các trình duyệt không hỗ trợ :has() thì sao?​

Tất cả các trình duyệt chính hiện nay đều hỗ trợ :has() Hãy tìm hiểu về các nền tảng hiện đại nếu bạn có thể. Nhưng nếu bạn cần cân nhắc đến các trình duyệt cũ, như Internet Explorer, thì có hai hướng bạn có thể thực hiện: ẩn hoặc xóa trình chọn phối màu cho các trình duyệt đó hoặc sử dụng JavaScript nhiều hơn.

Nếu bạn coi hỗ trợ phối màu là một cải tiến tiến bộ, bạn có thể ẩn hoàn toàn giao diện người dùng lựa chọn trong các trình duyệt không hỗ trợ :has():
Mã:
@supports not selector(:has(body)) { @media (prefers-color-scheme: dark) { :root { /* dark styles here */ } } #color-scheme { display: none; }}
Nếu không, bạn sẽ cần dựa vào giải pháp JavaScript không chỉ để duy trì mà còn để có chức năng cốt lõi. Quay lại trình lắng nghe sự kiện truyền thống để chuyển đổi một lớp hoặc thuộc tính.

CSS-Tricks “Hướng dẫn đầy đủ về chế độ tối” trình bày chi tiết một số phương pháp thay thế mà bạn cũng có thể cân nhắc khi làm việc trên nền tảng cũ.
 
Back
Bên trên