개요

HTML의 네이티브 <dialog> 요소와 Popover API는 JavaScript 라이브러리 없이 모달, 툴팁, 드롭다운을 구현할 수 있게 합니다. 2024년부터 모든 주요 브라우저에서 안정적으로 지원되며, 접근성, 키보드 내비게이션, 포커스 관리가 내장되어 있습니다. 수십 KB의 모달 라이브러리를 대체할 수 있는 웹 표준 솔루션입니다.

핵심 개념

<dialog> 요소는 HTML5 표준 모달/비모달 대화상자입니다. showModal() 메서드는 모달 모드로, show()는 비모달 모드로 열립니다. 모달 모드에서는 자동으로 배경이 비활성화되고 ESC 키로 닫을 수 있습니다.

Popover API는 툴팁, 드롭다운, 토스트 알림 등 경량 오버레이 UI를 위한 API입니다. popover 속성만으로 자동 표시/숨김, 스택 관리, Light Dismiss(바깥 클릭 시 닫기)가 처리됩니다.

::backdrop 의사 요소는 모달의 배경 오버레이를 스타일링합니다.

실전 예제

<dialog> 요소로 모달을 구현합니다.

<button id="open-dialog">모달 열기</button>

<dialog id="my-dialog">
  <form method="dialog">
    <h2>설정</h2>
    <p>여기에 설정 옵션이 표시됩니다.</p>

    <label for="username">사용자명</label>
    <input type="text" id="username" name="username">

    <div class="dialog-actions">
      <button type="button" id="cancel">취소</button>
      <button type="submit" value="save">저장</button>
    </div>
  </form>
</dialog>
dialog {
  max-width: 500px;
  border: none;
  border-radius: 8px;
  padding: 2rem;
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
              0 2px 4px -1px rgba(0, 0, 0, 0.06);
}

/* 모달 배경 오버레이 */
dialog::backdrop {
  background-color: rgba(0, 0, 0, 0.5);
  backdrop-filter: blur(4px);
}

/* 오픈 애니메이션 */
dialog[open] {
  animation: slideIn 0.3s ease-out;
}

@keyframes slideIn {
  from {
    opacity: 0;
    transform: translateY(-20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.dialog-actions {
  display: flex;
  gap: 0.5rem;
  justify-content: flex-end;
  margin-top: 1.5rem;
}
const dialog = document.getElementById('my-dialog');
const openBtn = document.getElementById('open-dialog');
const cancelBtn = document.getElementById('cancel');

openBtn.addEventListener('click', () => {
  dialog.showModal(); // 모달 모드로 열기
});

cancelBtn.addEventListener('click', () => {
  dialog.close(); // 닫기
});

// 폼 제출 처리
dialog.addEventListener('close', () => {
  if (dialog.returnValue === 'save') {
    console.log('저장 버튼 클릭됨');
    const formData = new FormData(dialog.querySelector('form'));
    console.log('사용자명:', formData.get('username'));
  }
});

// 배경 클릭 시 닫기
dialog.addEventListener('click', (e) => {
  if (e.target === dialog) {
    dialog.close();
  }
});

Popover API로 드롭다운 메뉴를 구현합니다.

<button popovertarget="menu">메뉴 열기</button>

<div id="menu" popover>
  <ul>
    <li><a href="/profile">프로필</a></li>
    <li><a href="/settings">설정</a></li>
    <li><a href="/logout">로그아웃</a></li>
  </ul>
</div>
[popover] {
  margin: 0;
  padding: 0.5rem 0;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  background: white;
  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
              0 4px 6px -2px rgba(0, 0, 0, 0.05);
}

[popover]::backdrop {
  background: transparent; /* Popover는 배경 오버레이 없음 */
}

[popover] ul {
  list-style: none;
  padding: 0;
  margin: 0;
}

[popover] li {
  padding: 0;
}

[popover] a {
  display: block;
  padding: 0.75rem 1.5rem;
  color: #374151;
  text-decoration: none;
}

[popover] a:hover {
  background: #f3f4f6;
}

/* 오픈 애니메이션 */
[popover]:popover-open {
  animation: fadeIn 0.2s ease-out;
}

@keyframes fadeIn {
  from {
    opacity: 0;
    transform: scale(0.95);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

Popover API로 툴팁을 구현합니다.

<button popovertarget="tooltip" popovertargetaction="hover">
  도움말
</button>

<div id="tooltip" popover="manual" role="tooltip">
  이 기능은 사용자 프로필을 편집합니다.
</div>
// Manual popover로 호버 동작 구현
const button = document.querySelector('[popovertarget="tooltip"]');
const tooltip = document.getElementById('tooltip');

button.addEventListener('mouseenter', () => {
  tooltip.showPopover();
});

button.addEventListener('mouseleave', () => {
  tooltip.hidePopover();
});

button.addEventListener('focus', () => {
  tooltip.showPopover();
});

button.addEventListener('blur', () => {
  tooltip.hidePopover();
});

중첩된 모달을 구현합니다.

<button id="open-main">메인 다이얼로그 열기</button>

<dialog id="main-dialog">
  <h2>메인 다이얼로그</h2>
  <button id="open-nested">중첩 다이얼로그 열기</button>
  <button onclick="this.closest('dialog').close()">닫기</button>
</dialog>

<dialog id="nested-dialog">
  <h2>중첩 다이얼로그</h2>
  <p>메인 다이얼로그 위에 표시됩니다.</p>
  <button onclick="this.closest('dialog').close()">닫기</button>
</dialog>
document.getElementById('open-main').addEventListener('click', () => {
  document.getElementById('main-dialog').showModal();
});

document.getElementById('open-nested').addEventListener('click', () => {
  document.getElementById('nested-dialog').showModal();
});

Popover의 자동 닫기 동작을 제어합니다.

<!-- auto (기본값): 다른 popover가 열리거나 바깥 클릭 시 자동으로 닫힘 -->
<div popover="auto">자동 닫힘</div>

<!-- manual: 명시적으로 hidePopover() 호출해야만 닫힘 -->
<div popover="manual">수동으로만 닫힘</div>

활용 팁

  • 접근성 내장: <dialog>는 자동으로 포커스 트랩, ESC 키 닫기, ARIA 역할을 제공합니다. 추가 JavaScript 없이 WCAG 준수가 가능합니다.
  • 폼 통합: <form method="dialog"> 내부의 submit 버튼은 자동으로 다이얼로그를 닫고, button의 value가 returnValue가 됩니다.
  • 브라우저 지원: <dialog>는 Chrome 37+, Safari 15.4+, Firefox 98+에서 지원됩니다. Popover API는 Chrome 114+, Safari 17+, Firefox 125+입니다.
  • Polyfill: dialog-polyfill 라이브러리로 구형 브라우저를 지원할 수 있습니다.
  • Popover 위치: CSS Anchor Positioning API와 결합하면 Popover를 정확한 위치에 표시할 수 있습니다.
  • 애니메이션: @starting-style 규칙으로 열기/닫기 애니메이션을 더욱 세밀하게 제어할 수 있습니다.

마무리

<dialog>와 Popover API는 모달 UI 구현의 복잡성을 웹 플랫폼 레벨에서 해결합니다. 접근성, 포커스 관리, 키보드 내비게이션이 자동으로 처리되며, JavaScript 라이브러리 의존성을 제거할 수 있습니다. 모던 웹 애플리케이션에서 모달과 오버레이 UI를 구현할 때 가장 먼저 고려해야 할 표준 솔루션입니다.