본문으로 건너뛰기
wooncloud

disabled 버튼에 툴팁이 안 뜨는 이유와 해결

disabled 버튼은 포인터 이벤트를 차단해 툴팁 표시를 방해하므로 CSS의 pointer-events: none을 활용해 이벤트를 부모 요소로 전달하는 해결법과 함께 키보드 사용자를 위한 접근성 보완책을 제시합니다

·7 min read·— views··

비활성(disabled) 버튼에 마우스를 올리면 "왜 못 누르는지"를 툴팁으로 알려주고 싶을 때가 있습니다. 권한이 없거나, 지금 위치에서는 불가능한 액션이라거나. 그런데 커스텀 툴팁을 달아보면 disabled 버튼 위에서는 툴팁이 아예 안 뜹니다. 버튼을 <span onMouseEnter> 같은 트리거로 감싸도 마찬가지입니다.

원인은 브라우저의 이벤트 모델에 있고, 해결은 CSS 한 줄입니다.

왜 안 뜨는가

HTML 명세가 disabled 컨트롤에서 막는 건 click 뿐입니다. 나머지 마우스 이벤트는 브라우저마다 갈립니다 — 크롬·파이어폭스는 mouseover/mousemove 를 정상적으로 발생시켜 상위로 버블시키지만, WebKit 계열은 오랫동안 disabled 폼 컨트롤에서 마우스 이벤트를 억제해 왔습니다. 툴팁이 어떤 브라우저에서만 안 뜨는 이유가 여기 있습니다.

부모로 감싸는 것도 확실한 해법이 아닙니다. 히트 테스트의 대상은 여전히 disabled 버튼이라(document.elementFromPoint 가 버튼을 돌려줍니다), 그 브라우저가 disabled 컨트롤의 마우스 이벤트를 억제하면 부모의 mouseenter 까지 같이 막힙니다. 크롬에서는 되고 사파리에서만 안 되는, 가장 늦게 발견되는 종류의 차이입니다.

게다가 disabled 버튼은 포커스도 받지 못합니다. 그래서 onFocus 로 툴팁을 띄우는 키보드 경로도 막힙니다.

// ❌ 이렇게 감싸도 disabled 버튼 위에선 onMouseEnter 가 안 온다
<span onMouseEnter={showTip} onMouseLeave={hideTip}>
  <button disabled>새로 만들기</button>
</span>

해결: 버튼에 pointer-events: none

비활성 버튼에 pointer-events: none 을 줍니다. 그러면 히트 테스트가 버튼을 건너뛰어 포인터가 그대로 "통과"하고, 부모가 정식 타깃이 됩니다(elementFromPoint 도 부모를 돌려줍니다). disabled 컨트롤의 이벤트 억제 여부와 무관해지므로 브라우저를 가리지 않는 해법입니다.

<span
  onMouseEnter={showTip}
  onMouseLeave={hideTip}
  style={{ display: "inline-flex", cursor: "not-allowed" }}
>
  <button
    disabled
    style={{ pointerEvents: "none" }}
  >
    새로 만들기
  </button>
  {tipOpen && <Bubble>공유받은 폴더 안에서만 만들 수 있어요</Bubble>}
</span>

핵심은 세 가지입니다.

  • 툴팁 트리거는 버튼을 감싸는 별도 요소여야 합니다. 그 요소에 hover 핸들러를 둡니다.
  • disabled 버튼에 pointer-events: none 을 줘서 포인터가 트리거까지 도달하게 합니다.
  • cursor: not-allowed 는 트리거(부모)에 주는 편이 안전합니다. pointer-events: none 인 버튼은 자기 cursor 를 적용하지 못하기 때문입니다.

레이아웃 함정: full-width 버튼

버튼이 width: 100% 로 컨테이너를 꽉 채우는 디자인이라면, 감싸는 트리거도 같은 폭이어야 합니다. 트리거를 무심코 display: inline-flex(콘텐츠 폭) 로 두면, 그 안의 width: 100% 버튼이 트리거 폭에 갇혀 찌그러집니다.

트리거를 display: flex; width: 100% 로 블록처럼 펴주면 버튼이 원래 폭을 유지합니다.

<span style={{ display: "flex", width: "100%" }}>
  <button disabled style={{ width: "100%", pointerEvents: "none" }}>…</button>
</span>

접근성: 툴팁만으로 끝내지 말 것

이 방법은 hover 전용입니다. disabled 버튼은 포커스가 안 되므로 키보드·스크린리더 사용자는 툴팁 내용을 못 봅니다. 툴팁에만 이유를 담으면 그들에게는 "이유 없이 막힌 버튼"이 됩니다.

두 가지 보완책이 있습니다.

  • 같은 정보를 항상 보이는 텍스트로도 제공합니다. 화면 상단 설명문이나 버튼 옆 안내 문구처럼, 마우스 없이도 읽히는 곳에 사유를 한 번 더 둡니다.
  • 또는 진짜로 disable 하지 않습니다. disabled 속성 대신 aria-disabled="true" 로 표시하고, 시각적으로만 비활성처럼 스타일링하며, onClick 에서 아무 일도 하지 않게(또는 안내를 띄우게) 막는 방식입니다. 이러면 버튼이 여전히 포커스·hover 가능하므로 툴팁과 키보드 접근성을 모두 살릴 수 있습니다. 대신 "클릭은 되지만 아무 일도 안 일어남"을 직접 처리해야 합니다.

어느 쪽이든, 툴팁은 보조 수단이지 유일한 안내 채널이 되어선 안 된다는 원칙은 같습니다.

정리

  • disabled 폼 컨트롤은 포인터 이벤트를 삼켜서, 자신은 물론 감싸는 부모의 hover 도 막습니다.
  • 버튼에 pointer-events: none 을 주면 포인터가 통과해 부모 트리거가 hover 를 받아 툴팁이 뜹니다.
  • full-width 버튼이면 트리거도 width: 100% 로 펴줍니다.
  • disabled 는 포커스 불가 → 툴팁만으로 안내하지 말고, 항상 보이는 텍스트나 aria-disabled 대안으로 접근성을 챙깁니다.