색상에 적응하고 접근성이 좋은 툴팁 맞춤 요소를 빌드하는 방법에 관한 기본 개요입니다.
이 게시물에서는 색상에 적응하고 접근성이 좋은 <tool-tip>
맞춤 요소를 빌드하는 방법에 관한 생각을 공유하고자 합니다. 데모를 사용해 보고 소스를 확인하세요.
동영상을 선호하는 경우 이 게시물의 YouTube 버전을 확인하세요.
개요
툴팁은 사용자 인터페이스에 대한 보충 정보를 포함하는 모달이 아니고 차단되지 않으며 상호작용이 불가능한 오버레이입니다. 기본적으로 숨겨져 있으며 연결된 요소에 마우스를 가져가거나 요소가 포커스되면 숨김 해제됩니다. 툴팁은 직접 선택하거나 상호작용할 수 없습니다. 도움말은 라벨이나 기타 가치 있는 정보를 대체하지 않습니다. 사용자는 도움말 없이도 작업을 완전히 완료할 수 있어야 합니다.

잘못된 예: 라벨 대신 도움말을 사용함
토글팁과 도움말 비교
많은 구성요소와 마찬가지로 툴팁이 무엇인지에 대한 설명은 MDN, WAI ARIA, Sarah Higley, Inclusive Components 등에서 다양하게 제공됩니다. 도움말과 토글팁 간의 분리가 마음에 듭니다. 도움말에는 상호작용이 없는 보충 정보가 포함되어야 하지만, 토글팁에는 상호작용과 중요한 정보가 포함될 수 있습니다. 이러한 분할의 주요 이유는 접근성입니다. 사용자가 팝업으로 이동하고 팝업 내의 정보와 버튼에 액세스할 수 있는 방법이 무엇인지 고려해야 합니다. 토글팁은 빠르게 복잡해집니다.
Designcember 사이트의 토글 팁 동영상입니다. 사용자가 고정하여 열고 탐색한 다음, 가벼운 닫기 또는 Esc 키로 닫을 수 있는 상호작용이 있는 오버레이입니다.
이 GUI 챌린지에서는 툴팁을 사용해 CSS로 거의 모든 작업을 수행하려고 합니다. 빌드 방법은 다음과 같습니다.
마크업
맞춤 요소 <tool-tip>
를 사용하기로 했습니다. 원하는 경우 맞춤 요소를 웹 구성요소로 만들지 않아도 됩니다. 브라우저는 <foo-bar>
를 <div>
와 똑같이 취급합니다. 맞춤 요소를 구체성이 낮은 클래스 이름으로 생각할 수 있습니다. JavaScript는 사용되지 않습니다.
<tool-tip>A tooltip</tool-tip>
이는 텍스트가 포함된 div와 같습니다. [role="tooltip"]
를 추가하여 지원되는 스크린 리더의 접근성 트리에 연결할 수 있습니다.
<tool-tip role="tooltip">A tooltip</tool-tip>
이제 스크린 리더에서 도움말로 인식합니다. 다음 예에서 첫 번째 링크 요소에는 트리에서 인식된 도움말 요소가 있고 두 번째 링크 요소에는 없는 것을 확인하세요. 두 번째 사용자에게는 역할이 없습니다. 스타일 섹션에서는 이 트리 뷰를 개선합니다.
다음으로 도움말에 포커스를 둘 수 없어야 합니다. 스크린 리더가 도움말 역할을 이해하지 못하면 사용자가 <tool-tip>
에 포커스를 맞춰 콘텐츠를 읽을 수 있지만 사용자 환경에는 필요하지 않습니다. 스크린 리더는 콘텐츠를 상위 요소에 추가하므로 접근성을 위해 포커스를 설정할 필요가 없습니다. 여기서는 inert
을 사용하여 사용자가 탭 흐름에서 이 도움말 콘텐츠를 실수로 찾지 않도록 할 수 있습니다.
<tool-tip inert role="tooltip">A tooltip</tool-tip>
그런 다음 속성을 사용하여 도움말의 위치를 지정하는 인터페이스를 사용하기로 했습니다. 기본적으로 모든 <tool-tip>
는 '상단' 위치를 가정하지만 tip-position
를 추가하여 요소의 위치를 맞춤설정할 수 있습니다.
<tool-tip role="tooltip" tip-position="right ">A tooltip</tool-tip>
<tool-tip>
에 동시에 여러 위치가 할당되지 않도록 하기 위해 클래스 대신 속성을 사용하는 경향이 있습니다.
하나만 있거나 없을 수 있습니다.
마지막으로 툴팁을 제공하려는 요소 안에 <tool-tip>
요소를 배치합니다. 여기에서는 <picture>
요소 내에 이미지와 <tool-tip>
를 배치하여 시각 장애가 없는 사용자와 alt
텍스트를 공유합니다.
<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>
다음은 <abbr>
요소 내부에 <tool-tip>
를 배치한 것입니다.
<p>
The <abbr>HTML <tool-tip role="tooltip" tip-position="top">Hyper Text Markup Language</tool-tip></abbr> abbr element.
</p>
접근성
토글팁이 아닌 도움말을 빌드하기로 선택했으므로 이 섹션은 훨씬 간단합니다. 먼저 Google이 원하는 사용자 환경을 간략하게 설명해 드리겠습니다.
- 제약이 있는 공간이나 복잡한 인터페이스에서는 보조 메시지를 숨깁니다.
- 사용자가 마우스를 가져가거나, 포커스를 맞추거나, 터치를 사용하여 요소와 상호작용할 때 메시지를 표시합니다.
- 마우스 오버, 포커스 또는 터치가 끝나면 메시지를 다시 숨깁니다.
- 마지막으로, 사용자가 동작 줄이기 환경설정을 지정한 경우 동작이 줄어들도록 해야 합니다.
Google의 목표는 주문형 보충 메시지입니다. 시각 장애가 없는 마우스 또는 키보드 사용자는 마우스를 가져가 메시지를 표시하고 눈으로 읽을 수 있습니다. 시각장애인 스크린 리더 사용자는 포커스를 맞춰 메시지를 표시하고 도구를 통해 음성으로 메시지를 받을 수 있습니다.

