도움말 구성요소 빌드

색상 적응형이며 접근성이 뛰어난 도움말 텍스트 맞춤 요소를 빌드하는 방법에 관한 기본 개요입니다.

이 게시물에서는 색상 적응형이며 접근성이 뛰어난 <tool-tip> 맞춤 요소를 빌드하는 방법에 관한 생각을 공유하고자 합니다. 데모를 사용해 보고 소스를 확인하세요.

다양한 예시와 색 구성표에 대한 도움말이 표시됩니다.

동영상을 선호하는 경우 이 게시물의 YouTube 버전을 확인하세요.

개요

도움말은 사용자 인터페이스에 대한 추가 정보를 포함하는 모달이 아니고 차단되지 않으며 상호작용이 없는 오버레이입니다. 이 버튼은 기본적으로 숨겨져 있으며 연결된 요소에 마우스를 가져가거나 포커스를 지정할 때 숨겨지지 않습니다. 도움말은 직접 선택하거나 상호작용할 수 없습니다. 도움말은 라벨이나 기타 가치가 높은 정보를 대체하는 것이 아닙니다. 사용자는 도움말 없이도 작업을 완전히 완료할 수 있어야 합니다.

권장사항: 항상 입력에 라벨을 지정하세요.
금지사항: 라벨 대신 도움말을 사용하지 않음

전환 도움말과 도움말 비교

많은 구성요소와 마찬가지로 도움말의 정의도 다양합니다(예: MDN, WAI ARIA, Sarah Higley, 포용적 구성요소). 도움말과 전환 팁이 구분되어 있어서 좋습니다. 도움말에는 상호작용이 없는 보충 정보가 포함되어야 하지만 전환 도움말에는 상호작용과 중요한 정보가 포함될 수 있습니다. 이러한 구분의 주된 이유는 접근성입니다. 사용자가 팝업으로 이동하여 내부 정보와 버튼에 액세스하는 방법입니다. 전환 도움말은 금방 복잡해집니다.

다음은 사용자가 고정하여 열고 탐색한 후 조명이 꺼지거나 이스케이프 키를 눌러 닫을 수 있는 양방향 오버레이인 Designcember 사이트의 전환 도움말 동영상입니다.

이 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>

이제 스크린 리더에서는 도움말로 인식됩니다. 다음 예에서 첫 번째 링크 요소의 트리에 인식된 도움말 요소가 있고 두 번째 링크 요소에는 인식된 도움말 요소가 없는 것을 확인할 수 있습니다. 두 번째 사용자에게는 역할이 없습니다. 스타일 섹션에서 이 트리 뷰를 개선합니다.

HTML을 나타내는 Chrome DevTools 접근성 트리의 스크린샷 포커스를 설정할 수 있는 &#39;top ; Has tooltip: Hey, a tooltip!&#39; 텍스트가 포함된 링크를 표시합니다. 내부에는 &#39;top&#39;의 정적 텍스트와 도움말 요소가 있습니다.

다음으로 도움말에 포커스를 설정할 수 없도록 해야 합니다. 스크린 리더가 도움말 역할을 이해하지 못하면 사용자가 <tool-tip>에 포커스를 맞추어 콘텐츠를 읽을 수 있으며 사용자 환경에는 이 역할이 필요하지 않습니다. 스크린 리더는 콘텐츠를 상위 요소에 추가하므로 액세스할 수 있도록 포커스가 필요하지 않습니다. 여기서는 inert를 사용하여 사용자가 탭 흐름에서 실수로 이 도움말 콘텐츠를 찾지 못하도록 할 수 있습니다.

<tool-tip inert role="tooltip">A tooltip</tool-tip>

Chrome DevTools 접근성 트리의 또 다른 스크린샷입니다. 이번에는 도움말 요소가 누락되어 있습니다.

그런 다음 속성을 인터페이스로 사용하여 도움말의 위치를 지정했습니다. 기본적으로 모든 <tool-tip>는 'top' 위치를 가정하지만 tip-position를 추가하여 요소에서 위치를 맞춤설정할 수 있습니다.

<tool-tip role="tooltip" tip-position="right ">A tooltip</tool-tip>

오른쪽에 &#39;A tooltip&#39;이라는 도움말이 있는 링크의 스크린샷

저는 <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>

&#39;GUI Challenges 해골 로고&#39;라는 도움말이 있는 이미지의 스크린샷

여기서는 <abbr> 요소 내에 <tool-tip>를 배치합니다.

<p>
  The <abbr>HTML <tool-tip role="tooltip" tip-position="top">Hyper Text Markup Language</tool-tip></abbr> abbr element.
</p>

HTML이라는 약어가 밑줄이 그어져 있고 그 위에 &#39;하이퍼 텍스트 마크업 언어&#39;라는 도움말이 있는 단락의 스크린샷

접근성

전환 도움말이 아닌 도움말을 빌드하기로 선택했으므로 이 섹션은 훨씬 간단합니다. 먼저 Google에서 원하는 사용자 환경을 간략히 설명해 드리겠습니다.

  1. 제약된 공간이나 복잡한 인터페이스에서는 보조 메시지를 숨깁니다.
  2. 사용자가 요소에 마우스를 가져가거나 포커스를 지정하거나 터치를 사용하여 요소와 상호작용하면 메시지를 표시합니다.
  3. 마우스 오버, 초점 또는 터치가 종료되면 메일을 다시 숨깁니다.
  4. 마지막으로 사용자가 모션 감소 환경설정을 지정한 경우 모션이 감소하는지 확인합니다.

