HTML <dialog> 요소 사용 및 스타일링하기
요약
본 글은 네이티브 HTML `<dialog>` 요소를 깊이 있게 다루며, 기본적인 사용법과 스타일링 아이디어를 제공합니다. 특히 JavaScript를 사용하여 다이얼로그를 열 때 `show()` 대신 모달 기능을 갖춘 `showModal()` 메서드를 사용하는 것이 중요함을 강조합니다. 또한, 닫기 기능 구현 방법 등 다양한 활용 방안을 설명합니다.
핵심 포인트
- `<dialog>` 요소는 웹 아키텍처의 중요한 조각입니다.
- `show()`보다 배경과 위치 지정이 되는 `showModal()` 사용이 권장됩니다.
- 다이얼로그가 열리면 기본적으로 포커스가 맞춰지며, `Esc` 키로 닫을 수 있습니다.
- JavaScript 없이 `<form method="dialog">`를 사용하여 선언적으로 다이얼로그를 닫을 수 있습니다.
네이티브 HTML <dialog> 요소를 얻게 된 날은 정말 굉장하지 않았나요? 아니, 놀라웠죠. 벌써 거의 10년 가까이 되었네요. 그럼에도 불구하고 저는 여전히 이 요소가 어떻게 작동하는지, 언제 사용해야 하는지, 스타일링 아이디어는 무엇인지 등을 찾아보곤 합니다. 이 겉보기에는 작아 보이는 웹 아키텍처 조각에는 많은 미묘한 차이가 있고, 제가 제대로 살펴볼 때가 된 것 같습니다... 미래의 저를 위해서이기도 하지만, 어쩌면 미래의 당신을 위해서일 수도 있겠죠.
<dialog>
마크업은 다음과 같습니다. 이것이 기본 마크업입니다:
<button id="dialog-button">Open Dialog</button>
<dialog id="dialog">...</dialog>
기본적으로는 열리지 않습니다. 우리는 open 속성을 수동으로 설정할 수 있습니다:
<dialog id="dialog" open>...</dialog>
하지만 기본적으로 다이얼로그를 열고 싶은 경우가 언제 있을까요? 희귀한 사용 사례일 겁니다. 대신, JavaScript의 show() 메서드를 사용할 수 있습니다. 다이얼로그와 버튼에 대한 변수를 설정한 다음, 이 메서드를 호출할 수 있습니다:
const dialogButton = document.querySelector('#dialog-button');
const dialog = document.querySelect('#dialog');
formButton.addEventListener('click', () => {
...
이것은 작동하지만, 주의하세요. 이것은 다이얼로그를 *모달(modal)*이라기보다는 *팝업(pop-up)*처럼 취급하며, 대부분의 경우 당신이 원할 것은 모달일 겁니다. 주요 차이점은 무엇일까요? 모달에는 배경(backdrop)이 포함되고, 페이지 중앙에 자동으로 위치하며, Esc 키로 닫을 수 있다는 점입니다.
show()가 배경, 위치 지정, 닫기 기능을 갖추지 않은 것을 주목하세요:
그래서 대신 showModal() 메서드를 호출하는 것이 좋겠습니다:
const dialogButton = document.querySelector('#dialog-button');
const formDialog = document.querySelector('#dialog');
formButton.addEventListener('click', () => {
...
훌륭합니다! 열리고, 위치가 지정되고... 닫힙니다:
닫기 기능에 대해 더 알아보기
이제 다이얼로그가 포커스된 상태일 때(그리고 기본적으로 포커스되어 있다는 것을 주목하세요) Esc 키를 눌러 다이얼로그를 닫을 수 있습니다. 하지만 버튼처럼 다이얼로그를 닫는 UI가 필요하다면, 해당 요소를 내부에 추가할 수 있습니다:
바로 작동하지는 않지만, 여러분이 이미 어떻게 할지 추측하셨을 것이라고 확신합니다. 이를 위한 close() 메서드가 있습니다:
const formButton = document.querySelector('#dialog-button');
const formDialog = document.querySelector('#dialog');
const formClose = document.querySelector('#dialog-close');
...
저는 이에 상응하는 closeModal()이 없다는 것이 조금 재미있거나 이상하다고 생각합니다. 하지만 걱정할 필요는 없습니다. close()가 그냥 작동하기 때문입니다:
그리고 JavaScript를 사용하지 않는 접근 방식을 원한다면, 실제로 HTML에서 선언적으로 처리할 수 있습니다:
<dialog id="dialog">
<form method="dialog">
<button type="submit">Close dialog</button>
...
이것이 시맨틱(semantics)에 어떤 영향을 미치는지 보장할 수는 없지만, JavaScript 없이도 해당 요소를 닫는 것이 실제로 가능합니다.
호출자 명령어 (Invoker Commands)
선언적인 방식으로 닫는 것에 대해 이야기하는 한, 대화 상자를 선언적으로 열고 닫기 위해 특별히 설계된 **호출자 명령어(invoker commands)**라는 진화하는 기능도 언급할 가치가 있습니다. 제가 이 글을 쓰는 지금은 모두 완전히 실험적이지만, 이것을 확인해 보세요:
<button command="show-modal" commandfor="my-dialog">Show Dialog</button>
<dialog id="my-dialog">...</dialog>
맞습니다! command와 commandfor 속성을 사용하여 버튼을 대화 상자에 직접 연결하여 특정 대화 상자(#my-dialog)를 show-modal로 호출할 수 있게 될 것입니다.
멋지네요. 그리고 이것은 닫기 버튼에도 해당됩니다:
<dialog id="my-dialog">
<!-- Close #my-dialog -->
<button command="close" commandfor="my-dialog">Close Dialog</button>
...
그리고 Danny가 여기서 설명하듯이, 이 명령어들을 수신하고 발생할 때 이벤트를 발생시키려면 JavaScript를 사용하여 모든 것을 연결할 수 있습니다:
// 모든 dialog 선택
const dialogs = document.querySelectorAll("dialog");
// 모든 dialog 반복
...
이 지원을 계속 주시하세요!
버튼 레이블링에 대하여
닫기 버튼의 레이블로 “X”를 사용하고 싶을 수도 있습니다 (적어도 SVG 아이콘이라도).
<button id="dialog-button">Open Dialog</button>
<dialog id="dialog">
<button id="dialog-close">X</button>
...
…하지만 이것이 스크린리더가 알리기에는 정확히 가장 좋은 방법은 아닙니다. 여전히 “Close Dialog” 또는 이와 유사한 것을 말하게 하고 싶습니다. 따라서, 만약 “X” 아이콘을 사용하는 데 열정이 있다면, 읽고 싶은 텍스트를 포함하는 <span> 태그를 추가하고 aria-hidden 속성을 사용하여 시각적으로 숨기면서도 아이콘이 발표되는 것을 방지하는 것이 좋습니다:
<button id="form-button">Open Dialog</button>
<dialog id="form-dialog">
<button id="form-close">
...
접근성 측면에서 한 가지 더 알아둘 점이 있습니다. 다이얼로그가 열릴 때 닫기 버튼에 포커스가 가는 것을 보셨나요?

원치 않을 수도 있고, 원할 수도 있습니다. 왜냐하면 이제 Space 키를 실수로 누르면 버튼이 예기치 않게 다이얼로그를 닫을 수 있기 때문입니다. 세상의 끝은 아니지만, 모달(modal)을 닫는 것은 파괴적인 행위가 아니며 다시 열 수 있다는 점에서 그렇습니다. 하지만 만약 다이얼로그 내에 다른 포커스 가능한 요소(예: 링크나 폼 필드)가 있다면, tabindex 속성을 사용하여 그중 하나에 초기 포커스를 주는 것을 고려해 보세요.
본질적인 비활성화 (Innate inertness)
배경 화면을 스타일링하는 것에 대해 이야기하고 싶지만, 그 전에 열린 다이얼로그 뒤의 페이지가 inert 상태라는 점을 언급할 가치가 있다고 생각합니다. 즉, 텍스트 선택, 버튼 클릭, 포커스, 입력 등 어떤 종류의 상호작용도 불가능합니다. 어쨌든 이것이 원하는 것이므로, 기본적으로 설정할 필요가 없다는 것은 좋습니다. 다만, 실제로 마크업에서 inert 속성을 보지는 못할 것입니다.
하지만 이것은 다이얼로그가 *모달(modal)*로 설정된 경우에만 해당됩니다. 가장 처음 데모를 기억하십니까? 클릭 시 다이얼로그를 열기 위해 더 명시적인 showModal() 대신 show() 메서드를 사용했습니다.
. 즉, 첫 번째 데모는 포커스를 가두고 가장 상위 레이어에 위치하는 진정한 어텐션(attention)을 독점하는 모달(modal)이라기보다는 포버(popover)(툴팁 같은 것들을 생각해보세요)와 비슷한 것을 보여주고 있습니다.
여러분이 동시에 열리는 경쟁적인 다이얼로그, 예를 들어 포버와 모달 같은 것이 궁금할 수 있습니다. 음, 애초에 어떻게 그것들을 동시에 열었을까요? inert 동작을 유발하지 않기 때문에 먼저 다이얼로그 포버를 열어야 합니다. 모달 다이얼로그만 그렇게 합니다. 그리고 모달 다이얼로그가 열려 있을 때, 포버 다이얼로그는 최상위 레이어에 있지 않아 접근할 수 없게 됩니다.
어쨌든, 스타일링으로 넘어가 봅시다!
배경(backdrop) 스타일링하기
여기서부터 시작하는 것이 좋을 것 같습니다. 기본적으로 스타일링된 배경을 보는 것이 엄청나게 어렵다고 생각합니다. 마지막 예제에서 다이얼로그를 열면 페이지 배경이 약간 색조가 도는 것을 알아차릴 수 있습니다. 아주 많이는 아니지만요. 그것이 바로 배경(backdrop)입니다.

매우 미묘하죠. 우리는 ::backdrop 의 유사 요소(pseudo-element)를 사용하여 직접 스타일링할 수 있습니다. 예를 들어, 완전히 단색으로 만들 수도 있습니다:
하지만 이제 전체 페이지를 가리고 있습니다. 괜찮을 수도 있지만, 약간의 투명도와 어쩌면 작은 blur() 액션이 더해지면 나쁠 건 없다고 생각합니다. 왜냐하면, 아시다시피, 컨텍스트가 중요하니까요:
사실 저는 CSS-Tricks Almanac에 있는 Mojtaba의 배경 이미지 예제가 정말 마음에 듭니다. 비록 그것이 페이지 나머지 부분을 가리는 것에 대한 제 감정과 모순되지만 말이죠:
테두리 및 배경 스타일링하기
<dialog> 의 스타일을 설정하는 두 가지 매우 명백한 기본값이 있습니다: 순수한 흰색 배경과 크고 굵은 검은색 테두리입니다. 원하시면 그대로 두는 것도 완전히 괜찮습니다. 아니면, 그럴 필요가 없을 수도 있고요.

사용자 정의 스타일이 <dialog> 요소에 바로 적용될 것이라고 생각할 수 있습니다:
/* 👎 */
dialog {
background-color: gold;
...
하지만 실제로는 그것을 open 상태에서 선택하고 싶습니다:
dialog {
/* ... */
&[open] {
...```
위에 있는 DevTools 스크린샷에서 `:modal` 유사 클래스가 심지어 `:open`보다 더 높은 특이성(specificity)을 가지고 있다는 것을 알아차리셨을 수도 있습니다.
. 오버라이드가 필요한 경우에도 이 방법을 완전히 사용할 수 있습니다.
하지만 `:open` 가상 클래스에 주의하세요. 제가 글을 쓰는 오늘, Safari 26.5가 이를 지원하게 되었습니다. 더 깊은 지원이 필요하다면 대신 `[open]` 속성을 선택하거나 단순히 `:modal` 을 사용하는 것을 고려해 보세요.
John Rouillard가 댓글에서 좋은 메모를 공유하며, 다이얼로그가 열릴 때 스크롤바를 제거하는 것이 실제로 배경 요소의 너비를 변경한다는 점을 상기시켜 주었습니다. 따라서 의도치 않은 변화를 방지하려면 스크롤바를 표시해야 합니다:
dialog {
&[open] {
scrollbar-gutter: stable;
...
## 위치 스타일링하기
덜 명확한 기본 다이얼로그 스타일 중 하나는 뷰포트 중앙에 배치되는 방식입니다. 개발자 도구를 열면 이를 수행하는 UA(User Agent) 스타일링을 볼 수 있습니다:

다이얼로그를 뷰포트 상단과 조금 더 밀착시키기 위해 `margin-top` 을 오버라이드할 수 있습니다:
하지만 아마도 *원하지 않을* 것은 다이얼로그의 `display` 를 오버라이드하는 것입니다. 이는 초기 닫힌 상태에서 `display: none` 으로 설정되어 있습니다. 이 설정을 요소 자체에 `block` 과 같은 것으로 변경하면 모달을 사용하는 모든 목적(기본적으로 닫히는 것)을 완전히 잃게 됩니다. 여전히 기본적인 열고 닫기는 가능하지만, 편리한 `Esc` 키 기능은 사라집니다.
그리고 사용자 정의 스타일이 `:open` 상태에서만 적용되는 것을 주목하세요. 왜냐하면 그곳에 존재하기 때문입니다:
## 열렸을 때 스크롤 방지하기
아마도 `::backdrop` 뒤의 콘텐츠가 스크롤되기를 원하지 않을 것입니다. 이것은 사용자가 다이얼로그를 열었을 때 있던 페이지 위치와 완전히 다른 곳으로 이동할 수 있는 상황 중 하나입니다.
기본적으로 하부 콘텐츠가 제자리에 고정되면 정말 좋을 텐데, 그것이 안 되는 이유도 완벽하게 이해됩니다: **다이얼로그는 스크롤 컨테이너가 아닙니다.** 만약 그렇다면, `overscroll-behavior: contain` 을 적용하고 끝낼 수 있을 것입니다.
글쎄요, Chrome 144에서 이 부분을 약간 수정하여 `overscroll-behavior`
스크롤 가능한 컨테이너가 아닌 곳에서도 작동합니다. 따라서 Chrome 144 이상을 가정한다면, 해당 동작을 dialog와 그 배경(backdrop)에 설정할 수 있습니다:
dialog {
overscroll-behavior: contain;
&::backdrop {
...
마지막으로 필요한 것은 dialog 자체를 스크롤 컨테이너로 만들어야 한다는 것입니다:
dialog {
overflow: hidden;
overscroll-behavior: contain;
...```
Chrome 144 이상 필요:
그것도 좋지만, 광범위한 브라우저 지원을 위해 더 간결하게 처리하는 방법은 body 요소가 :has()
open 속성을 가진 dialog를 가지고 있는지 확인하는 것입니다. 만약 그렇다면, body의 오버플로우를 숨깁니다:
body:has(dialog[open]) {
overflow: hidden}
그렇긴 하지만, 저는 overscroll-behavior 접근 방식이 더 선언적이고 우리가 선택하는 요소에 연결되어 있어서 마음에 듭니다.
dialog 스타일링 창의 아이디어 발휘하기
Andy Clarke는 기본적인 콘텐츠 박스 이상의 방식으로 dialog를 스타일링하는 창의적인 방법에 대한 전체 글을 작성했습니다. 이는 저희가 다루는 범위를 벗어나지만, 읽어볼 가치가 충분합니다. 식욕을 돋울 한 가지 예시를 소개합니다:
dialog 애니메이션하기
dialog는 열리고 닫힐 때 일종의 ‘팝’하는 느낌으로 나타납니다. 하지만 진입하고 나갈 때 약간의 애니메이션 효과를 줄 수 있습니다.
예를 들어, dialog가 서서히 페이드인(fade in) 되도록 만들면 어떨까요? 이렇게 생각할 수도 있습니다:
/* 안돼요! 👎 */
dialog {
oppacity: 0;
...```
하지만 아닙니다. 요소가 DOM에 렌더링되는 순간의 *시작 스타일(starting style)*을 명시적으로 설정해야 합니다. 이 경우, dialog는 기본적으로 `display: none`이며 활성화될 때 opacity가 설정되어 있지 않습니다. 바로 여기에 `@starting-style`
규칙이 사용됩니다:
/* 네! 👍 */
@starting-style {
dialog:open {
...```
여기 있습니다:
뷰에 진입하고 나가는 것? 그건 사실 View Transitions API의 영역이네요! 하지만, 아아, 다이얼로그는 그 용도로 적합하지 않습니다. 왜냐하면 모달 다이얼로그는 최상위 레이어에 존재하며, 이를 닫을 때 항상 이름 지정된 전환(named transition)에 대한 신뢰할 수 있는 이전/새로운 쌍(old/new pair)을 생성하는 방식이 아니기 때문입니다.
여기 view-transition-name을 다이얼로그 요소에 설정하고 그 다음 ::view-transition-new()를 사용하는 예시가 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 CSS-Tricks의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기