Thông tin tổng quan cơ bản về cách tạo một thanh tải có thể thích ứng 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 thích ứng màu và có thể truy cập bằng phần tử <progress>
. Hãy dùng thử bản minh hoạ và xem nguồn!
Nếu bạn thích xem video, thì đây là 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ề trạng thái hoàn tất. Phản hồi trực quan này rất hữu ích trong các trường hợp như: tiến trình điền 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 số lượng tiến trình không xác định nhưng công việc vẫn đang diễn ra.
Thử thách GUI này hoạt động với phần tử HTML <progress>
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, nhằm hiện đại hoá thành phần và giúp thành phần này phù hợp hơn trong các hệ thống thiết kế.

Markup (note: đây là tên ứng dụng)
Tôi chọn bao bọc phần tử <progress>
trong một <label>
để có thể bỏ qua các thuộc tính mối quan hệ rõ ràng để chuyển sang 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, nhờ đó các công nghệ trình đọc màn hình có thể chuyển thông tin đó 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, nên 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 giữ trong giới hạn từ 0 đến 1, chuyển đổi 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ởi một nhãn như sau:
<label>Loading progress<progress></progress></label>
Trong bản minh hoạ, tôi chọn chỉ thêm nhãn cho trình đọc màn hình.
Việc này được thực hiện bằng cách bao bọc văn bản nhãn trong một <span>
và áp dụng một số kiểu cho văn bản đó để văn bản này thực sự nằm ngoài màn hình:
<label>
<span class="sr-only">Loading progress</span>
<progress></progress>
</label>
Với CSS đi kèm sau đây từ 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 thị lực của bạn bình thường, 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à vùng 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. Cải thiện điều này bằng cách chỉ định 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.
Ngoài ra, hãy cho biết 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 đổi aria-busy
thành true
khi bắt đầu tác vụ và thành false
sau khi hoàn tất.
Thêm thuộc tính Aria
Mặc dù vai trò ngầm định của phần tử <progress>
là progressbar
, nhưng tôi đã đặt vai trò này một cách rõ ràng cho những trình duyệt không có vai trò ngầm định đó. Tôi cũng đã thêm thuộc tính indeterminate
để đặt rõ ràng phần tử vào trạng thái không xác định. Điều này rõ ràng hơn so với việc quan sát thấy phần tử không có value
được đặt.
<label>
Loading
<progress
indeterminate
role="progressbar"
aria-describedby="loading-zone"
tabindex="-1"
>unknown</progress>
</label>
Sử dụng tabindex="-1"
để làm cho phần tử tiến trình có thể lấy 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 biết tiến trình đã cập nhật được bao xa.
Kiểu
Phần tử tiến trình hơi khó tạo kiểu. Các phần tử HTML tích hợp có những phần ẩn đặc biệt mà bạn khó có thể chọn và thường chỉ cung cấp một số ít thuộc tính để thiết lập.
Bố cục
Các kiểu bố cục được thiết kế để cho phép một số linh hoạt về kích thước và vị trí nhãn của phần tử tiến trình. Một 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 trực quan 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 thay đổi để có thể thu hẹp và mở rộng theo không gian cần thiết trong thiết kế. Các kiểu tích hợp sẵn sẽ bị loại bỏ bằng cách đặt appearance
và border
thành none
. Điều này được thực hiện để phần tử có thể được chuẩn hoá 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ố khoa học để biểu thị một số lớn nên border-radius
luôn được làm tròn. Quy tắc 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 để tôi có thể đặt và quên (và giá trị này ngắn hơn 1000px
). Bạn cũng có thể dễ dàng tăng giá trị này nếu cần: chỉ cần thay đổi 3 thành 4, thì 1e4px
sẽ tương đương với 10000px
.
overflow: hidden
được 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 các giá trị border-radius
xuống phần tử theo dõi và phần tử điền vào phần tử theo dõi; nhưng điều này 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 lần lặp lại 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 các hoạt ảnh 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 một tín hiệu trực quan bổ sung đẹp mắt 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à có khả năng 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ể tạo 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 sử dụng một phần tử <progress>
thích ứng với giao diện tối và sáng cho trang web của bạn, bạn chỉ cần color-scheme
.
progress {
color-scheme: light dark;
}
Màu đã điền của tiến trình một thành phần
Để tạo sắc thái cho một phần tử <progress>
, hãy dùng accent-color
.
progress {
accent-color: rebeccapurple;
}
Lưu ý rằng 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 đảm bảo độ tương phản phù hợp: khá hay.
Tuỳ chỉnh hoàn toàn màu sáng và tối
Đặt hai thuộc tính tuỳ chỉnh trên phần tử <progress>
, một cho màu của đường và một cho màu tiến trình của đường. Trong truy vấn nội dung nghe nhìn prefers-color-scheme
, hãy cung cấp các giá trị màu mới cho tiến trình của phụ đề và phụ đề.
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 đã đặt chỉ mục thẻ âm cho phần tử để có thể lập trình tiêu điểm. Sử dụng :focus-visible
để tuỳ chỉnh tiêu điểm nhằm chọn sử dụng kiểu vòng tiêu điểm thông minh hơn. Với lựa chọn này, thao tác nhấp chuột và tiêu điểm sẽ không hiển thị vòng tiêu điểm, nhưng thao tác nhấp bằng bàn phím sẽ hiển thị. Video trên YouTube này sẽ giải thích chi tiết hơn và bạn nên xem video 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 một 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 được tạo thành từ một số phần tử con được hiển thị thông qua bộ chọn giả CSS. Công cụ cho nhà phát triển của Chrome sẽ cho bạn thấy những phần tử này nếu bạn bật chế độ cài đặt:
- Nhấp chuột phải vào trang của bạn 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 Cài đặt ở góc trên cùng bên phải của cửa sổ Công cụ cho nhà phát triển.
- Trong tiêu đề Elements (Phần tử), hãy tìm và đánh dấu vào hộp Show user agent shadow DOM (Hiện shadow DOM 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 nhóm nhỏ CSS. Hiện tại, hãy đặt background-color
bằng cách sử dụ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 tô màu cho đường dẫn.
/* Firefox */
progress[value]::-moz-progress-bar {
background-color: var(--_progress);
}
Lưu ý rằng Firefox có một màu đường đánh dấu được đặt từ accent-color
trong khi iOS Safari có một đường đánh dấu màu xanh dương nhạt. Điều này cũng xảy ra ở chế độ tối: Firefox có một đường kẻ tối nhưng không có màu tuỳ chỉnh mà chúng ta đã đặt và đường kẻ 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ả tích hợp của trình duyệt, thường thì bạn chỉ được phép sử dụng một số thuộc tính CSS.
Tạo ảnh động cho đường chạy
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 trên Chromium nhưng không hoạt động trên 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ẽ sáng tạo hơn một chút để có thể cung cấp một ảnh động. Một phần tử giả được tạo cho Chromium và một hiệu ứng chuyển màu được áp dụng để tạo hiệu ứng chuyển độ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 việc tôi yêu thích là chỉ cần đặt tên cho một giá trị CSS có vẻ như là phép thuật. Sau đây là một linear-gradient
khá phức tạp, nhưng có một cái tên hay. Người dùng có thể hiểu rõ mục đích và trường hợp sử dụng của ứng dụ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;
}
Các thuộc tính tuỳ chỉnh cũng sẽ giúp mã luôn DRY (không lặp lại) 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 với nhau.
Khung hình chính
Mục tiêu là một ảnh động vô hạn di chuyển qua lại. Các keyframe 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í ban đầ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. Nhiều trình duyệt hỗ trợ việc tạo hiệu ứng cho rãnh 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 các thanh có hiệu ứng chuyển động.
Phần tử giả Chromium
Chromium cho phép sử dụng phần tử giả: ::after
với một vị trí để che phủ phần tử. Các thuộc tính tuỳ chỉnh không xác định được dùng và ảnh động qua lại 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à một ả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>
. Thuộc tính 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 nơi diễn ra quá trình phối hợp 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()
. Không cần tham số nào vì thành phần 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 đã hoàn tất hay chưa, phần tử <main>
có liên quan cần được cập nhật thành 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 số tiền đang tải
Nếu giá trị không xác định hoặc chưa được đặt, null
trong cách 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>
sang trạng thái 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ề phép toán số thập phân trong JavaScript
Vì tôi chọn giữ nguyên giá trị tối đa mặc định của tiến trình là 1, nên các hàm tăng và giảm của bản minh hoạ sẽ sử dụng phép toán số thập phân. JavaScript và các ngôn ngữ khác không phải lúc nào cũng phù hợp với điều đó.
Sau đây là hàm roundDecimals()
sẽ cắt bỏ phần thừa của kết quả phép tính:
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 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ập trung vào tiến trình
Sau khi các giá trị được cập nhật, người dùng có thị lực sẽ thấy sự thay đổi về tiến trình, nhưng người dùng trình đọc màn hình vẫn chưa nhận được thông báo về sự thay đổi này. Tập trung 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ờ bạn đã biết cách tôi làm, vậy bạn sẽ làm như thế nào‽ 🙂
Chắc chắn là có một vài thay đổi mà tôi muốn thực hiện nếu có cơ hội khác. Tôi nghĩ rằng 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ông có các hạn chế về kiểu giả lớp của phần tử <progress>
. Bạn nên khám phá!
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 một bản minh hoạ, gửi đường liên kết cho tôi qua Twitter và tôi sẽ thêm bản minh hoạ đó vào phần bả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ạ