Thông tin tổng quan cơ bản về cách tạo thanh tải thích ứng với màu sắc và dễ tiếp cận bằng phần tử <progress>
.
Trong bài đăng này, tôi muốn chia sẻ suy nghĩ về cách tạo một thanh tải có thể thích ứng với màu sắc và hỗ trợ tiếp cận bằng phần tử <progress>
. Dùng thử bản minh hoạ và xem mã nguồn!
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
Phần tử <progress>
cung cấp phản hồi bằng hình ảnh và âm thanh cho người dùng về việc hoàn tất. Phản hồi trực quan này rất có giá trị trong các trường hợp như: tiến trình của một biểu mẫu, hiển thị thông tin tải xuống hoặc tải lên, hoặc thậm chí cho biết rằng tiến trình không xác định nhưng công việc vẫn đang hoạt động.
Thử thách GUI này đã hoạt động với phần tử <progress>
HTML hiện có để tiết kiệm một số công sức trong việc hỗ trợ tiếp cận. Màu sắc và bố cục đẩy giới hạn tuỳ chỉnh cho phần tử tích hợp, để hiện đại hoá thành phần và giúp thành phần đó phù hợp hơn trong hệ thống thiết kế.
Markup (note: đây là tên ứng dụng)
Tôi đã chọn gói phần tử <progress>
trong
<label>
để
có thể bỏ qua các thuộc tính mối quan hệ rõ ràng để ưu tiên mối quan hệ
ngầm ẩn.
Tôi cũng đã gắn nhãn một phần tử mẹ chịu ảnh hưởng của trạng thái tải, vì vậy, các công nghệ trình đọc màn hình có thể chuyển tiếp thông tin đó trở lại cho người dùng.
<progress></progress>
Nếu không có value
, thì tiến trình của phần tử sẽ là không xác định.
Thuộc tính max
mặc định là 1, vì vậy, tiến trình nằm trong khoảng từ 0 đến 1. Ví dụ: việc đặt max
thành 100 sẽ đặt phạm vi thành 0-100. Tôi chọn nằm trong giới hạn 0 và 1, chuyển đổi các giá trị tiến trình thành 0,5 hoặc 50%.
Tiến trình được bao bọc bằng nhãn
Trong mối quan hệ ngầm ẩn, một phần tử tiến trình được bao bọc bằng một nhãn như sau:
<label>Loading progress<progress></progress></label>
Trong bản minh hoạ, tôi chọn thêm nhãn chỉ dành cho trình đọc màn hình.
Bạn có thể thực hiện việc này bằng cách gói văn bản nhãn trong <span>
và áp dụng một số kiểu cho văn bản đó để văn bản đó nằm ngoài màn hình một cách hiệu quả:
<label>
<span class="sr-only">Loading progress</span>
<progress></progress>
</label>
Với CSS đi kèm sau đây của WebAIM:
.sr-only {
clip: rect(1px, 1px, 1px, 1px);
clip-path: inset(50%);
height: 1px;
width: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
}
Khu vực bị ảnh hưởng bởi tiến trình tải
Nếu có thị lực tốt, bạn có thể dễ dàng liên kết chỉ báo tiến trình với các phần tử và khu vực trang có liên quan, nhưng đối với người dùng khiếm thị, điều này không rõ ràng. Hãy cải thiện vấn đề này bằng cách gán thuộc tính aria-busy
cho phần tử trên cùng sẽ thay đổi khi quá trình tải hoàn tất.
Hơn nữa, hãy chỉ ra mối quan hệ giữa tiến trình và vùng tải bằng aria-describedby
.
<main id="loading-zone" aria-busy="true">
…
<progress aria-describedby="loading-zone"></progress>
</main>
Từ JavaScript, hãy chuyển aria-busy
sang true
khi bắt đầu tác vụ và sang false
khi hoàn tất.
Thêm thuộc tính Aria
Mặc dù vai trò ngầm ẩn của phần tử <progress>
là progressbar
, nhưng tôi đã làm rõ vai trò này cho các trình duyệt thiếu vai trò ngầm ẩn đó. Tôi cũng đã thêm thuộc tính indeterminate
để đặt rõ ràng phần tử này vào trạng thái không xác định, rõ ràng hơn so với việc quan sát phần tử không có value
.
<label>
Loading
<progress
indeterminate
role="progressbar"
aria-describedby="loading-zone"
tabindex="-1"
>unknown</progress>
</label>
Sử dụng tabindex="-1"
để đặt phần tử tiến trình làm tiêu điểm từ JavaScript. Điều này rất quan trọng đối với công nghệ trình đọc màn hình, vì việc đặt tiêu điểm tiến trình khi tiến trình thay đổi sẽ thông báo cho người dùng tiến trình cập nhật đã đạt được đến đâu.
Kiểu
Phần tử tiến trình hơi khó xử lý khi tạo kiểu. Các phần tử HTML tích hợp có các phần ẩn đặc biệt có thể khó chọn và thường chỉ cung cấp một bộ thuộc tính hạn chế để đặt.
Bố cục
Kiểu bố cục được thiết kế để cho phép một số tính linh hoạt trong kích thước và vị trí nhãn của phần tử tiến trình. Trạng thái hoàn thành đặc biệt được thêm vào có thể là một tín hiệu hình ảnh bổ sung hữu ích nhưng không bắt buộc.
Bố cục <progress>
Chiều rộng của phần tử tiến trình không bị thay đổi để có thể thu nhỏ và mở rộng với không gian cần thiết trong thiết kế. Các kiểu tích hợp sẽ bị xoá bằng cách đặt appearance
và border
thành none
. Việc này được thực hiện để có thể chuẩn hoá phần tử trên các trình duyệt, vì mỗi trình duyệt có kiểu riêng cho phần tử của trình duyệt đó.
progress {
--_track-size: min(10px, 1ex);
--_radius: 1e3px;
/* reset */
appearance: none;
border: none;
position: relative;
height: var(--_track-size);
border-radius: var(--_radius);
overflow: hidden;
}
Giá trị của 1e3px
cho _radius
sử dụng ký hiệu số thập phân để biểu thị một số lớn, vì vậy border-radius
luôn được làm tròn. Cách này tương đương với 1000px
. Tôi thích sử dụng giá trị này vì mục tiêu của tôi là sử dụng một giá trị đủ lớn để có thể đặt và quên giá trị đó (và giá trị này ngắn hơn so với 1000px
). Bạn cũng có thể dễ dàng tăng giá trị này lên nếu cần: chỉ cần thay đổi 3 thành 4, sau đó 1e4px
sẽ tương đương với 10000px
.
overflow: hidden
được sử dụng và là một kiểu gây tranh cãi. Điều này giúp một số việc trở nên dễ dàng, chẳng hạn như không cần truyền giá trị border-radius
xuống kênh và theo dõi các phần tử lấp đầy; nhưng điều đó cũng có nghĩa là không có phần tử con nào của tiến trình có thể nằm ngoài phần tử. Bạn có thể thực hiện một vòng lặp khác trên phần tử tiến trình tuỳ chỉnh này mà không cần overflow: hidden
và điều này có thể mở ra một số cơ hội cho ảnh động hoặc trạng thái hoàn thành tốt hơn.
Tiến trình đã hoàn tất
Bộ chọn CSS sẽ thực hiện công việc khó khăn ở đây bằng cách so sánh giá trị tối đa với giá trị và nếu chúng khớp nhau thì tiến trình sẽ hoàn tất. Khi hoàn tất, một phần tử giả sẽ được tạo và thêm vào cuối phần tử tiến trình, cung cấp thêm một tín hiệu hình ảnh thú vị cho quá trình hoàn tất.
progress:not([max])[value="1"]::before,
progress[max="100"][value="100"]::before {
content: "✓";
position: absolute;
inset-block: 0;
inset-inline: auto 0;
display: flex;
align-items: center;
padding-inline-end: max(calc(var(--_track-size) / 4), 3px);
color: white;
font-size: calc(var(--_track-size) / 1.25);
}
Màu
Trình duyệt mang màu sắc riêng cho phần tử tiến trình và thích ứng với chế độ sáng và tối chỉ bằng một thuộc tính CSS. Bạn có thể xây dựng dựa trên một số bộ chọn đặc biệt dành riêng cho trình duyệt.
Kiểu trình duyệt sáng và tối
Để chọn trang web của bạn thành phần <progress>
thích ứng với chế độ sáng và tối, bạn chỉ cần color-scheme
.
progress {
color-scheme: light dark;
}
Màu nền của tiến trình xử lý một cơ sở lưu trú
Để phủ màu một phần tử <progress>
, hãy sử dụng accent-color
.
progress {
accent-color: rebeccapurple;
}
Lưu ý màu nền của bản nhạc thay đổi từ sáng sang tối tuỳ thuộc vào accent-color
. Trình duyệt đang đảm bảo độ tương phản phù hợp: khá gọn gàng.
Màu sáng và tối tuỳ chỉnh hoàn toàn
Đặt hai thuộc tính tuỳ chỉnh trên phần tử <progress>
, một thuộc tính cho màu của bản nhạc và một thuộc tính cho màu của tiến trình bản nhạc. Bên trong truy vấn nội dung nghe nhìn prefers-color-scheme
, hãy cung cấp giá trị màu mới cho bản nhạc và tiến trình theo dõi.
progress {
--_track: hsl(228 100% 90%);
--_progress: hsl(228 100% 50%);
}
@media (prefers-color-scheme: dark) {
progress {
--_track: hsl(228 20% 30%);
--_progress: hsl(228 100% 75%);
}
}
Kiểu tiêu điểm
Trước đó, chúng ta đã cung cấp cho phần tử một chỉ mục thẻ âm để có thể lấy tiêu điểm theo phương thức lập trình. Sử dụng :focus-visible
để tuỳ chỉnh tiêu điểm nhằm chọn kiểu vòng tròn tiêu điểm thông minh hơn. Với cách này, thao tác nhấp chuột và lấy tiêu điểm sẽ không hiển thị vòng tiêu điểm, nhưng thao tác nhấp bàn phím sẽ hiển thị. Bạn nên xem video trên YouTube này để hiểu rõ hơn về vấn đề này.
progress:focus-visible {
outline-color: var(--_progress);
outline-offset: 5px;
}
Kiểu tuỳ chỉnh trên các trình duyệt
Tuỳ chỉnh kiểu bằng cách chọn các phần của phần tử <progress>
mà mỗi trình duyệt hiển thị. Việc sử dụng phần tử tiến trình là một thẻ duy nhất, nhưng phần tử này được tạo từ một số phần tử con hiển thị thông qua bộ chọn giả CSS. Công cụ cho nhà phát triển Chrome sẽ hiển thị các phần tử này cho bạn nếu bạn bật chế độ cài đặt:
- Nhấp chuột phải vào trang rồi chọn Kiểm tra phần tử để mở Công cụ cho nhà phát triển.
- Nhấp vào biểu tượng bánh răng Cài đặt ở góc trên bên phải của cửa sổ DevTools.
- Trong tiêu đề Elements (Thành phần), hãy tìm và bật hộp đánh dấu Show user agent shadow DOM (Hiển thị shadow DOM của tác nhân người dùng).
Kiểu Safari và Chromium
Các trình duyệt dựa trên WebKit như Safari và Chromium hiển thị ::-webkit-progress-bar
và ::-webkit-progress-value
, cho phép sử dụng một tập hợp con CSS. Hiện tại, hãy đặt background-color
bằng các thuộc tính tuỳ chỉnh đã tạo trước đó để thích ứng với chế độ sáng và tối.
/* Safari/Chromium */
progress[value]::-webkit-progress-bar {
background-color: var(--_track);
}
progress[value]::-webkit-progress-value {
background-color: var(--_progress);
}
Kiểu Firefox
Firefox chỉ hiển thị bộ chọn giả ::-moz-progress-bar
trên phần tử <progress>
. Điều này cũng có nghĩa là chúng ta không thể trực tiếp phủ màu cho bản nhạc.
/* Firefox */
progress[value]::-moz-progress-bar {
background-color: var(--_progress);
}
Lưu ý rằng Firefox có màu theo dõi được đặt từ accent-color
trong khi iOS Safari có màu theo dõi màu xanh dương nhạt. Tương tự như vậy trong chế độ tối: Firefox có một dải màu tối nhưng không phải là màu tuỳ chỉnh mà chúng ta đã đặt và dải màu này hoạt động trong các trình duyệt dựa trên Webkit.
Hoạt ảnh
Khi làm việc với bộ chọn giả lập tích hợp sẵn trong trình duyệt, bộ chọn này thường có một nhóm thuộc tính CSS được phép hạn chế.
Tạo ảnh động cho quá trình lấp đầy bản nhạc
Việc thêm hiệu ứng chuyển đổi vào inline-size
của phần tử tiến trình hoạt động được cho Chromium nhưng không hoạt động cho Safari. Firefox cũng không sử dụng thuộc tính chuyển đổi trên ::-moz-progress-bar
.
/* Chromium Only 😢 */
progress[value]::-webkit-progress-value {
background-color: var(--_progress);
transition: inline-size .25s ease-out;
}
Tạo ảnh động cho trạng thái :indeterminate
Ở đây, tôi sáng tạo hơn một chút để có thể cung cấp ảnh động. Một phần tử giả cho Chromium được tạo và một hiệu ứng chuyển màu được áp dụng với ảnh động qua lại cho cả ba trình duyệt.
Thuộc tính tuỳ chỉnh
Thuộc tính tuỳ chỉnh rất hữu ích cho nhiều việc, nhưng một trong những thuộc tính mà tôi yêu thích nhất chỉ là đặt tên cho một giá trị CSS trông có vẻ kỳ diệu. Sau đây là một linear-gradient
khá phức tạp nhưng có tên rất hay. Mục đích và trường hợp sử dụng của ứng dụng có thể được hiểu rõ ràng.
progress {
--_indeterminate-track: linear-gradient(to right,
var(--_track) 45%,
var(--_progress) 0%,
var(--_progress) 55%,
var(--_track) 0%
);
--_indeterminate-track-size: 225% 100%;
--_indeterminate-track-animation: progress-loading 2s infinite ease;
}
Thuộc tính tuỳ chỉnh cũng sẽ giúp mã luôn khô ráo vì một lần nữa, chúng ta không thể nhóm các bộ chọn dành riêng cho trình duyệt này lại với nhau.
Khung hình chính
Mục tiêu là một ảnh động vô hạn lặp đi lặp lại. Các khung hình bắt đầu và kết thúc sẽ được đặt trong CSS. Bạn chỉ cần một khung hình chính, đó là khung hình chính ở giữa tại 50%
, để tạo một ảnh động quay lại vị trí bắt đầu lặp đi lặp lại!
@keyframes progress-loading {
50% {
background-position: left;
}
}
Nhắm đến từng trình duyệt
Không phải trình duyệt nào cũng cho phép tạo phần tử giả trên chính phần tử <progress>
hoặc cho phép tạo ảnh động cho thanh tiến trình. Có nhiều trình duyệt hỗ trợ tạo ảnh động cho bản nhạc hơn là phần tử giả, vì vậy, tôi nâng cấp từ phần tử giả làm cơ sở thành thanh tạo ảnh động.
Phần tử giả lập Chromium
Chromium cho phép phần tử giả: ::after
được sử dụng với một vị trí để bao phủ phần tử. Các thuộc tính tuỳ chỉnh không xác định được sử dụng và ảnh động quay lại và đi tiếp hoạt động rất tốt.
progress:indeterminate::after {
content: "";
inset: 0;
position: absolute;
background: var(--_indeterminate-track);
background-size: var(--_indeterminate-track-size);
background-position: right;
animation: var(--_indeterminate-track-animation);
}
Thanh tiến trình của Safari
Đối với Safari, các thuộc tính tuỳ chỉnh và ảnh động được áp dụng cho thanh tiến trình phần tử giả:
progress:indeterminate::-webkit-progress-bar {
background: var(--_indeterminate-track);
background-size: var(--_indeterminate-track-size);
background-position: right;
animation: var(--_indeterminate-track-animation);
}
Thanh tiến trình của Firefox
Đối với Firefox, các thuộc tính tuỳ chỉnh và ảnh động cũng được áp dụng cho thanh tiến trình phần tử giả:
progress:indeterminate::-moz-progress-bar {
background: var(--_indeterminate-track);
background-size: var(--_indeterminate-track-size);
background-position: right;
animation: var(--_indeterminate-track-animation);
}
JavaScript
JavaScript đóng vai trò quan trọng với phần tử <progress>
. Phương thức này kiểm soát giá trị được gửi đến phần tử và đảm bảo có đủ thông tin trong tài liệu cho trình đọc màn hình.
const state = {
val: null
}
Bản minh hoạ cung cấp các nút để kiểm soát tiến trình; các nút này sẽ cập nhật state.val
rồi gọi một hàm để cập nhật DOM.
document.querySelector('#complete').addEventListener('click', e => {
state.val = 1
setProgress()
})
setProgress()
Đây là hàm diễn ra quá trình điều phối giao diện người dùng/trải nghiệm người dùng. Bắt đầu bằng cách tạo hàm setProgress()
. Bạn không cần tham số nào vì lớp này có quyền truy cập vào đối tượng state
, phần tử tiến trình và vùng <main>
.
const setProgress = () => {
}
Đặt trạng thái tải trên vùng <main>
Tuỳ thuộc vào việc tiến trình có hoàn tất hay không, phần tử <main>
liên quan cần được cập nhật thuộc tính aria-busy
:
const setProgress = () => {
zone.setAttribute('aria-busy', state.val < 1)
}
Xoá các thuộc tính nếu không xác định được lượng tải
Nếu giá trị không xác định hoặc chưa được đặt, null
trong trường hợp sử dụng này, hãy xoá các thuộc tính value
và aria-valuenow
. Thao tác này sẽ chuyển <progress>
thành không xác định.
const setProgress = () => {
zone.setAttribute('aria-busy', state.val < 1)
if (state.val === null) {
progress.removeAttribute('aria-valuenow')
progress.removeAttribute('value')
progress.focus()
return
}
}
Khắc phục vấn đề về toán thập phân JavaScript
Vì tôi chọn sử dụng tiến trình mặc định tối đa là 1, nên các hàm tăng và giảm trong bản minh hoạ sử dụng toán học thập phân. JavaScript và các ngôn ngữ khác không phải lúc nào cũng làm tốt việc đó.
Dưới đây là hàm roundDecimals()
sẽ cắt bớt phần thừa khỏi kết quả toán học:
const roundDecimals = (val, places) =>
+(Math.round(val + "e+" + places) + "e-" + places)
Làm tròn giá trị để có thể trình bày và dễ đọc:
const setProgress = () => {
zone.setAttribute('aria-busy', state.val < 1)
if (state.val === null) {
progress.removeAttribute('aria-valuenow')
progress.removeAttribute('value')
progress.focus()
return
}
const val = roundDecimals(state.val, 2)
const valPercent = val * 100 + "%"
}
Đặt giá trị cho trình đọc màn hình và trạng thái trình duyệt
Giá trị này được sử dụng ở 3 vị trí trong DOM:
- Thuộc tính
value
của phần tử<progress>
. - Thuộc tính
aria-valuenow
. - Nội dung văn bản bên trong
<progress>
.
const setProgress = () => {
zone.setAttribute('aria-busy', state.val < 1)
if (state.val === null) {
progress.removeAttribute('aria-valuenow')
progress.removeAttribute('value')
progress.focus()
return
}
const val = roundDecimals(state.val, 2)
const valPercent = val * 100 + "%"
progress.value = val
progress.setAttribute('aria-valuenow', valPercent)
progress.innerText = valPercent
}
Đặt tiêu điểm vào tiến trình
Khi các giá trị được cập nhật, người dùng bình thường sẽ thấy tiến trình thay đổi, nhưng người dùng trình đọc màn hình chưa được thông báo về thay đổi. Đặt tiêu điểm vào phần tử <progress>
và trình duyệt sẽ thông báo về nội dung cập nhật!
const setProgress = () => {
zone.setAttribute('aria-busy', state.val < 1)
if (state.val === null) {
progress.removeAttribute('aria-valuenow')
progress.removeAttribute('value')
progress.focus()
return
}
const val = roundDecimals(state.val, 2)
const valPercent = val * 100 + "%"
progress.value = val
progress.setAttribute('aria-valuenow', valPercent)
progress.innerText = valPercent
progress.focus()
}
Kết luận
Giờ thì bạn đã biết cách tôi làm, còn bạn thì sao‽ 🙂
Chắc chắn là tôi muốn thực hiện một vài thay đổi nếu có cơ hội. Tôi nghĩ rằng bạn có thể dọn dẹp thành phần hiện tại và thử tạo một thành phần khác mà không bị giới hạn kiểu lớp giả của phần tử <progress>
. Bạn nên khám phá tính năng này!
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
- Varun KS – nguồn và bản minh hoạ