Thông tin tổng quan cơ bản về cách tạo một phần tử tuỳ chỉnh của chú giải công cụ có thể tiếp cận và thích ứng với màu sắc.
Trong bài đăng này, tôi muốn chia sẻ suy nghĩ của mình về cách tạo một phần tử tuỳ chỉnh <tool-tip>
có thể thích ứng với màu sắc và hỗ trợ tiếp cận. 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
Chú giải công cụ là một lớp phủ không phương thức, không chặn và không tương tá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 bị ẩn và sẽ xuất hiện khi một phần tử được liên kết được di chuột qua hoặc đượ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 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 toàn bộ nhiệm vụ của mình mà không cần chú giải công cụ.
Toggletip so với Tooltip
Giống như nhiều thành phần, có nhiều nội dung mô tả về chú giải công cụ, chẳng hạn như trong MDN, WAI ARIA, Sarah Higley và Thành phần bao gồm. Tôi thích việc phân tách giữa chú giải công cụ và chú giải bật/tắt. Chú giải công cụ phải chứa thông tin bổ sung không tương tác, trong khi chú giải bật/tắt có thể chứa thông tin tương tác và quan trọng. Lý do chính cho sự phân chia này là khả năng hỗ trợ tiếp cận, cách người dùng dự kiến sẽ chuyển đến cửa sổ bật lên và có quyền truy cập vào thông tin cũng như các nút trong đó. Toggletips nhanh chóng trở nên phức tạp.
Dưới đây là video về một mẹo bật/tắt trên trang web Designcember; một lớp phủ có tính tương tác mà người dùng có thể ghim để mở và khám phá, sau đó đóng bằng thao tác đóng bằng đèn hoặc phím Escape:
Thách thức về giao diện người dùng này đã đi theo hướng của chú giải công cụ, tìm cách thực hiện hầu hết mọi việc bằng CSS. Dưới đây là cách tạo chú giải công cụ.
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 phải tạo các phần tử tùy chỉnh thành thành phần web nếu không muốn. Trình duyệt sẽ xử lý <foo-bar>
giống như <div>
. Bạn có thể coi một phần tử tuỳ chỉnh như tên lớp có ít cụ thể hơn. Không có JavaScript nào liên quan.
<tool-tip>A tooltip</tool-tip>
Đây giống như một div có một số văn bản bên trong. Chúng ta có thể liên kết với cây hỗ trợ tiếp cận của các trình đọc màn hình có khả năng bằng cách thêm [role="tooltip"]
.
<tool-tip role="tooltip">A tooltip</tool-tip>
Giờ đây, với trình đọc màn hình, chú thích sẽ được nhận dạng là một chú giải công cụ. Hãy xem ví dụ sau đây để biết cách phần tử đường liên kết đầu tiên có phần tử chú giải công cụ được nhận dạng trong cây của phần tử đó và phần tử thứ hai không có phần tử chú giải công cụ. Người dùng thứ hai không có vai trò này. Trong phần kiểu, chúng ta sẽ cải thiện chế độ xem cây này.
Tiếp theo, chúng ta cần chú giải công cụ không thể lấy tiêu điểm. Nếu trình đọc màn hình không hiểu vai trò trong chú giải công cụ, thì người dùng sẽ có thể tập trung vào <tool-tip>
để đọc nội dung và trải nghiệm người dùng không cần như vậy. Trình đọc màn hình sẽ thêm nội dung vào phần tử mẹ, do đó, bạn không cần phải truy cập vào nội dung này. Ở đây, chúng ta có thể sử dụng inert
để đảm bảo không có người dùng nào vô tình tìm thấy nội dung chú thích 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í 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 trường hợp như thế này để <tool-tip>
không thể có nhiều vị trí được chỉ định cùng một lúc.
Chỉ có thể có một hoặc không có.
Cuối cùng, hãy đặt các phần tử <tool-tip>
bên trong phần tử mà 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 có thị lực bằng cách đặt mộ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 tạo chú giải công cụ chứ không phải chú giả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 trình bày trải nghiệm người dùng mà chúng ta mong muốn:
- Trong không gian bị hạn chế hoặc giao diện lộn xộn, hãy ẩn thông báo bổ sung.
- Khi người dùng di chuột, đặt 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ử, hãy hiển thị thông báo.
- Khi thao tác di chuột, lấy tiêu điểm hoặc chạm kết thúc, hãy ẩn lại thông báo.
- Cuối cùng, hãy đảm bảo mọi chuyển động đều được giảm nếu người dùng đã chỉ định một tuỳ chọn ưu tiên giảm chuyển động.
Mục tiêu của chúng tôi là cung cấp thông báo bổ sung theo yêu cầu. Người dùng chuột hoặc bàn phím bình thường có thể di chuột để hiển thị thông báo, đọc thông báo bằng mắt. Người dùng trình đọc màn hình khiếm thị có thể tập trung để hiển thị thông báo, nhận thông báo bằng âm thanh 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ò của chú giải công cụ và tính chất không hoạt động. 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 chú giải công cụ cho người dùng một cách phù hợp. Sau khi kiểm thử, không rõ phần nào của thông báo âm thanh là chú giải công cụ. Bạn cũng có thể thấy điều này trong khi gỡ lỗi trong cây hỗ trợ tiếp cận, văn bản đường liên kết của "top" (trên cùng) được chạy cùng nhau, không do dự, với "Look, tooltips!" (Nhìn kìa, chú giải công cụ!). Trình đọc màn hình không ngắt hoặc xác định văn bản là nội dung 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 của riêng mình cho người dùng khiếm thị.
&::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;
}
Bạn có thể thấy cây hỗ trợ tiếp cận đã cập nhật ở bên dưới. Cây này 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 đặt tiêu điểm vào đường liên kết, đường liên kết sẽ hiển thị là "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ụ: nhìn, chú giải công cụ). Điều này cung cấp cho người dùng trình đọc màn hình một số gợi ý về trải nghiệm người dùng. Độ trễ này tạo ra sự phân tách rõ ràng giữa văn bản liên kết và chú giải công cụ. Ngoài ra, khi thông báo "có chú giải công cụ" được đưa ra, người dùng trình đọc màn hình có thể dễ dàng huỷ thông báo đó nếu họ đã nghe thấy thông báo đó trước đây. Như bạn đã thấy thông báo bổ sung, điều này khiến người dùng liên tưởng đến việc di chuột và không di chuột một cách nhanh chóng. Đây là một trải nghiệm người dùng đồng nhất.
Kiểu
Phần tử <tool-tip>
sẽ là phần tử con của phần tử mà nó đạ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 thông tin cơ bản về hiệu ứng lớp phủ. Loại bỏ 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ị đến thành phần mẹ gần nhất, đây 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ể giúp bạn, :has()
:
:has(> tool-tip) {
position: relative;
}
Đừng quá lo lắng 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à bổ sung. Nếu 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 cần thiết cho các trình duyệt không hỗ trợ :has()
.
Tiếp theo, hãy làm cho chú thích không mang tính tương tác để 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 đó, ẩ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 hiệu ứng chuyển tiếp:
tool-tip {
opacity: 0;
}
:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
opacity: 1;
}
:is()
và :has()
sẽ đảm nhậ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 tương tác của người dùng để bật/tắt chế độ hiển thị của chú thích 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 và người dùng cảm ứng có thể nhấn.
Khi lớp phủ hiển thị và ẩn hoạt động cho người dùng có thị lực, đã đến lúc thêm một số kiểu để tạo 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 đây bắt đầu sử dụng thuộc tính tuỳ chỉnh, dựa trên vị trí chúng ta đã đạt được cho đến thời điểm hiện tại nhưng cũng 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ú thích chỉ có một vài màu để bạn 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ữ các giá trị, nên chúng ta chỉ có thể cập nhật các thuộc tính tuỳ chỉnh đó và để 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 ta điều chỉnh nền thành màu trắng và làm cho bóng mờ hơn bằng cách điều chỉnh độ mờ.
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ị hướng tài liệu vào giá trị tương ứng là -1 hoặc 1.
tool-tip {
--isRTL: -1;
}
tool-tip:dir(rtl) {
--isRTL: 1;
}
Bạn có thể dùng công cụ này để hỗ trợ việc định vị chú thích:
tool-tip[tip-position="top"]) {
--_x: calc(50% * var(--isRTL));
}
Cũng hỗ trợ trong trường hợp 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ể sử dụng hàm này cho các phép biến đổi logic trên translateX()
:
--_x: calc(var(--isRTL) * -3px * -1);
Vị trí chú giải công cụ
Hãy đặt chú thích hợp lý với các thuộc tính inset-block
hoặc inset-inline
để xử lý cả vị trí chú thích thực tế và logic. Mã sau đây cho biết cách tạo kiểu của từng vị trí trong số 4 vị trí theo cả hướng từ trái sang phải và từ phải sang trái.
Căn trên và căn đầu 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ăn chỉnh cuối cùng 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 trái và căn đầu 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
Cho đến nay, chúng ta chỉ bật/tắt chế độ hiển thị của chú giải công cụ. Trong phần này, trước tiên chúng ta sẽ tạo ảnh động độ mờ cho tất cả người dùng, vì đây là hiệu ứng chuyển đổi chuyển động giảm thường an toàn. Sau đó, chúng ta sẽ tạo ảnh động cho vị trí biến đổi để chú giải công cụ có vẻ như trượt ra khỏi phần tử mẹ.
Quá trình chuyển đổi mặc định an toàn và hiệu quả
Tạo kiểu cho phần tử chú thích để chuyển đổi độ mờ và biế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 đổi
Ở mỗi bên, một chú thích có thể xuất hiện, nếu người dùng đồng ý chuyển động, hãy đặt một chút thuộc tính translateX cho thuộc tính đó 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 đây là việc thiết lập trạng thái "out" (ngoài) vì trạng thái "in" (trong) nằm ở translateX(0)
.
JavaScript
Theo tôi, bạn không bắt buộc phải sử dụng JavaScript. Lý do là bạn không cần phải đọc bất kỳ chú giải công cụ nào trong số này để hoàn thành một tác vụ trong giao diện người dùng. Vì vậy, nếu chú thích hoàn toàn không thành công thì cũng không có vấn đề gì lớn. Điều này cũng có nghĩa là chúng tôi có thể coi chú thích là nâng cao dần dần. Cuối cùng, tất cả trình duyệt sẽ hỗ trợ :has()
và tập lệnh này có thể biến mất hoàn toàn.
Tập lệnh polyfill thực hiện hai việc và chỉ thực hiện nếu trình duyệt không hỗ trợ :has()
. Trước tiên, hãy kiểm tra khả năng hỗ trợ :has()
:
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, mọi trình duyệt sẽ đều sẵn sàng hiển thị chú thích nếu :has()
không được hỗ trợ.
Kết luận
Giờ thì bạn đã biết cách tôi thực hiện, còn bạn thì sao‽ 🙂 Tôi rất mong chờ API popup
để tạo các nút bật/tắt dễ dàng hơn, lớp trên cùng để không có các trận chiến về chỉ mục z và API anchor
để định vị các mục trong cửa sổ tốt hơn. Cho đến lúc đó, tôi sẽ tạo chú giải công cụ.
Hãy đa dạng hoá phương pháp tiếp cận và tìm hiểu tất cả các cách xây dựng ứng dụng trên web.
Tạo một bản minh hoạ, tweet cho tôi các đường liên kết và tôi sẽ thêm 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ó gì để xem ở đây.
Tài nguyên
- Mã nguồn trên GitHub