이전 섹션에서는 접근성 트리, 도움말 역할, inert를 다뤘습니다. 이제 이를 테스트하고 사용자 환경에서 사용자에게 도움말 메시지가 적절하게 표시되는지 확인하면 됩니다. 테스트 결과, 청각적 메시지의 어느 부분이 도움말인지 명확하지 않습니다. 접근성 트리에서 디버깅하는 동안에도 확인할 수 있습니다. 'top'의 링크 텍스트가 'Look, tooltips!'와 함께 망설임 없이 실행됩니다. 스크린 리더가 텍스트를 나누거나 툴팁 콘텐츠로 식별하지 않습니다.
<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;
}
아래에서 업데이트된 접근성 트리를 확인할 수 있습니다. 이제 링크 텍스트 뒤에 세미콜론이 있고 도움말 프롬프트 '도움말 있음: '이 있습니다.
이제 스크린 리더 사용자가 링크에 포커스를 두면 '상단'이라고 말하고 잠시 멈춘 다음 '도움말 있음: 보세요, 도움말'이라고 알립니다. 이렇게 하면 스크린 리더 사용자에게 몇 가지 유용한 UX 힌트가 제공됩니다. 지연 시간으로 인해 링크 텍스트와 도움말이 적절하게 분리됩니다. 또한 '도움말 있음'이 공지되면 스크린 리더 사용자는 이전에 들은 적이 있는 경우 쉽게 취소할 수 있습니다. 보충 메시지를 이미 확인했으므로 마우스를 빠르게 가져갔다가 떼는 것과 매우 유사합니다. 이것은 UX 패리티가 적절한 것으로 보입니다.
스타일
<tool-tip>
요소는 보충 메시지를 나타내는 요소의 하위 요소가 되므로 먼저 오버레이 효과의 필수 요소부터 시작해 보겠습니다. position absolute
를 사용하여 문서 흐름에서 제외합니다.
tool-tip {
position: absolute;
z-index: 1;
}
상위 요소가 스태킹 컨텍스트가 아닌 경우 툴팁이 가장 가까운 스태킹 컨텍스트에 배치되는데 이는 원하는 결과가 아닙니다. 블록에 도움이 되는 새로운 선택기가 있습니다(:has()
).
:has(> tool-tip) {
position: relative;
}
브라우저 지원에 대해 너무 걱정하지 마세요. 먼저 이러한 도움말은 보조적인 용도입니다. 작동하지 않으면 괜찮습니다. 두 번째로 JavaScript 섹션에서 :has()
지원이 없는 브라우저에 필요한 기능을 polyfill하는 스크립트를 배포합니다.
다음으로, 부모 요소에서 포인터 이벤트를 도용하지 않도록 툴팁을 비상호작용형으로 만듭니다.
tool-tip {
…
pointer-events: none;
user-select: none;
}
그런 다음 불투명도로 툴팁을 숨겨 크로스페이드로 툴팁을 전환할 수 있습니다.
tool-tip {
opacity: 0;
}
:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
opacity: 1;
}
여기서는 :is()
와 :has()
가 많은 작업을 처리하여 tool-tip
에 포함된 상위 요소가 하위 도움말의 표시 상태를 전환하는 사용자 상호작용을 인식하도록 합니다. 마우스 사용자는 마우스를 가져갈 수 있고, 키보드 및 스크린 리더 사용자는 포커스를 둘 수 있으며, 터치 사용자는 탭할 수 있습니다.
시각 장애가 없는 사용자를 위해 오버레이를 표시하고 숨기는 작업이 완료되었으므로 이제 테마 설정, 위치 지정, 풍선에 삼각형 모양 추가를 위한 스타일을 추가할 차례입니다. 다음 스타일은 맞춤 속성을 사용하여 지금까지의 내용을 기반으로 하지만 그림자, 서체, 색상도 추가하여 떠 있는 도움말처럼 보이게 합니다.
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;
}
테마 조정
툴팁에는 관리할 색상이 몇 개 없습니다. 텍스트 색상은 시스템 키워드 CanvasText
를 통해 페이지에서 상속되기 때문입니다. 또한 값을 저장하는 맞춤 속성을 만들었으므로 이러한 맞춤 속성만 업데이트하고 나머지는 테마에서 처리하도록 할 수 있습니다.
@media (prefers-color-scheme: light) {
tool-tip {
--_bg: white;
--_shadow-alpha: 15%;
}
}
밝은 테마의 경우 배경을 흰색으로 조정하고 불투명도를 조정하여 그림자를 훨씬 약하게 만듭니다.
오른쪽에서 왼쪽으로
오른쪽에서 왼쪽으로 읽기 모드를 지원하기 위해 맞춤 속성은 문서 방향 값을 각각 -1 또는 1 값에 저장합니다.
tool-tip {
--isRTL: -1;
}
tool-tip:dir(rtl) {
--isRTL: 1;
}
이는 툴팁의 위치를 지정하는 데 사용할 수 있습니다.
tool-tip[tip-position="top"]) {
--_x: calc(50% * var(--isRTL));
}
삼각형의 위치를 파악하는 데도 도움이 됩니다.
tool-tip[tip-position="right"]::after {
--_tip: var(--_left-tip);
}
tool-tip[tip-position="right"]:dir(rtl)::after {
--_tip: var(--_right-tip);
}
마지막으로 translateX()
의 논리 변환에도 사용할 수 있습니다.
--_x: calc(var(--isRTL) * -3px * -1);
도움말 위치 지정
inset-block
또는 inset-inline
속성을 사용하여 툴팁을 논리적으로 배치하여 물리적 툴팁 위치와 논리적 툴팁 위치를 모두 처리합니다. 다음 코드는 왼쪽에서 오른쪽 방향과 오른쪽에서 왼쪽 방향 모두에 대해 네 가지 위치 각각의 스타일을 지정하는 방법을 보여줍니다.
상단 및 블록 시작 정렬
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;
}
오른쪽 및 인라인 끝 정렬
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);
}
하단 및 블록 끝 맞춤
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;
}
왼쪽 및 inline-start 정렬
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);
}
애니메이션
지금까지는 도움말의 공개 상태만 전환했습니다. 이 섹션에서는 일반적으로 안전한 동작 감소 전환이므로 먼저 모든 사용자의 불투명도를 애니메이션으로 표시합니다. 그런 다음 변환 위치에 애니메이션을 적용하여 도움말이 상위 요소에서 슬라이드 아웃되는 것처럼 보이게 합니다.
안전하고 의미 있는 기본 전환
다음과 같이 불투명도를 전환하고 변환하도록 도움말 요소를 스타일 지정합니다.
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;
}
전환에 모션 추가
사용자가 모션을 허용하는 경우 툴팁이 표시될 수 있는 각 측면에 대해 이동할 작은 거리를 지정하여 translateX 속성을 약간 배치합니다.
@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;
}
}
'in' 상태가 translateX(0)
에 있으므로 'out' 상태가 설정됩니다.
자바스크립트
내 생각에는 JavaScript는 선택사항입니다. 이는 이러한 도움말을 읽지 않아도 UI에서 작업을 완료할 수 있기 때문입니다. 따라서 도움말이 완전히 실패해도 큰 문제는 아닙니다. 이는 툴팁을 점진적으로 개선된 것으로 취급할 수 있다는 의미이기도 합니다. 결국 모든 브라우저가 :has()
를 지원하게 되므로 이 스크립트는 완전히 사라질 수 있습니다.
폴리필 스크립트는 브라우저가 :has()
를 지원하지 않는 경우에만 두 가지 작업을 실행합니다. 먼저 :has()
지원을 확인합니다.
if (!CSS.supports('selector(:has(*))')) {
// do work
}
다음으로 <tool-tip>
의 상위 요소를 찾아 작업할 클래스 이름을 지정합니다.
if (!CSS.supports('selector(:has(*))')) {
document.querySelectorAll('tool-tip').forEach(tooltip =>
tooltip.parentNode.classList.add('has_tool-tip'))
}
다음으로 해당 클래스 이름을 사용하는 스타일 세트를 삽입하여 정확히 동일한 동작에 대해 :has()
선택자를 시뮬레이션합니다.
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)
}
이제 :has()
가 지원되지 않는 경우 모든 브라우저에서 툴팁이 표시됩니다.
결론
이제 제가 어떻게 했는지 아셨으니 여러분은 어떻게 하실 건가요? 🙂 토글 팁을 더 쉽게 만드는 popup
API, z-index 충돌이 없는 상위 레이어, 창에서 항목을 더 잘 배치하는 anchor
API가 정말 기대됩니다. 그때까지는 툴팁을 만들겠습니다.
다양한 접근 방식을 사용하고 웹에서 빌드하는 모든 방법을 알아보세요.
데모를 만들고 트윗으로 링크를 보내주세요. 아래 커뮤니티 리믹스 섹션에 추가해 드리겠습니다.
커뮤니티 리믹스
아직 표시할 내용이 없습니다.
리소스
- GitHub의 소스 코드