Tổng quan cơ bản về cách tạo phần tử tuỳ chỉnh cho chú giải công cụ thích ứng màu sắc 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 một phần tử tuỳ chỉnh <tool-tip>
thích ứng màu sắc và dễ tiếp cận. Hãy xem thử bản minh hoạ và xem nguồn!
Nếu bạn thích video, đây là phiên bản YouTube của bài đăng này:
Tổng quan
Phần chú thích là một lớp phủ không theo phương thức, không chặn, không tương tác, có chứa thông tin bổ sung cho giao diện người dùng. Theo mặc định, thành phần này sẽ bị ẩn và không bị ẩn khi một phần tử liên kết được di chuột hoặc lấy làm tâm điểm. Bạn không thể chọn hoặc tương tác trực tiếp với chú giải công cụ. Chú giải công cụ không phải là phần thay thế cho nhãn hoặc thông tin có giá trị cao khác, người dùng phải có thể hoàn thành đầy đủ tác vụ mà không cần chú thích.
Bật/tắt chú thích so với Chú giải công cụ
Giống như nhiều thành phần, có nhiều mô tả khác nhau về chú giải công cụ, ví dụ: trong MDN, WAI ARIA, Sarah Higley và Thành phần bao hàm. Tôi thích cách tách biệt giữa chú giải công cụ và nút bật/tắt. Phần chú thích nên chứa thông tin bổ sung không mang tính tương tác, còn phần chú thích có thể chứa thông tin tương tác và thông tin quan trọng. Lý do chính của việc phân chia là khả năng hỗ trợ tiếp cận, dự kiến người dùng sẽ chuyển đến cửa sổ bật lên như thế nào cũng như có quyền truy cập vào thông tin và các nút trong đó. Bật/tắt mẹo sẽ nhanh chóng trở nên phức tạp.
Dưới đây là video về mẹo bật/tắt trên trang web Designcember; một lớp phủ có tính năng tương tác mà người dùng có thể ghim và khám phá, sau đó đóng bằng phím tắt ( sáng) hoặc phím Escape:
Thử thách GUI này đi theo hướng dẫn về chú giải công cụ, nhằm tìm cách làm hầu hết mọi thứ bằng CSS và sau đây là cách tạo nó.
Markup (note: đây là tên ứng dụng)
Tôi đã chọn sử dụng phần tử tuỳ chỉnh <tool-tip>
. Tác giả không cần đưa phần tử tuỳ chỉnh vào thành phần web nếu họ không muốn. Trình duyệt sẽ coi <foo-bar>
giống như <div>
. Bạn có thể nghĩ đến một phần tử tuỳ chỉnh như
tên lớp ít cụ thể hơn. Không liên quan đến JavaScript.
<tool-tip>A tooltip</tool-tip>
Đây giống như một div với một số văn bản bên trong. Chúng ta có thể liên kết cây hỗ trợ tiếp cận của các trình đọc màn hình có hỗ trợ bằng cách thêm [role="tooltip"]
.
<tool-tip role="tooltip">A tooltip</tool-tip>
Còn đối với trình đọc màn hình, văn bản này được nhận dạng là chú giải công cụ. Hãy xem trong ví dụ sau đây về cách mà phần tử đường liên kết đầu tiên có một phần tử chú thích được nhận dạng trong cây còn phần tử thứ hai thì không? Mục thứ hai không có vai trò này. Trong phần kiểu, chúng ta sẽ cải thiện chế độ xem dạng cây này.
Tiếp theo, chúng ta cần đặt phần chú thích để không thể làm tâm điểm. Nếu trình đọc màn hình không hiểu vai trò của chú giải công cụ, thì điều này sẽ cho phép người dùng tập trung vào <tool-tip>
để đọc nội dung và trải nghiệm người dùng không cần đến điều này. Trình đọc màn hình sẽ thêm nội dung vào phần tử mẹ và do đó, bạn không cần lấy tiêu điểm để truy cập được. Tại đây, chúng ta có thể sử dụng inert
để đảm bảo không người dùng nào
vô tình tìm thấy nội dung chú giải công cụ này trong luồng thẻ của họ:
<tool-tip inert role="tooltip">A tooltip</tool-tip>
Sau đó, tôi chọn sử dụng các thuộc tính làm giao diện để chỉ định vị trí của chú giải công cụ. Theo mặc định, tất cả <tool-tip>
sẽ giả định vị trí "trên cùng", nhưng bạn có thể tuỳ chỉnh vị trí này trên một phần tử bằng cách thêm tip-position
:
<tool-tip role="tooltip" tip-position="right ">A tooltip</tool-tip>
Tôi thường sử dụng các thuộc tính thay vì lớp cho những mục như thế này để <tool-tip>
không thể được gán nhiều vị trí cùng một lúc.
Có thể chỉ có một hoặc không có.
Cuối cùng, hãy đặt các phần tử <tool-tip>
vào bên trong phần tử bạn muốn cung cấp chú giải công cụ. Ở đây, tôi chia sẻ văn bản alt
với người dùng mắt bằng cách đặt hình ảnh và <tool-tip>
bên trong phần tử <picture>
:
<picture>
<img alt="The GUI Challenges skull logo" width="100" src="...">
<tool-tip role="tooltip" tip-position="bottom">
The <b>GUI Challenges</b> skull logo
</tool-tip>
</picture>
Ở đây, tôi đặt <tool-tip>
bên trong phần tử <abbr>
:
<p>
The <abbr>HTML <tool-tip role="tooltip" tip-position="top">Hyper Text Markup Language</tool-tip></abbr> abbr element.
</p>
Hỗ trợ tiếp cận
Vì tôi đã chọn xây dựng chú giải công cụ chứ không phải bật/tắt nên phần này sẽ đơn giản hơn nhiều. Trước tiên, hãy để tôi phác thảo trải nghiệm người dùng mong muốn:
- Trong không gian hạn chế hoặc giao diện lộn xộn, hãy ẩn các thông báo bổ sung.
- Khi người dùng di chuột, lấy tiêu điểm hoặc sử dụng thao tác chạm để tương tác với một phần tử, thông báo sẽ hiển thị.
- Khi thao tác di chuột, tiêu điểm hoặc thao tác chạm kết thúc, hãy ẩn tin nhắn trở lại.
- Cuối cùng, hãy đảm bảo mọi chuyển động sẽ giảm đi nếu người dùng đã chỉ định một lựa chọn ưu tiên là giảm chuyển động.
Mục tiêu của chúng tôi là thông báo bổ sung theo yêu cầu. Người dùng chuột hoặc bàn phím khi nhìn thấy có thể di chuột để hiển thị tin nhắn và đọc tin nhắn bằng mắt. Người dùng trình đọc màn hình không khiếm thị có thể tập trung để hiển thị tin nhắn, nhận được tin nhắn rõ ràng thông qua công cụ của họ.
Trong phần trước, chúng ta đã đề cập đến cây hỗ trợ tiếp cận, vai trò và giá trị không hoạt động của chú giải công cụ. Việc còn lại là kiểm thử và xác minh trải nghiệm người dùng hiển thị thông báo trong chú giải công cụ cho người dùng một cách phù hợp. Trong quá trình thử nghiệm, chúng tôi chưa rõ phần nào của thông báo âm thanh là phần chú thích. Bạn cũng có thể thấy văn bản này trong khi gỡ lỗi trong cây hỗ trợ tiếp cận. Văn bản liên kết "top" (trên cùng) sẽ được chạy cùng nhau mà không do dự, với "Look, tooltips!". Trình đọc màn hình không làm hỏng hoặc xác định văn bản là nội dung trong chú giải công cụ.
Thêm một phần tử giả chỉ dành cho trình đọc màn hình vào <tool-tip>
và chúng ta có thể thêm văn bản lời nhắc riêng cho người dùng không nhìn thấy.
&::before {
content: "; Has tooltip: ";
clip: rect(1px, 1px, 1px, 1px);
clip-path: inset(50%);
height: 1px;
width: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
}
Dưới đây là cây hỗ trợ tiếp cận đã cập nhật, hiện có dấu chấm phẩy sau văn bản đường liên kết và lời nhắc cho chú giải công cụ "Có chú giải công cụ: ".
Giờ đây, khi người dùng trình đọc màn hình lấy tiêu điểm đường liên kết, thông báo sẽ hiển thị "top" (trên cùng) và tạm dừng một chút, sau đó thông báo "has tooltip: Look, tooltips" (có chú giải công cụ: xem, chú giải công cụ). Điều này sẽ cung cấp cho người dùng trình đọc màn hình một vài gợi ý thú vị về trải nghiệm người dùng. Sự do dự giúp phân tách tốt giữa văn bản liên kết và chú thích. Ngoài ra, khi "có chú giải công cụ" được thông báo, người dùng trình đọc màn hình có thể dễ dàng huỷ chú thích nếu họ đã từng nghe trước đó. Thao tác này rất gợi nhớ việc di chuột và di chuột nhanh chóng, vì bạn đã xem thông báo bổ sung. Cảm giác như trải nghiệm người dùng tương đương.
Kiểu
Phần tử <tool-tip>
sẽ là phần tử con của phần tử đại diện cho thông điệp bổ sung, vì vậy, trước tiên, hãy bắt đầu với các yếu tố cần thiết cho hiệu ứng lớp phủ. Lấy trường hợp này ra khỏi quy trình tài liệu bằng position absolute
:
tool-tip {
position: absolute;
z-index: 1;
}
Nếu thành phần mẹ không phải là ngữ cảnh xếp chồng, thì chú giải công cụ sẽ tự định vị ở vị trí gần nhất, không phải là điều chúng ta muốn. Có một bộ chọn mới trên khối có thể hữu ích, đó là :has()
:
:has(> tool-tip) {
position: relative;
}
Đừng lo lắng quá nhiều về khả năng hỗ trợ của trình duyệt. Trước tiên, hãy nhớ rằng các chú giải công cụ này
là thông tin bổ sung. Nếu chúng không hoạt động thì cũng không sao. Thứ hai, trong phần JavaScript, chúng ta sẽ triển khai một tập lệnh để polyfill chức năng chúng ta cần cho các trình duyệt không hỗ trợ :has()
.
Tiếp theo, hãy làm cho chú giải công cụ không mang tính tương tác để chúng không đánh cắp các sự kiện con trỏ từ phần tử mẹ:
tool-tip {
…
pointer-events: none;
user-select: none;
}
Sau đó, hãy ẩn chú giải công cụ bằng độ mờ để chúng ta có thể chuyển đổi chú giải công cụ bằng tính năng làm mờ:
tool-tip {
opacity: 0;
}
:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
opacity: 1;
}
:is()
và :has()
sẽ thực hiện phần công việc khó khăn ở đây, giúp tool-tip
chứa các phần tử mẹ nhận biết được khả năng tương tác của người dùng khi chuyển đổi chế độ hiển thị của chú giải công cụ con. Người dùng chuột có thể di chuột, người dùng bàn phím và trình đọc màn hình có thể lấy tiêu điểm, còn người dùng chạm vào có thể nhấn.
Khi lớp phủ hiện và ẩn hoạt động với người dùng bình thường, đã đến lúc thêm một số kiểu để tuỳ chỉnh giao diện, định vị và thêm hình tam giác vào bong bóng trò chuyện. Các kiểu sau bắt đầu sử dụng các thuộc tính tuỳ chỉnh, xây dựng dựa trên vị trí hiện tại của chúng ta, đồng thời thêm bóng, kiểu chữ và màu sắc để trông giống như một chú giải công cụ nổi:
tool-tip {
--_p-inline: 1.5ch;
--_p-block: .75ch;
--_triangle-size: 7px;
--_bg: hsl(0 0% 20%);
--_shadow-alpha: 50%;
--_bottom-tip: conic-gradient(from -30deg at bottom, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) bottom / 100% 50% no-repeat;
--_top-tip: conic-gradient(from 150deg at top, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) top / 100% 50% no-repeat;
--_right-tip: conic-gradient(from -120deg at right, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) right / 50% 100% no-repeat;
--_left-tip: conic-gradient(from 60deg at left, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) left / 50% 100% no-repeat;
pointer-events: none;
user-select: none;
opacity: 0;
transform: translateX(var(--_x, 0)) translateY(var(--_y, 0));
transition: opacity .2s ease, transform .2s ease;
position: absolute;
z-index: 1;
inline-size: max-content;
max-inline-size: 25ch;
text-align: start;
font-size: 1rem;
font-weight: normal;
line-height: normal;
line-height: initial;
padding: var(--_p-block) var(--_p-inline);
margin: 0;
border-radius: 5px;
background: var(--_bg);
color: CanvasText;
will-change: filter;
filter:
drop-shadow(0 3px 3px hsl(0 0% 0% / var(--_shadow-alpha)))
drop-shadow(0 12px 12px hsl(0 0% 0% / var(--_shadow-alpha)));
}
/* create a stacking context for elements with > tool-tips */
:has(> tool-tip) {
position: relative;
}
/* when those parent elements have focus, hover, etc */
:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
opacity: 1;
transition-delay: 200ms;
}
/* prepend some prose for screen readers only */
tool-tip::before {
content: "; Has tooltip: ";
clip: rect(1px, 1px, 1px, 1px);
clip-path: inset(50%);
height: 1px;
width: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
}
/* tooltip shape is a pseudo element so we can cast a shadow */
tool-tip::after {
content: "";
background: var(--_bg);
position: absolute;
z-index: -1;
inset: 0;
mask: var(--_tip);
}
/* top tooltip styles */
tool-tip:is(
[tip-position="top"],
[tip-position="block-start"],
:not([tip-position]),
[tip-position="bottom"],
[tip-position="block-end"]
) {
text-align: center;
}
Điều chỉnh giao diện
Chú giải công cụ chỉ có một vài màu sắc để quản lý vì màu văn bản được kế thừa từ trang thông qua từ khoá hệ thống CanvasText
. Ngoài ra, vì đã tạo các thuộc tính tuỳ chỉnh để lưu trữ giá trị, nên chúng ta chỉ có thể cập nhật các thuộc tính tuỳ chỉnh đó và cho phép giao diện xử lý phần còn lại:
@media (prefers-color-scheme: light) {
tool-tip {
--_bg: white;
--_shadow-alpha: 15%;
}
}
Đối với giao diện sáng, chúng tôi điều chỉnh nền thành màu trắng và làm cho bóng trở nên bớt mạnh bằng cách điều chỉnh độ mờ của nền.
Phải sang trái
Để hỗ trợ chế độ đọc từ phải sang trái, thuộc tính tuỳ chỉnh sẽ lưu trữ giá trị của hướng tài liệu thành giá trị -1 hoặc 1 tương ứng.
tool-tip {
--isRTL: -1;
}
tool-tip:dir(rtl) {
--isRTL: 1;
}
Công cụ này có thể được dùng để hỗ trợ việc đặt chú thích:
tool-tip[tip-position="top"]) {
--_x: calc(50% * var(--isRTL));
}
Cũng như hỗ trợ vị trí của tam giác:
tool-tip[tip-position="right"]::after {
--_tip: var(--_left-tip);
}
tool-tip[tip-position="right"]:dir(rtl)::after {
--_tip: var(--_right-tip);
}
Cuối cùng, bạn cũng có thể dùng phép biến đổi logic trên translateX()
:
--_x: calc(var(--isRTL) * -3px * -1);
Vị trí phần chú thích
Đặt chú giải công cụ một cách hợp lý với các thuộc tính inset-block
hoặc inset-inline
để xử lý cả vị trí của chú giải công cụ thực tế và vị trí logic. Đoạn mã sau đây cho biết cách mỗi vị trí trong số 4 vị trí đó được tạo kiểu cho cả hướng từ trái sang phải và hướng từ phải sang trái.
Căn chỉnh phần trên cùng và bắt đầu theo khối
tool-tip:is([tip-position="top"], [tip-position="block-start"], :not([tip-position])) {
inset-inline-start: 50%;
inset-block-end: calc(100% + var(--_p-block) + var(--_triangle-size));
--_x: calc(50% * var(--isRTL));
}
tool-tip:is([tip-position="top"], [tip-position="block-start"], :not([tip-position]))::after {
--_tip: var(--_bottom-tip);
inset-block-end: calc(var(--_triangle-size) * -1);
border-block-end: var(--_triangle-size) solid transparent;
}
Căn phải và cùng dòng
tool-tip:is([tip-position="right"], [tip-position="inline-end"]) {
inset-inline-start: calc(100% + var(--_p-inline) + var(--_triangle-size));
inset-block-end: 50%;
--_y: 50%;
}
tool-tip:is([tip-position="right"], [tip-position="inline-end"])::after {
--_tip: var(--_left-tip);
inset-inline-start: calc(var(--_triangle-size) * -1);
border-inline-start: var(--_triangle-size) solid transparent;
}
tool-tip:is([tip-position="right"], [tip-position="inline-end"]):dir(rtl)::after {
--_tip: var(--_right-tip);
}
Căn chỉnh dưới cùng và cuối khối
tool-tip:is([tip-position="bottom"], [tip-position="block-end"]) {
inset-inline-start: 50%;
inset-block-start: calc(100% + var(--_p-block) + var(--_triangle-size));
--_x: calc(50% * var(--isRTL));
}
tool-tip:is([tip-position="bottom"], [tip-position="block-end"])::after {
--_tip: var(--_top-tip);
inset-block-start: calc(var(--_triangle-size) * -1);
border-block-start: var(--_triangle-size) solid transparent;
}
Căn chỉnh trái và cùng dòng
tool-tip:is([tip-position="left"], [tip-position="inline-start"]) {
inset-inline-end: calc(100% + var(--_p-inline) + var(--_triangle-size));
inset-block-end: 50%;
--_y: 50%;
}
tool-tip:is([tip-position="left"], [tip-position="inline-start"])::after {
--_tip: var(--_right-tip);
inset-inline-end: calc(var(--_triangle-size) * -1);
border-inline-end: var(--_triangle-size) solid transparent;
}
tool-tip:is([tip-position="left"], [tip-position="inline-start"]):dir(rtl)::after {
--_tip: var(--_left-tip);
}
Hoạt ảnh
Hiện tại, chúng tôi chỉ mới bật chế độ hiển thị của phần chú thích. Trong phần này, trước tiên, chúng tôi sẽ tạo ảnh động về độ mờ cho tất cả người dùng, vì đây là quá trình chuyển đổi chuyển động giảm an toàn thường. Sau đó, chúng ta sẽ tạo ảnh động cho vị trí biến đổi để chú giải công cụ xuất hiện trượt ra từ phần tử mẹ.
Chuyển đổi mặc định an toàn và có ý nghĩa
Tạo kiểu cho phần tử chú giải công cụ thành độ mờ và biến đổi chuyển đổi, như sau:
tool-tip {
opacity: 0;
transform: translateX(var(--_x, 0)) translateY(var(--_y, 0));
transition: opacity .2s ease, transform .2s ease;
}
:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
opacity: 1;
transition-delay: 200ms;
}
Thêm chuyển động vào hiệu ứng chuyển cảnh
Đối với mỗi bên, một chú giải công cụ có thể xuất hiện. Nếu người dùng ổn định với chuyển động, hãy định vị nhẹ thuộc tính translationX bằng cách cho nó một khoảng cách nhỏ để di chuyển từ:
@media (prefers-reduced-motion: no-preference) {
:has(> tool-tip:is([tip-position="top"], [tip-position="block-start"], :not([tip-position]))):not(:hover):not(:focus-visible):not(:active) tool-tip {
--_y: 3px;
}
:has(> tool-tip:is([tip-position="right"], [tip-position="inline-end"])):not(:hover):not(:focus-visible):not(:active) tool-tip {
--_x: -3px;
}
:has(> tool-tip:is([tip-position="bottom"], [tip-position="block-end"])):not(:hover):not(:focus-visible):not(:active) tool-tip {
--_y: -3px;
}
:has(> tool-tip:is([tip-position="left"], [tip-position="inline-start"])):not(:hover):not(:focus-visible):not(:active) tool-tip {
--_x: 3px;
}
}
Lưu ý rằng thao tác này đang đặt trạng thái "out" do trạng thái "in" nằm ở translateX(0)
.
JavaScript
Theo tôi, JavaScript là không bắt buộc. Lý do là không có phần chú thích nào trong số này cần phải đọc để hoàn thành một thao tác trong giao diện người dùng. Nếu chú giải công cụ có lỗi hoàn toàn, thì cũng không sao. Điều này cũng có nghĩa là chúng tôi có thể xem chú thích
được cải tiến dần dần. Cuối cùng, mọi trình duyệt sẽ hỗ trợ :has()
và tập lệnh này có thể hoàn toàn biến mất.
Tập lệnh polyfill thực hiện hai việc và chỉ hoạt động nếu trình duyệt không hỗ trợ :has()
. Trước tiên, hãy kiểm tra xem :has()
có hỗ trợ không:
if (!CSS.supports('selector(:has(*))')) {
// do work
}
Tiếp theo, hãy tìm các phần tử mẹ của <tool-tip>
và đặt tên lớp cho các phần tử đó để sử dụng:
if (!CSS.supports('selector(:has(*))')) {
document.querySelectorAll('tool-tip').forEach(tooltip =>
tooltip.parentNode.classList.add('has_tool-tip'))
}
Tiếp theo, hãy chèn một tập hợp các kiểu sử dụng tên lớp đó, mô phỏng bộ chọn :has()
cho cùng một hành vi:
if (!CSS.supports('selector(:has(*))')) {
document.querySelectorAll('tool-tip').forEach(tooltip =>
tooltip.parentNode.classList.add('has_tool-tip'))
let styles = document.createElement('style')
styles.textContent = `
.has_tool-tip {
position: relative;
}
.has_tool-tip:is(:hover, :focus-visible, :active) > tool-tip {
opacity: 1;
transition-delay: 200ms;
}
`
document.head.appendChild(styles)
}
Vậy là xong, giờ đây tất cả trình duyệt sẽ sẵn sàng hiển thị phần chú thích nếu :has()
không được hỗ trợ.
Kết luận
Giờ bạn đã biết cách thực hiện điều đó, bạn sẽ làm như thế nào 🙂 Tôi thực sự đang mong chờ API popup
để giúp nút bật/tắt dễ dàng hơn, lớp trên cùng để không có cuộc chiến chỉ mục z và API anchor
để định vị mọi thứ trong cửa sổ tốt hơn. Cho đến lúc đó, tôi sẽ tạo
phần chú thích.
Hãy đa dạng hoá phương pháp tiếp cận của chúng ta 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ạ, đường liên kết tweet me và tôi sẽ thêm bản phối lại đó 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
Chưa có nội dung nào để xem ở đây.
Tài nguyên
- Mã nguồn trên GitHub