도움말 구성요소 빌드

색상 적응형과 액세스 가능한 도움말 맞춤 요소를 빌드하는 방법에 관한 기본적인 개요입니다.

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

다양한 예와 색 구성표에 적용되는 도움말이 표시됩니다.

동영상을 선호한다면 이 게시물의 YouTube 버전을 참조하세요.

개요

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

권장사항: 항상 입력에 라벨을 지정합니다.
라벨 대신 도움말에 의존하지 않음

Toggletip과 도움말 비교

많은 구성요소와 마찬가지로 도움말이 무엇인지에 관한 다양한 설명이 있습니다. 예를 들어 MDN, WAI ARIA, Sarah Higley, 포함 구성요소가 있습니다. 툴팁과 전환 팁이 구분되어 있는 점이 좋습니다. 도움말에는 대화형이 아닌 추가 정보가 포함되어야 하고 토글팁에는 상호작용 및 중요한 정보가 포함될 수 있습니다. 분할의 주된 이유는 접근성, 즉 사용자가 어떻게 팝업으로 이동하고 팝업 안에 있는 정보와 버튼에 액세스할 수 있어야 하기 때문입니다. 전환 팁은 금방 복잡해집니다.

다음은 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>

이제 스크린 리더에서는 도움말로 인식됩니다. 다음 예에서 첫 번째 링크 요소는 트리에서 도움말 요소를 인식하고 두 번째 링크 요소는 인식하지 못하는 방법을 확인하세요. 두 번째는 해당 역할이 없습니다. 스타일 섹션에서 이 트리 보기를 개선해 보겠습니다.

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

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

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

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

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

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

오른쪽에 &#39;도움말&#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;Hyper Text Markup Language&#39;라는 팁이 있는 단락의 스크린샷

접근성

전환 팁이 아닌 도움말을 빌드하기로 선택했으므로 이 섹션이 훨씬 간단합니다. 먼저 원하는 사용자 환경을 개괄적으로 설명하겠습니다.

  1. 제한된 공간이나 복잡한 인터페이스에서는 추가 메시지를 숨길 수 있습니다.
  2. 사용자가 마우스를 가져가거나 포커스를 맞추거나 터치를 사용하여 요소와 상호작용하면 메시지가 표시됩니다.
  3. 마우스 오버, 포커스 또는 터치가 끝나면 메시지를 다시 숨깁니다.
  4. 마지막으로, 사용자가 감소 모션에 대한 기본 설정을 지정한 경우 모든 모션을 줄여야 합니다.

Google의 목표는 주문형 추가 메시지를 제공하는 것입니다. 시력이 정상인 마우스나 키보드 사용자는 마우스를 가져가서 눈으로 메시지를 읽을 수 있습니다. 시각 장애가 없는 스크린 리더 사용자는 도구를 통해 메시지를 소리 내어 받은 메시지를 표시하는 데 집중할 수 있습니다.

도움말이 있는 링크를 읽는 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: a tooltip!&#39;

이제 스크린 리더 사용자가 링크에 포커스를 맞추면 '상단'이라는 텍스트가 표시되고 잠시 일시중지한 후 '도움말 있음: 보기, 도움말'이 표시됩니다. 이렇게 하면 스크린 리더 사용자에게 몇 가지 유용한 UX 힌트가 제공됩니다. 망설임으로 링크 텍스트와 도움말이 잘 구분됩니다. 또한 '도움말 있음'이 발표되면 스크린 리더 사용자는 이전에 이미 들은 적이 있다면 쉽게 취소할 수 있습니다. 이미 추가 메시지를 보았으므로 이는 마우스 오버 및 마우스 오버를 빠르게 해제하는 것과 매우 유사합니다. 멋진 UX 패리티처럼 느껴졌습니다.

스타일

<tool-tip> 요소는 이 요소가 추가 메시지를 나타내는 요소의 하위 요소이므로 오버레이 효과의 기본사항부터 시작하겠습니다. position absolute를 사용하여 문서 흐름에서 벗어날 수 있습니다.

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

상위 요소가 스택 컨텍스트가 아니면 도움말 자체가 가장 가까운 컨텍스트에 배치되는데, 이는 원하는 위치가 아닙니다. 블록에 도움이 될 수 있는 새로운 선택기 :has()가 있습니다.

브라우저 지원

  • 105
  • 105
  • 121
  • 15.4

소스

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

브라우저 지원에 대해서는 너무 걱정하지 않아도 됩니다. 먼저, 이 도움말은 추가사항입니다. 작동하지 않아도 괜찮습니다. 둘째, 자바스크립트 섹션에서 :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 속성을 사용하여 도움말을 논리적으로 배치하여 실제 도움말 위치와 논리적 도움말 위치를 모두 처리합니다. 다음 코드는 네 개의 위치 각각이 왼쪽에서 오른쪽 방향과 오른쪽에서 왼쪽 방향의 스타일이 지정되는 방식을 보여줍니다.

상단 및 블록 시작 정렬

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

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

제 생각에 JavaScript는 선택사항입니다. UI에서 작업을 실행하는 데 이러한 도움말을 읽지 않아도 되기 때문입니다. 따라서 도움말이 완전히 실패하더라도 문제가 되지 않습니다. 즉, 도움말을 점진적으로 개선할 수 있습니다. 결국 모든 브라우저에서 :has()를 지원할 예정이며 이 스크립트는 완전히 사라질 수 있습니다.

polyfill 스크립트는 두 가지 작업을 하며 브라우저에서 :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-색인 전투가 없는 상단 레이어, 창에서 항목을 더 잘 배치할 수 있는 anchor API가 기대됩니다. 그때까지는 도움말을 만드겠습니다

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

데모를 만들고 링크를 트윗해 주세요. 그러면 아래의 커뮤니티 리믹스 섹션에 추가하겠습니다.

커뮤니티 리믹스

표시할 항목 없음

자료