우리의 목표는 주문형 보충 메시지인 것입니다. 시력이 정상인 마우스나 키보드 사용자는 마우스를 가져가서 메시지를 표시하고 눈으로 읽을 수 있습니다. 시각장애인 스크린 리더 사용자는 포커스를 맞춰 메시지를 표시하고 도구를 통해 소리로 메시지를 받을 수 있습니다.

도움말이 있는 링크를 읽는 MacOS VoiceOver의 스크린샷

이전 섹션에서는 접근성 트리, 도움말 역할, 비활성 상태를 다뤘습니다. 이제 테스트를 진행하고 사용자 환경이 사용자에게 도움말 메시지를 적절하게 표시하는지 확인하면 됩니다. 테스트 결과, 음성 메시지의 어느 부분이 도움말인지 명확하지 않습니다. 접근성 트리에서 디버깅하는 동안에도 확인할 수 있습니다. 'top'의 링크 텍스트가 'Look, tooltips!'와 함께 주저함 없이 실행됩니다. 스크린 리더가 텍스트를 끊거나 도움말 콘텐츠로 식별하지 않습니다.

링크 텍스트가 &#39;top Hey, a tooltip!&#39;이라고 표시된 Chrome DevTools 접근성 트리의 스크린샷

<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;
}

아래에서 업데이트된 접근성 트리를 확인할 수 있습니다. 이제 링크 텍스트 뒤에 세미콜론이 있고 도움말 프롬프트인 '도움말 있음: '이 표시됩니다.

Chrome DevTools 접근성 트리의 업데이트된 스크린샷으로, 링크 텍스트의 문구가 &#39;top ; Has tooltip: Hey, a tooltip!&#39;로 개선되었습니다.

이제 스크린 리더 사용자가 링크에 포커스를 맞추면 '상단'이라고 말한 후 잠시 멈추었다가 '도움말이 있음: 도움말 보기'라고 알려줍니다. 이렇게 하면 화면 리더 사용자에게 몇 가지 유용한 UX 힌트를 제공할 수 있습니다. 지연을 사용하면 링크 텍스트와 도움말 텍스트를 깔끔하게 구분할 수 있습니다. 또한 'has tooltip'이 공지되면 화면 리더 사용자가 이전에 이미 들었을 경우 쉽게 취소할 수 있습니다. 이미 보셨듯이 마우스 오버했다가 빠르게 마우스 오버를 해제하는 것과 매우 유사합니다. 이는 UX 패리티가 잘 지켜진 것처럼 느껴졌습니다.

스타일

<tool-tip> 요소는 보조 메시지를 나타내는 요소의 하위 요소이므로 먼저 오버레이 효과의 기본사항부터 살펴보겠습니다. position absolute를 사용하여 문서 흐름에서 제외합니다.

tool-tip {
  position: absolute;
  z-index: 1;
}

상위 요소가 스태킹 컨텍스트가 아닌 경우 도움말이 가장 가까운 컨텍스트에 위치하지만 원하는 위치가 아닙니다. 블록에 도움이 되는 새로운 선택기가 있습니다. :has()

브라우저 지원

  • Chrome: 105.
  • Edge: 105.
  • Firefox: 121.
  • Safari: 15.4

소스

:has(> tool-tip) {
  position: relative;
}

브라우저 지원에 대해 너무 걱정하지 마세요. 먼저 이러한 도움말은 보조적인 기능이라는 점을 기억하세요. 작동하지 않는 경우에도 괜찮습니다. 두 번째로, JavaScript 섹션에서 :has() 지원이 없는 브라우저에 필요한 기능을 폴리필하는 스크립트를 배포합니다.

다음으로, 상위 요소에서 포인터 이벤트를 훔치지 않도록 도움말을 비상호작용 상태로 만들어 보겠습니다.

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가 하위 도움말의 표시 여부를 전환하는 사용자 상호작용을 인식하도록 합니다. 마우스 사용자는 마우스 오버할 수 있고, 키보드 및 스크린 리더 사용자는 포커스를 맞출 수 있으며, 터치 사용자는 탭할 수 있습니다.

시각 장애가 없는 사용자를 위해 오버레이 표시 및 숨기기가 작동하는 상태에서 테마 설정, 배치, 풍선에 삼각형 도형을 추가하기 위한 스타일을 추가해 보겠습니다. 다음 스타일은 맞춤 속성을 사용하여 지금까지의 작업을 기반으로 섀도우, 서체, 색상을 추가하여 플로팅 도움말처럼 보이게 합니다.

&#39;block-start&#39; 링크 위에 표시된 어두운 모드의 도움말 스크린샷

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 속성을 사용하여 도움말을 논리적으로 배치하여 물리적 및 논리적 도움말 위치를 모두 처리합니다. 다음 코드는 4가지 위치 각각에 왼쪽에서 오른쪽 및 오른쪽에서 왼쪽 방향 모두에 맞게 스타일이 지정되는 방식을 보여줍니다.

상단 및 블록 시작 정렬

왼쪽에서 오른쪽 상단 위치와 오른쪽에서 왼쪽 상단 위치 간의 게재위치 차이를 보여주는 스크린샷

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;
}

왼쪽 및 인라인 시작 정렬

왼쪽에서 오른쪽 왼쪽 위치와 오른쪽에서 왼쪽 인라인 시작 위치 간의 게재위치 차이를 보여주는 스크린샷

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 싸움을 피할 수 있는 top 레이어, 창에서 항목을 더 효과적으로 배치할 수 있는 anchor API를 정말 기대하고 있습니다. 그때까지는 도움말을 작성하겠습니다.

접근 방식을 다양화하고 웹에서 빌드하는 모든 방법을 알아보겠습니다.

데모를 만들어 트윗해 주시면 아래의 커뮤니티 리믹스 섹션에 추가해 드리겠습니다.

커뮤니티 리믹스

아직 표시할 내용이 없습니다.

리소스