Thông tin tổng quan cơ bản về cách tạo thành phần chuyển đổi giao diện thích ứng và dễ tiếp cận.
Trong bài đăng này, tôi muốn chia sẻ suy nghĩ về cách tạo thành phần chuyển đổi giao diện sáng và tối. Dùng thử bản minh hoạ.
Nếu bạn thích xem video, hãy xem phiên bản video của bài đăng này trên YouTube:
Tổng quan
Một trang web có thể cung cấp các chế độ cài đặt để kiểm soát bảng phối màu thay vì hoàn toàn dựa vào lựa chọn ưu tiên của hệ thống. Điều này có nghĩa là người dùng có thể duyệt web ở một chế độ khác với lựa chọn ưu tiên của hệ thống. Ví dụ: hệ thống của người dùng đang ở giao diện sáng, nhưng người dùng muốn trang web hiển thị ở giao diện tối.
Có một số điểm cần cân nhắc về kỹ thuật web khi xây dựng tính năng này. Ví dụ: trình duyệt cần được thông báo về lựa chọn ưu tiên càng sớm càng tốt để ngăn chặn hiện tượng nhấp nháy màu trang. Trước tiên, chế độ điều khiển cần đồng bộ hoá với hệ thống, sau đó cho phép các trường hợp ngoại lệ được lưu trữ phía máy khách.
Markup (note: đây là tên ứng dụng)
Bạn nên sử dụng <button>
cho nút bật/tắt, vì sau đó, bạn sẽ được hưởng lợi từ các sự kiện và tính năng tương tác do trình duyệt cung cấp, chẳng hạn như sự kiện nhấp và khả năng lấy tiêu điểm.
Nút
Nút cần có một lớp để sử dụng từ CSS và một mã nhận dạng để sử dụng từ JavaScript.
Ngoài ra, vì nội dung của nút là biểu tượng thay vì văn bản, hãy thêm thuộc tính title (tiêu đề) để cung cấp thông tin về mục đích của nút. Cuối cùng, hãy thêm một [aria-label]
để giữ trạng thái của nút biểu tượng, nhờ đó, trình đọc màn hình có thể chia sẻ trạng thái của giao diện với những người khiếm thị.
<button
class="theme-toggle"
id="theme-toggle"
title="Toggles light & dark"
aria-label="auto"
>
…
</button>
aria-label
và aria-live
lịch sự
Để cho trình đọc màn hình biết rằng bạn cần thông báo về các thay đổi đối với aria-label
, hãy thêm aria-live="polite"
vào nút.
<button
class="theme-toggle"
id="theme-toggle"
title="Toggles light & dark"
aria-label="auto"
aria-live="polite"
>
…
</button>
Việc thêm mã đánh dấu này sẽ báo hiệu cho trình đọc màn hình một cách lịch sự, thay vì aria-live="assertive"
, cho người dùng biết nội dung đã thay đổi. Trong trường hợp của nút này, nút sẽ thông báo "sáng" hoặc "tối" tuỳ thuộc vào trạng thái của aria-label
.
Biểu tượng đồ hoạ vectơ có thể mở rộng (SVG)
SVG cung cấp một cách để tạo các hình dạng có chất lượng cao, có thể mở rộng với ít mã đánh dấu nhất. Việc tương tác với nút này có thể kích hoạt các trạng thái hình ảnh mới cho các vectơ, giúp SVG trở thành một lựa chọn tuyệt vời cho biểu tượng.
Mã đánh dấu SVG sau đây nằm bên trong <button>
:
<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
…
</svg>
aria-hidden
đã được thêm vào phần tử SVG để trình đọc màn hình biết bỏ qua phần tử đó vì phần tử này được đánh dấu là trình bày. Đây là cách tuyệt vời để trang trí hình ảnh, chẳng hạn như biểu tượng bên trong nút. Ngoài thuộc tính viewBox
bắt buộc trên phần tử, hãy thêm chiều cao và chiều rộng vì các lý do tương tự khiến hình ảnh phải có kích thước nội tuyến.
Mặt trời
Hình ảnh mặt trời bao gồm một vòng tròn và các đường mà SVG có hình dạng thuận tiện. <circle>
được căn giữa bằng cách đặt thuộc tính cx
và cy
thành 12,
tức là một nửa kích thước khung nhìn (24), sau đó đặt bán kính (r
) là 6
để đặt kích thước.
<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
<circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
</svg>
Ngoài ra, thuộc tính mặt nạ trỏ đến mã nhận dạng của phần tử SVG mà bạn sẽ tạo tiếp theo và cuối cùng là cung cấp màu tô khớp với màu văn bản của trang bằng currentColor
.
Ánh nắng
Tiếp theo, các đường tia nắng được thêm ngay bên dưới vòng tròn, bên trong một nhóm phần tử <g>
.
<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
<circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
<g class="sun-beams" stroke="currentColor">
<line x1="12" y1="1" x2="12" y2="3" />
<line x1="12" y1="21" x2="12" y2="23" />
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
<line x1="1" y1="12" x2="3" y2="12" />
<line x1="21" y1="12" x2="23" y2="12" />
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
</g>
</svg>
Lần này, thay vì giá trị của fill là currentColor
, mỗi dòng sẽ được đặt stroke. Các đường kẻ cùng với hình tròn tạo nên một mặt trời đẹp với các tia sáng.
Mặt trăng
Để tạo hiệu ứng chuyển đổi liền mạch giữa ánh sáng (mặt trời) và bóng tối (mặt trăng), mặt trăng là một phần mở rộng của biểu tượng mặt trời, sử dụng mặt nạ SVG.
<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
<circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
<g class="sun-beams" stroke="currentColor">
…
</g>
<mask class="moon" id="moon-mask">
<rect x="0" y="0" width="100%" height="100%" fill="white" />
<circle cx="24" cy="10" r="6" fill="black" />
</mask>
</svg>
Mặt nạ bằng SVG rất mạnh mẽ, cho phép màu trắng và đen xoá hoặc bao gồm các phần của một hình ảnh khác. Biểu tượng mặt trời sẽ bị che khuất bởi hình dạng mặt trăng <circle>
có mặt nạ SVG, chỉ bằng cách di chuyển hình tròn vào và ra khỏi vùng mặt nạ.
Điều gì sẽ xảy ra nếu CSS không tải?
Bạn nên kiểm thử SVG như thể CSS không tải để đảm bảo kết quả không quá lớn hoặc gây ra vấn đề về bố cục. Các thuộc tính chiều cao và chiều rộng cùng dòng trên SVG cùng với việc sử dụng currentColor
cung cấp các quy tắc kiểu tối thiểu để trình duyệt sử dụng nếu CSS không tải. Điều này tạo ra các kiểu phòng thủ hiệu quả chống lại sự nhiễu mạng.
Bố cục
Thành phần nút chuyển đổi giao diện có diện tích bề mặt nhỏ, vì vậy bạn không cần lưới hoặc hộp flex cho bố cục. Thay vào đó, vị trí SVG và phép biến đổi CSS được sử dụng.
Kiểu
Kiểu .theme-toggle
Phần tử <button>
là vùng chứa cho các hình dạng và kiểu biểu tượng. Ngữ cảnh gốc này sẽ chứa các màu và kích thước thích ứng để truyền xuống SVG.
Nhiệm vụ đầu tiên là tạo nút thành hình tròn và xoá các kiểu nút mặc định:
.theme-toggle {
--size: 2rem;
background: none;
border: none;
padding: 0;
inline-size: var(--size);
block-size: var(--size);
aspect-ratio: 1;
border-radius: 50%;
}
Tiếp theo, hãy thêm một số kiểu tương tác. Thêm kiểu con trỏ cho người dùng chuột. Thêm touch-action: manipulation
để có trải nghiệm cảm ứng phản hồi nhanh.
Xoá hiệu ứng đánh dấu bán trong suốt mà iOS áp dụng cho các nút. Cuối cùng, hãy tạo một khoảng trống cho đường viền trạng thái tiêu điểm từ cạnh của phần tử:
.theme-toggle {
--size: 2rem;
background: none;
border: none;
padding: 0;
inline-size: var(--size);
block-size: var(--size);
aspect-ratio: 1;
border-radius: 50%;
cursor: pointer;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
outline-offset: 5px;
}
SVG bên trong nút cũng cần một số kiểu. SVG phải vừa với kích thước của nút và để tạo hiệu ứng mềm mại cho hình ảnh, hãy bo tròn các đầu đường viền:
.theme-toggle {
--size: 2rem;
background: none;
border: none;
padding: 0;
inline-size: var(--size);
block-size: var(--size);
aspect-ratio: 1;
border-radius: 50%;
cursor: pointer;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
outline-offset: 5px;
& > svg {
inline-size: 100%;
block-size: 100%;
stroke-linecap: round;
}
}
Định cỡ thích ứng bằng truy vấn nội dung đa phương tiện hover
Kích thước nút biểu tượng hơi nhỏ ở 2rem
, điều này không gây vấn đề gì cho người dùng chuột nhưng có thể gây khó khăn cho con trỏ thô như ngón tay. Đảm bảo nút đáp ứng nhiều nguyên tắc về kích thước thao tác chạm bằng cách sử dụng truy vấn nội dung đa phương tiện khi di chuột để chỉ định tăng kích thước.
.theme-toggle {
--size: 2rem;
…
@media (hover: none) {
--size: 48px;
}
}
Kiểu mặt trời và mặt trăng SVG
Nút này chứa các khía cạnh tương tác của thành phần nút chuyển đổi giao diện, trong khi SVG bên trong sẽ chứa các khía cạnh hình ảnh và ảnh động. Đây là nơi bạn có thể làm cho biểu tượng trở nên đẹp mắt và sống động.
Giao diện sáng
Để ảnh động thay đổi tỷ lệ và xoay từ tâm của các hình dạng SVG, hãy đặt transform-origin: center center
của các hình dạng đó. Các hình dạng sử dụng màu thích ứng do nút cung cấp tại đây. Mặt trăng và mặt trời sử dụng nút được cung cấp var(--icon-fill)
và var(--icon-fill-hover)
để tô màu, trong khi các tia nắng sử dụng các biến để vẽ nét.
.sun-and-moon {
& > :is(.moon, .sun, .sun-beams) {
transform-origin: center center;
}
& > :is(.moon, .sun) {
fill: var(--icon-fill);
@nest .theme-toggle:is(:hover, :focus-visible) > & {
fill: var(--icon-fill-hover);
}
}
& > .sun-beams {
stroke: var(--icon-fill);
stroke-width: 2px;
@nest .theme-toggle:is(:hover, :focus-visible) & {
stroke: var(--icon-fill-hover);
}
}
}
Giao diện tối
Kiểu mặt trăng cần xoá các tia nắng, tăng kích thước vòng tròn mặt trời và di chuyển mặt nạ vòng tròn.
.sun-and-moon {
@nest [data-theme="dark"] & {
& > .sun {
transform: scale(1.75);
}
& > .sun-beams {
opacity: 0;
}
& > .moon > circle {
transform: translateX(-7px);
@supports (cx: 1px) {
transform: translateX(0);
cx: 17px;
}
}
}
}
Lưu ý rằng giao diện tối không có thay đổi hoặc chuyển đổi màu. Thành phần nút mẹ sở hữu các màu, trong đó các màu này đã thích ứng trong ngữ cảnh sáng và tối. Thông tin chuyển đổi phải nằm sau truy vấn nội dung đa phương tiện ưu tiên về chuyển động của người dùng.
Hoạt ảnh
Nút này phải hoạt động và có trạng thái nhưng không có hiệu ứng chuyển đổi tại thời điểm này. Các phần sau đây đều hướng đến việc xác định cách và nội dung chuyển đổi.
Chia sẻ truy vấn nội dung nghe nhìn và nhập easing
Để dễ dàng đặt hiệu ứng chuyển đổi và ảnh động theo lựa chọn ưu tiên về chuyển động của hệ điều hành của người dùng, trình bổ trợ PostCSS Nội dung đa phương tiện tuỳ chỉnh cho phép sử dụng cú pháp thông số kỹ thuật CSS đã soạn cho các biến truy vấn nội dung đa phương tiện:
@custom-media --motionOK (prefers-reduced-motion: no-preference);
/* usage example */
@media (--motionOK) {
.sun {
transition: transform .5s var(--ease-elastic-3);
}
}
Để có các hiệu ứng CSS độc đáo và dễ sử dụng, hãy nhập phần easings (hiệu ứng chuyển động) của Open Props:
@import "https://unpkg.com/open-props/easings.min.css";
/* usage example */
.sun {
transition: transform .5s var(--ease-elastic-3);
}
Mặt trời
Hiệu ứng chuyển đổi mặt trời sẽ vui nhộn hơn so với mặt trăng, đạt được hiệu ứng này bằng cách sử dụng hiệu ứng giảm độ nảy. Các tia nắng sẽ nảy một chút khi xoay và tâm của mặt trời sẽ nảy một chút khi mở rộng.
Kiểu mặc định (giao diện sáng) xác định các hiệu ứng chuyển đổi và kiểu giao diện tối xác định các tuỳ chỉnh cho hiệu ứng chuyển đổi sang giao diện sáng:
.sun-and-moon {
@media (--motionOK) {
& > .sun {
transition: transform .5s var(--ease-elastic-3);
}
& > .sun-beams {
transition:
transform .5s var(--ease-elastic-4),
opacity .5s var(--ease-3)
;
}
@nest [data-theme="dark"] & {
& > .sun {
transform: scale(1.75);
transition-timing-function: var(--ease-3);
transition-duration: .25s;
}
& > .sun-beams {
transform: rotateZ(-25deg);
transition-duration: .15s;
}
}
}
}
Trong bảng điều khiển Ảnh động trong Công cụ của Chrome cho nhà phát triển, bạn có thể tìm thấy tiến trình chuyển đổi ảnh động. Bạn có thể kiểm tra thời lượng của toàn bộ ảnh động, các phần tử và thời gian làm dịu.
Mặt trăng
Vị trí ánh sáng mặt trăng và tối đã được đặt, hãy thêm các kiểu chuyển đổi bên trong truy vấn nội dung nghe nhìn --motionOK
để tạo hiệu ứng sống động trong khi vẫn tuân theo các lựa chọn ưu tiên về chuyển động của người dùng.
Thời gian với độ trễ và thời lượng là yếu tố quan trọng để quá trình chuyển đổi này diễn ra suôn sẻ. Ví dụ: nếu mặt trời bị che khuất quá sớm, thì quá trình chuyển đổi sẽ không có cảm giác được sắp xếp hoặc vui nhộn, mà sẽ có cảm giác hỗn loạn.
.sun-and-moon {
@media (--motionOK) {
& .moon > circle {
transform: translateX(-7px);
transition: transform .25s var(--ease-out-5);
@supports (cx: 1px) {
transform: translateX(0);
cx: 17px;
transition: cx .25s var(--ease-out-5);
}
}
@nest [data-theme="dark"] & {
& > .moon > circle {
transition-delay: .25s;
transition-duration: .5s;
}
}
}
}
Ưu tiên giảm chuyển động
Trong hầu hết các Thử thách về giao diện người dùng đồ hoạ, tôi cố gắng giữ lại một số ảnh động, chẳng hạn như hiệu ứng chuyển đổi độ mờ, cho những người dùng muốn giảm chuyển động. Tuy nhiên, thành phần này hoạt động tốt hơn với các thay đổi trạng thái tức thì.
JavaScript
JavaScript có rất nhiều việc cần làm trong thành phần này, từ việc quản lý thông tin ARIA cho trình đọc màn hình đến việc lấy và đặt giá trị từ bộ nhớ cục bộ.
Trải nghiệm tải trang
Điều quan trọng là không được có hiện tượng nhấp nháy màu sắc khi tải trang. Nếu người dùng có bảng phối màu tối cho biết họ ưu tiên màu sáng với thành phần này, sau đó tải lại trang, thì ban đầu trang sẽ có màu tối rồi chuyển sang màu sáng.
Để ngăn chặn điều này, bạn cần chạy một lượng nhỏ JavaScript chặn với mục tiêu đặt thuộc tính HTML data-theme
càng sớm càng tốt.
<script src="./theme-toggle.js"></script>
Để làm được điều này, trước tiên, thẻ <script>
thuần tuý trong tài liệu <head>
sẽ được tải trước mọi mã đánh dấu CSS hoặc <body>
. Khi gặp một tập lệnh chưa được đánh dấu như thế này, trình duyệt sẽ chạy và thực thi tập lệnh đó trước phần còn lại của HTML. Bằng cách sử dụng khoảnh khắc chặn này một cách tiết kiệm, bạn có thể đặt thuộc tính HTML trước khi CSS chính vẽ trang, nhờ đó ngăn chặn hiện tượng nhấp nháy hoặc màu sắc.
Trước tiên, JavaScript sẽ kiểm tra lựa chọn ưu tiên của người dùng trong bộ nhớ cục bộ và dự phòng để kiểm tra lựa chọn ưu tiên của hệ thống nếu không tìm thấy gì trong bộ nhớ:
const storageKey = 'theme-preference'
const getColorPreference = () => {
if (localStorage.getItem(storageKey))
return localStorage.getItem(storageKey)
else
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light'
}
Tiếp theo, một hàm để đặt lựa chọn ưu tiên của người dùng trong bộ nhớ cục bộ sẽ được phân tích cú pháp:
const setPreference = () => {
localStorage.setItem(storageKey, theme.value)
reflectPreference()
}
Tiếp theo là một hàm để sửa đổi tài liệu bằng các tuỳ chọn.
const reflectPreference = () => {
document.firstElementChild
.setAttribute('data-theme', theme.value)
document
.querySelector('#theme-toggle')
?.setAttribute('aria-label', theme.value)
}
Một điều quan trọng cần lưu ý tại thời điểm này là trạng thái phân tích cú pháp tài liệu HTML. Trình duyệt chưa biết về nút "#theme-toggle" vì thẻ <head>
chưa được phân tích cú pháp đầy đủ. Tuy nhiên, trình duyệt có document.firstElementChild
, còn gọi là thẻ <html>
. Hàm này cố gắng đặt cả hai để đồng bộ hoá, nhưng trong lần chạy đầu tiên, bạn chỉ có thể đặt thẻ HTML. Ban đầu, querySelector
sẽ không tìm thấy gì và toán tử nối tuỳ chọn sẽ đảm bảo không có lỗi cú pháp khi không tìm thấy toán tử này và hàm setAttribute sẽ được gọi.
Tiếp theo, hàm reflectPreference()
đó sẽ được gọi ngay lập tức để tài liệu HTML có thuộc tính data-theme
:
reflectPreference()
Nút vẫn cần thuộc tính, vì vậy, hãy đợi sự kiện tải trang, sau đó bạn có thể truy vấn, thêm trình nghe và đặt thuộc tính trên:
window.onload = () => {
// set on load so screen readers can get the latest value on the button
reflectPreference()
// now this script can find and listen for clicks on the control
document
.querySelector('#theme-toggle')
.addEventListener('click', onClick)
}
Trải nghiệm bật/tắt
Khi người dùng nhấp vào nút này, giao diện cần được hoán đổi trong bộ nhớ JavaScript và trong tài liệu. Bạn cần kiểm tra giá trị giao diện hiện tại và đưa ra quyết định về trạng thái mới của giao diện. Sau khi đặt trạng thái mới, hãy lưu và cập nhật tài liệu:
const onClick = () => {
theme.value = theme.value === 'light'
? 'dark'
: 'light'
setPreference()
}
Đồng bộ hoá với hệ thống
Điểm đặc biệt của nút chuyển đổi giao diện này là đồng bộ hoá với tuỳ chọn hệ thống khi tuỳ chọn đó thay đổi. Nếu người dùng thay đổi lựa chọn ưu tiên về hệ thống trong khi một trang và thành phần này đang hiển thị, thì nút chuyển giao diện sẽ thay đổi để khớp với lựa chọn ưu tiên mới của người dùng, như thể người dùng đã tương tác với nút chuyển giao diện cùng lúc với nút chuyển hệ thống.
Bạn có thể thực hiện việc này bằng JavaScript và sự kiện matchMedia
theo dõi các thay đổi đối với truy vấn nội dung nghe nhìn:
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', ({matches:isDark}) => {
theme.value = isDark ? 'dark' : 'light'
setPreference()
})
Kết luận
Giờ thì bạn đã biết cách tôi làm, còn bạn thì sao‽ 🙂
Hãy đa dạng hoá các phương pháp và tìm hiểu tất cả các cách xây dựng trên web. Hãy tạo bản minh hoạ, gửi đường liên kết cho tôi trên Twitter và tôi sẽ thêm bản minh hoạ đó vào phần phối lại của cộng đồng ở bên dưới!
Bản phối lại của cộng đồng
- @NathanG trên Codepen với Vue
- @ShadowShahriar trên Codepen
- @tomayac dưới dạng phần tử tuỳ chỉnh
- @bramus bằng JavaScript thuần tuý
- @JoshWComeau với react