원문: https://www.matuzo.at/blog/2025/whats-an-interactive-element

2년 전에 저는 dialog 요소에 관한 글을 썼습니다. showModal() 메서드로 모달 다이얼로그를 열었을 때 포커스가 어디로 가는지 테스트했습니다. 요소와 속성을 여러 조합으로 바꿔 가며 어떤 일이 일어나는지 살펴봤는데, 2023년 당시에는 동작이 무척 일관되지 않았기 때문입니다.
테스트 중 하나에서 저는 dialog 요소에 tabindex 속성을 넣었습니다. 이번 주 초에 Joy가 저에게 연락한 것도 그래서였습니다. MDN의 dialog 요소 페이지에 다음과 같이 쓰여 있어서, 그게 좋은 방법인지 확신이 서지 않았던 겁니다.

그리고 사용 시 주의 사항은 이렇습니다.
<dialog>요소는 인터랙티브하지 않고 포커스도 받지 않으므로 tabindex 속성을 추가하지 마세요. 다이얼로그 안에 들어 있는 닫기 버튼을 포함해 다이얼로그의 콘텐츠는 포커스를 받을 수 있고 인터랙티브할 수 있습니다.
저는 이 경고를 무시하라고 권했고, 블로그 글로 제대로 된 더 자세한 답을 주겠다고 약속했습니다.
일러두기
제 의견을 풀어놓기 전에, 이 글은 MDN을 겨냥한 글이 아니라는 점을 분명히 해 두겠습니다. MDN은 제가 매일 방문하는 몇 안 되는 웹사이트 중 하나이고, 저는 MDN의 열렬한 팬입니다. 그 콘텐츠가 오픈 소스이고 커뮤니티가 함께 만들어 간다는 점도 이해합니다. 누구나 언젠가는 어떤 의견에 동의하지 않기 마련입니다.
dialog 요소에 tabindex 사용하기
이번 주 내내 이 경고가 머릿속을 떠나지 않았습니다. 작성자들이 왜 이걸 그렇게까지 강조하는지 이해가 되지 않았기 때문입니다. 페이지 앞부분에 dialog 요소에 tabindex 속성을 쓰면 안 된다는 큼지막하고 새빨간 경고가 떡하니 붙어 있습니다.
어디에서 나온 이야기인지는 압니다. dialog 요소에 대한 HTML 명세에는 이렇게 쓰여 있습니다.
tabindex 속성은 dialog 요소에 지정해서는 안 됩니다.
그러니 엄밀히 따지면 그들 말이 맞습니다. 명세가 dialog 요소에 tabindex 속성을 쓰면 안 된다고 하니까요. 다만 저는 그 이유를 모르겠고, 적어도 그 표현이 이해되지 않습니다. dialog 요소에 tabindex 속성을 써도 전혀 문제가 없기 때문입니다. 아무것도 망가뜨리지 않습니다. 쓰지 말아야 할 이유로 제가 떠올릴 수 있는 건 굳이 필요하지 않다는 것뿐입니다. dialog 요소는 이미 스스로 포커스를 잘 처리합니다. 제가 놓친 게 있는지는 모르겠지만, 저라면 "dialog 요소에 tabindex 속성을 넣는 것은 필요하지 않습니다" 정도로 바꿔 쓰겠습니다.
이 경고가 유난히 눈에 띄게 꾸며진 것도 거슬리지만, 정말로 신경 쓰이는 건 사용 시 주의 사항에 적힌 내용입니다.
<dialog>요소는 인터랙티브하지 않고 포커스도 받지 않으므로 tabindex 속성을 추가하지 마세요.
두 주장 모두 틀렸습니다. dialog 요소는 인터랙티브 요소이기 때문에 포커스를 받습니다. 직접 해 보세요.
역주: 라이브 데모는 원문에서 확인하실 수 있습니다.
HTML:
<dialog>
<h1>Demo 1</h2>
</dialog>
JS:
button.addEventListener('click', e => {
dialog.showModal();
output.textContent = document.activeElement.tagName
});
이 문장들이 너무 끔찍해서 제가 이 글을 쓰는 것처럼 들리겠지만, 그렇지 않습니다. 제가 이 글을 쓰는 이유는 인터랙티브 요소가 무엇이고 포커스 가능하다는 말이 정말로 무슨 뜻인지에 대해 전반적인 오해가 있다고 느끼기 때문입니다. 저도 100% 확신하지는 못했기에 조사를 좀 해 봤습니다.
인터랙티브 요소
HTML 명세의 포커스에 관한 절의 첫 문장은 이렇습니다.
HTML 사용자 인터페이스는 보통 폼 컨트롤, 스크롤 가능한 영역, 링크, 다이얼로그 박스, 브라우저 탭 등 여러 인터랙티브 위젯으로 이루어집니다. 이 위젯들은 계층 구조를 이루며, 그중 일부(예: 브라우저 탭, 다이얼로그 박스)가 다른 위젯(예: 링크, 폼 컨트롤)을 담습니다.
다이얼로그는 다른 인터랙티브 요소를 담을 수 있는 인터랙티브 요소입니다. 필요한 정보는 이미 얻었으니 여기서 멈춰도 되지만, 조금 더 나아가 보겠습니다. 인터랙티브 요소는 포커스를 받을 수 있고, 그래서 포커스 가능 영역(focusable areas)이 됩니다.
포커스 가능 영역은 다음과 같습니다.
- 다음 기준을 모두 충족하는 요소 (아래 예시 참고)
- 요소의 tabindex 값이 null이 아니거나, 사용자 에이전트가 그 요소를 포커스 가능하다고 판단한다
- 요소가 섀도우 호스트가 아니거나, delegates focus가 false인 섀도우 루트를 가진다
- 요소가 실제로 비활성화되어 있지 않다
- 요소가 inert 상태가 아니다
- 요소가 렌더링되고 있거나, 렌더링을 자식에게 위임하고 있거나, 관련 캔버스 대체 콘텐츠로 쓰이고 있다
- 이미지 맵에 있는 area 요소의 도형
- 사용자 에이전트가 제공하는 요소의 하위 위젯
예를 들어 video 요소의 사용자 인터페이스에 있는 컨트롤, 그리고<input type=number>의 스핀 컨트롤 형태에 있는 위아래 버튼이 여기에 해당합니다.<video controls> </video> - 요소의 스크롤 가능한 영역
div도 인터랙티브 요소가 될 수 있습니다. 다만 스크롤 가능한 요소는 레이블이 붙은 시맨틱 요소로 만드는 편이 좋습니다.<div style="inline-size: 10rem; overflow: auto;"> <div style="inline-size: 20rem"> I'm overflowing </div> </div> - 브라우징 컨텍스트가 null이 아니고 inert 상태가 아닌 문서의 뷰포트
예를 들어 iframe의 콘텐츠가 여기에 해당합니다. 참고로 iframe으로 전달된 키 이벤트는 곧바로 그 iframe의 활성 문서로 다시 전달됩니다. - 사용자 에이전트가 포커스 가능 영역이라고 판단한 그 밖의 요소 또는 요소의 일부. 특히 접근성을 돕거나 플랫폼 관례에 더 잘 맞추기 위한 경우가 그렇습니다.
예시
요소의 tabindex 값이 null이 아니다
<section tabindex="-1" aria-label="Results">
</section>
사용자 에이전트가 그 요소를 포커스 가능하다고 판단한다
<dialog></dialog>
요소가 delegates focus가 false인 섀도우 루트를 가진다
평소라면 포커스를 받을 수 있는 요소라도 섀도우 루트가 포커스를 위임하면 포커스를 받지 않습니다. 다음 예시에서 the-button은 tabindex가 0으로 설정되어 있는데도 포커스할 수 없습니다. 중첩된 button에 포커스를 위임하기 때문입니다.
html
<the-button tabindex="0">
This is my button
</the-button>
CSS
the-button {
border: 4px solid;
padding: 1rem;
}
JS
class TheButton extends HTMLElement {
constructor() {
super();
this.attachShadow({
mode: "open",
delegatesFocus: true
});
const button = document.createElement("button");
button.textContent = "Focus me";
this.shadowRoot.append(button);
}
}
customElements.define("the-button", TheButton);
요소가 실제로 비활성화되어 있지 않다
요소에 disabled 속성이 붙어 있지 않다는 뜻입니다.
<button disabled>This button is actually disabled</button>
요소가 inert 상태가 아니다
요소에 inert 속성이 붙어 있지 않다는 뜻입니다.
<button inert>This button is inert</button>
요소가 렌더링되고 있다
요소에 연결된 CSS 레이아웃 박스가 하나라도 있다는 뜻입니다.
<button hidden>This button is not being rendered</button>
<button style="display: none">This button is not being rendered</button>
요소가 렌더링을 자식에게 위임하고 있다
요소 자신은 렌더링되지 않지만 그 자식은 렌더링될 수 있다는 뜻입니다.
<div style="display: contents">
This div is not being rendered, but…
<button>…this button is being rendered</button>
</div>
포커스 가능한 요소
앞 장에서 쓴 내용은 대부분 여러분에게 새롭지 않았을 겁니다. 인터랙티브 요소가 포커스 가능하다는 건 이미 알고 계실 테니까요. 그런데 포커스 가능하다는 건 무슨 뜻일까요?
요소는 focus() 메서드나 autofocus 속성처럼 프로그래밍 방식으로 포커스를 줄 수 있을 때 포커스 가능합니다.
포커스 가능한 요소는 순차적으로 포커스 가능(sequentially focusable)하거나, 클릭으로 포커스 가능(click focusable)하거나, 둘 다이거나, 둘 다 아닐 수 있습니다. 이 두 용어는 사용자 에이전트가 사용자 인터렉션에 어떻게 반응하는지를 결정합니다.
순차적으로 포커스 가능
사용자는 Tab 키를 눌러 순차적으로 포커스 가능한 요소에 도달할 수 있습니다.
플랫폼 관례를 고려해 명세는 다음 요소들이 순차적으로 포커스 가능해야 한다고 제안합니다.
- href 속성이 있는
<a> <button><input>, 단<input type="hidden">은 제외<select><textarea>- details 요소의 첫 번째 summary 자식 요소인
<summary> - 편집 호스트(
[contenteditable]요소, 또는 design mode enabled가 true인 Document의 자식 HTML 요소) - 내비게이션 가능한 컨테이너(예:
<iframe>) [tabindex]가 0 이상인 요소
이런 요소들을 순차적으로 포커스 가능하다고 부르는 대신, 탭 이동 가능(tabbable)하다고 말해도 됩니다.
클릭으로 포커스 가능
사용자는 클릭으로 포커스 가능한 요소를 클릭해서 도달할 수 있습니다.
HTML:
<button>button</button>
<dialog open>dialog</dialog>
<div tabindex="-1">tabindex=-1</div>
CSS:
:focus {
background: aqua;
}
다음 요소 중 하나를 클릭하거나 탭으로 이동해 포커스를 줘 보세요.
역주: 라이브 데모는 원문에서 확인하실 수 있습니다.
button은 순차적으로도, 클릭으로도 포커스 가능합니다.
열린 dialog는 파이어폭스(144)에서는 클릭으로만 포커스 가능하지만, 크롬 141과 사파리 18.6에서는 순차적으로도 포커스 가능합니다(흥미롭네요!).
음수 tabindex를 가진 div는 클릭으로만 포커스 가능합니다.
즉 어떤 요소는 순차적으로 포커스 가능하지 않아도(탭 이동이 되지 않아도) 포커스 가능할 수 있습니다.
순차적으로도, 클릭으로도 포커스할 수 없는 경우
더 복잡하게도, 어떤 요소는 순차적으로도 클릭으로도 포커스할 수 없으면서 프로그래밍 방식으로는 여전히 포커스할 수 있습니다.
이 입력 필드는 Tab을 쓰거나 클릭해서는 포커스할 수 없지만, 레이블이나 focus 버튼을 클릭하면 포커스할 수 있습니다.
역주: 라이브 데모는 원문에서 확인하실 수 있습니다.
HTML:
<label for="r">Read only</label>
<input type="text" id="r" tabindex="-1" readonly>
CSS:
input {
pointer-events: none;
}
JS:
document.querySelector('button').addEventListener('click', () => {
document.querySelector('input').focus();
})
참고로 읽기 전용 input 요소는 기본적으로 탭 이동이 가능합니다. 이 데모를 위해 tabindex 속성을 덧붙였을 뿐입니다.
인터랙티브하지 않은 요소를 인터랙티브하게 만들기
아직 답하지 않은 질문이 하나 더 있습니다. 인터랙티브하지 않은 요소를 인터랙티브하게 만들어도 괜찮을까요?
MDN이 tabindex 속성에 관한 페이지에서 이에 대해 하는 말은 이렇습니다.
인터랙티브하도록 의도한 요소를 키보드 입력으로 포커스 가능하게 만들려고, 인터랙티브하지 않은 콘텐츠에 tabindex 속성을 함께 쓰는 일은 피하세요.
인터랙티브 요소라는 용어에 대한 그들의 잘못된 정의를 감안하면 이 문장은 말이 되지 않습니다. 순차적으로 포커스 가능한 요소에만 tabindex 속성을 넣어야 한다는 뜻이 되기 때문입니다. -1이나 0보다 큰 숫자는 쓰면 안 된다고 하니 남는 건 0뿐인데, 0은 이미 포커스 가능한 요소에는 아무 영향도 주지 않습니다.
접근성에 도움이 된다면 인터랙티브하지 않은 요소에 tabindex=-1을 넣는 것은 전혀 문제가 없습니다. 클라이언트 사이드 렌더링 애플리케이션에서 사용자를 페이지 안의 특정 위치로 이동시키고 싶을 수 있습니다. 예를 들어 검색 위젯이라면, 사용자가 검색 버튼을 클릭한 뒤 모든 검색 결과를 담고 있는 컨테이너에 포커스를 줄 수 있습니다.
<section tabindex="-1" aria-labelledby="heading">
<h2 id="heading">42 search results</h2>
…
</section>
그다음 문단은 이 문장으로 시작합니다.
인터랙티브하지 않은 요소로 작성한 인터랙티브 컴포넌트는 접근성 트리에 표시되지 않습니다.
이 문장은 정확하지 않지만, 무엇을 말하려는지는 압니다. 제네릭 요소에는 tabindex를 설정하지 말라는 것입니다. 저도 동의합니다. tabindex를 쓴다면 앞선 예시처럼 레이블이 붙은 시맨틱 요소에 쓰세요. 또 다른 예시를 보여 드리겠습니다.
<h2 tabindex="-1">42 search results</h2>
이렇게 하는 건 괜찮습니다. 요소를 프로그래밍 방식으로는 포커스 가능하게 만들되 순차적으로 포커스 가능하게 만들지는 않기 때문입니다. 대부분의 경우 인터랙티브하지 않은 요소를 순차적으로 포커스 가능한 요소로 바꿔서는 안 됩니다. 다시 말해 div나 h2를 탭 이동 가능하게 만들지 말고 button을 쓰세요.
tl;dr
자, 와, 이야기가 좀 커졌네요.
짧게 요약하면 이렇습니다. 인터랙티브 요소는 포커스 가능합니다. 하지만 포커스 가능하다고 해서 반드시 탭 이동이 가능한 건 아닙니다. 탭 이동은 되지 않지만 프로그래밍 방식이나 클릭으로 포커스할 수 있는, 클릭으로 포커스 가능한 요소도 있기 때문입니다.
그리고 접근성에 도움이 된다면 인터랙티브하지 않은 요소에 tabindex를 넣어도 전혀 문제가 없습니다.
'Web Frontend Developer' 카테고리의 다른 글
| [번역] 원샷으로 브라우저 API 만들기 (0) | 2026.09.21 |
|---|---|
| [번역] 프로그레시브 이미지 렌더링의 현재와 잠재적 미래 (1) | 2026.08.09 |
| [번역] Fetch 스트림은 훌륭하지만, 업로드/다운로드 진행 상황 측정에는 적합하지 않습니다 (0) | 2026.08.09 |
| [번역] 컴포넌트가 WCAG를 준수할 수 있을까요? (0) | 2026.06.05 |
| [번역] 타입스크립트 성능 문제 해결하기 — 사례 연구 (0) | 2026.06.05 |