Popover API란? JS 없이 툴팁 · 메뉴를 여닫는 법
popover 속성 하나로 열기/닫기, Esc 처리, 접근성이 자동으로 동작하는 브라우저 네이티브 UI
이 글을 읽으면 툴팁 · 메뉴 · 토스트를 JavaScript 없이 열고 닫고, 버튼 바로 밑에 붙이는 데까지 할 수 있다. 모달은 popover가 아니라 dialog라는 것도.
<button popovertarget="tip-1">도움말</button>
<div id="tip-1" popover="auto" role="tooltip">
이게 네이티브 툴팁
</div>
JavaScript 0줄. 이벤트 리스너 없음. 상태 추적 없음. 그런데 Esc로 닫히고, 키보드로 접근 가능하고, 스크린 리더가 제대로 읽는다.
하나만 먼저. 이건 버튼을 눌러야 열린다(키보드 Enter · Space 포함). 마우스를 올리기만 해서 뜨는 툴팁은 아직 JavaScript 몫이다 — 아래 「아직 JavaScript가 필요한 영역」에서 다시 나온다.
기존 툴팁이 깨지는 방식
"hover하면 뜨고 벗어나면 사라지는" 단순한 요구사항인데, 실제로 배포하면 문제가 쏟아진다.
키보드 사용자가 Tab으로 트리거에 도착해도 툴팁이 안 뜬다. 스크린 리더가 두 번 읽거나 아예 안 읽는다. 마우스를 빠르게 움직이면 깜빡인다. 작은 화면에서 겹친다. Esc로 안 닫힌다. 포커스가 유실된다.
결국 이벤트 리스너가 누적되고, hover/focus 별도 처리, 외부 클릭 특수 케이스, ARIA 속성 수동 동기화까지 코드가 비대해진다. 구현 자체의 문제가 아니라 웹 플랫폼에 "이 요소가 툴팁이다"라는 개념이 없었기 때문이다.
Popover API가 바꾼 것
popover 속성을 붙이면 브라우저가 이 요소의 역할을 이해한다. 3가지가 자동으로 해결된다.
1. JavaScript 없이 열기/닫기
popovertarget="id"로 버튼과 popover를 연결하면 끝. 클릭(키보드 Enter · Space 포함)으로 열고 닫는 걸 브라우저가 처리한다. popovertargetaction으로 동작을 지정할 수도 있다 — show, hide, toggle(기본값).
2. Esc 키 자동 처리
전역 keydown 핸들러, Esc 전용 정리 로직이 필요 없다. 브라우저가 popover를 해제 가능한 요소로 인식하기 때문에 Esc가 그냥 동작한다.
3. ARIA 상태 자동 동기화
트리거 버튼이 "지금 펼쳐져 있다/접혀 있다"를 스크린 리더에 알리는 걸 브라우저가 맡는다. DOM에 aria-expanded 속성이 붙는 건 아니고, 접근성 트리 쪽에서 처리된다. 그래서 JS로 속성을 맞춰 줄 일이 없고, 열림 상태와 속성이 어긋날 일도 없다.
z-index 싸움이 끝난다 — top layer
열린 popover는 top layer에 그려진다. 페이지의 다른 모든 것 위에 있는 별도 층이라, z-index: 9999를 준 요소보다도 위에 뜬다. 부모에 overflow: hidden이 걸려 있어도 잘리지 않는다.
카드 안에 넣은 드롭다운이 잘려서 결국 DOM을 body 끝으로 옮겨 본 적이 있다면, 이게 제일 반가운 부분이다.
뒤를 살짝 어둡게 깔고 싶으면 ::backdrop을 쓴다.
[popover]::backdrop {
background: rgb(0 0 0 / 0.3);
}
포커스를 가두지는 않는다 — 모달이면 dialog
popover가 열려 있어도 Tab으로 바깥 요소에 갈 수 있다. 뒤쪽 페이지를 못 누르게 막는 것(포커스 트랩)도 안 한다.
확인 창처럼 "이걸 끝내기 전엔 다른 걸 못 하게" 해야 하면 <dialog>를 showModal()로 연다. 그쪽은 나머지 페이지를 inert(조작 불가)로 만들어 준다.
고르는 기준은 단순하다. 툴팁 · 메뉴 · 토스트는 popover, 사용자가 답해야 넘어가는 창은 modal dialog.
auto vs manual
<!-- auto: 바깥 클릭 시 자동 닫힘 -->
<div popover="auto">자동 닫힘</div>
<!-- manual: 명시적으로만 닫힘 -->
<div popover="manual">수동 닫힘</div>
auto는 바깥 클릭, Esc, 다른 auto popover 열기 시 자동으로 닫힌다. 툴팁에 적합.
manual은 바깥 클릭으로도, Esc로도 안 닫힌다. 버튼(popovertarget)이나 hidePopover()로 닫는다. 대신 여러 개를 동시에 띄울 수 있어서 토스트 알림 같은 데 맞다.
버튼 여러 개가 popover 하나를 열어도 된다
popovertarget은 id를 가리킬 뿐이라, 같은 id를 가리키는 버튼을 몇 개 둬도 된다. 머리글의 「?」와 본문 속 버튼이 같은 도움말을 여는 식이다. popovertarget은 <button>(과 <input type="button">)에만 붙는다 — <a>에 붙여도 아무 일도 안 일어난다.
열기만 하는 버튼, 닫기만 하는 버튼은 popovertargetaction으로 나눈다. show는 열기만, hide는 닫기만, 기본값 toggle은 열려 있으면 닫는다.
<button popovertarget="help">?</button>
<button popovertarget="help" popovertargetaction="show">도움말 보기</button>
<div id="help" popover>
...
<button popovertarget="help" popovertargetaction="hide">닫기</button>
</div>
위치는 CSS로 — 연 버튼이 곧 앵커
처음 popover를 띄워 보면 화면 한가운데에 뜬다. 기본 스타일이 position: fixed + margin: auto라서 그렇다. 툴팁이면 버튼 옆에 붙어야 한다.
popovertarget으로 묶으면 그 버튼이 popover의 앵커(anchor — 「이 요소 기준으로 놓아라」의 기준점)가 된다. 그래서 anchor-name을 따로 붙이지 않아도 position-area 한 줄로 버튼 바로 밑에 놓인다.
[popover] {
margin: 0;
position-area: bottom;
}
margin: 0은 빼먹기 쉽다. 기본값 margin: auto가 남아 있으면 자리가 엉뚱하게 잡힌다. anchor()나 anchor-center로 세밀하게 놓을 때는 position-anchor: auto를 같이 써야 이 암묵 앵커를 쓴다(MDN).
CSS anchor positioning을 모르는 브라우저에서는 이 줄이 무시되고 다시 화면 가운데에 뜬다. 그게 곤란한 UI라면 아래 「라이브러리」 쪽이다.
아직 JavaScript가 필요한 영역
타이밍 제어. 네이티브 popover는 즉시 열리고 닫힌다. 마우스를 빠르게 움직이면 깜빡이는 문제가 여전하다. hover와 열림 사이에 딜레이를 넣으려면 JavaScript가 필요하다.
호버 인텐트. 브라우저는 포인터가 요소 위에 있는 이유를 모른다. 의도적인 호버인지, 지나가는 중인지 판단할 수 없다. 이건 JavaScript로 감지해야 한다.
다만 핵심이 바뀌었다. 이전에는 JavaScript가 상호작용 모델 전체를 소유했다. 이제는 Popover API가 기본 열기/닫기를 처리하고, JavaScript는 그 위에 인텐트 판별만 얹는다.
라이브러리가 여전히 유효한 경우
복잡한 포지셔닝. 중첩 스크롤 컨테이너 간 충돌 감지, 커스텀 플리핑 로직이 필요하면 Floating UI 같은 라이브러리가 여전히 낫다. 단순히 「버튼 밑에」면 위의 position-area로 되지만, anchor positioning을 모르는 브라우저까지 같은 자리에 놓아야 하면 아직 라이브러리가 편하다.
대규모 디자인 시스템. 여러 팀이 쓰는 디자인 시스템에서는 중앙화된 동작, 문서화된 패턴, 가드레일이 필요하다. 라이브러리가 이 역할을 한다.
접근성 경험이 부족한 팀. Popover API가 좋은 기본값을 제공하지만, role, 레이블, 포커스 관리를 언제 추가할지 아는 건 여전히 필요하다.
근데 대부분의 경우 — 단순 툴팁, 드롭다운 메뉴, 토스트 알림 — Popover API만으로 충분하다. 라이브러리 없이.
이미 있는 화면의 직접 만든 툴팁 · 모달을 popover · dialog로 바꾸다가 막혔다면 negabaro 에 편하게 물어봐도 된다.
핵심 포인트
popover 속성과 popovertarget만으로 열기/닫기, Esc, 키보드 내비게이션이 자동 동작
펼침 상태는 브라우저가 접근성 트리에 알려 준다 — ARIA 속성을 JS로 맞출 일이 없다
열린 popover는 top layer에 뜬다 — z-index · overflow: hidden에 안 잘린다. 모달(포커스 트랩)이 필요하면 dialog.showModal()
auto는 바깥 클릭/Esc로 닫히고 하나만 뜬다, manual은 버튼 · hidePopover()로만 닫히고 여러 개가 같이 뜬다
popovertarget으로 묶은 버튼이 곧 앵커 — position-area: bottom 한 줄로 버튼 밑에 놓인다 (margin: 0 잊지 말 것)
타이밍 제어, 호버 인텐트 같은 영역은 여전히 JavaScript가 필요하지만 핵심 모델은 브라우저 담당