다크 모달 컴포넌트
접근성을 갖춘 다크 테마 모달 다이얼로그. 키보드 트랩, ESC 닫기, 스크롤 잠금 포함.
<button class="btn-open-modal" data-modal="demo-modal">모달 열기</button>
<div class="modal" id="demo-modal" role="dialog" aria-modal="true" aria-labelledby="modal-title" hidden>
<div class="modal-backdrop"></div>
<div class="modal-container">
<div class="modal-header">
<h2 class="modal-title" id="modal-title">모달 제목</h2>
<button class="modal-close" aria-label="닫기">×</button>
</div>
<div class="modal-body">
<p>모달 본문 내용을 여기에 작성하세요. 접근성을 위해 포커스가 모달 내부에 갇힙니다.</p>
</div>
<div class="modal-footer">
<button class="btn btn-ghost modal-close-btn">취소</button>
<button class="btn btn-primary">확인</button>
</div>
</div>
</div>
<style>
:root {
--modal-bg: #161b22; --modal-border: #30363d; --modal-overlay: rgba(0,0,0,0.7);
--text: #e6edf3; --text-muted: #8b949e; --accent: #58a6ff; --radius: 12px;
}
.btn-open-modal, .btn {
display: inline-flex; align-items: center; padding: 0.5rem 1.25rem;
border-radius: 6px; font-size: 0.875rem; font-weight: 500;
border: 1px solid transparent; cursor: pointer; transition: all 0.15s;
}
.btn-open-modal { background: #1f6feb; color: #fff; border-color: #58a6ff; }
.btn-primary { background: #1f6feb; color: #fff; border-color: #58a6ff; }
.btn-primary:hover { background: #58a6ff; color: #000; }
.btn-ghost { background: #21262d; color: var(--text-muted); border-color: var(--modal-border); }
.btn-ghost:hover { background: #30363d; color: var(--text); }
.modal { position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; }
.modal[hidden] { display: none; }
.modal-backdrop { position: absolute; inset: 0; background: var(--modal-overlay); backdrop-filter: blur(4px); }
.modal-container {
position: relative; width: min(520px, calc(100vw - 2rem));
background: var(--modal-bg); border: 1px solid var(--modal-border);
border-radius: var(--radius); box-shadow: 0 20px 60px rgba(0,0,0,0.5);
display: flex; flex-direction: column; max-height: 90vh;
}
.modal-header {
display: flex; align-items: center; justify-content: space-between;
padding: 1.25rem 1.5rem; border-bottom: 1px solid var(--modal-border);
}
.modal-title { font-size: 1.125rem; font-weight: 600; color: var(--text); }
.modal-close {
width: 32px; height: 32px; display: flex; align-items: center; justify-content: center;
background: none; border: none; border-radius: 6px; color: var(--text-muted);
font-size: 1.25rem; cursor: pointer; transition: all 0.15s;
}
.modal-close:hover { background: #30363d; color: var(--text); }
.modal-body { padding: 1.5rem; overflow-y: auto; color: var(--text-muted); line-height: 1.6; flex: 1; }
.modal-footer {
padding: 1rem 1.5rem; border-top: 1px solid var(--modal-border);
display: flex; justify-content: flex-end; gap: 0.75rem;
}
</style>
<script>
const FOCUSABLE = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
function openModal(modal) {
modal.hidden = false;
document.body.style.overflow = 'hidden';
modal.querySelector(FOCUSABLE)?.focus();
}
function closeModal(modal) {
modal.hidden = true;
document.body.style.overflow = '';
}
document.querySelectorAll('[data-modal]').forEach(btn => {
btn.addEventListener('click', () => {
const m = document.getElementById(btn.dataset.modal);
if (m) openModal(m);
});
});
document.querySelectorAll('.modal').forEach(modal => {
modal.querySelector('.modal-backdrop')?.addEventListener('click', () => closeModal(modal));
modal.querySelectorAll('.modal-close, .modal-close-btn').forEach(b => b.addEventListener('click', () => closeModal(modal)));
modal.addEventListener('keydown', e => {
if (e.key === 'Escape') { closeModal(modal); return; }
if (e.key !== 'Tab') return;
const focusable = [...modal.querySelectorAll(FOCUSABLE)];
const first = focusable[0], last = focusable[focusable.length - 1];
if (e.shiftKey ? document.activeElement === first : document.activeElement === last) {
e.preventDefault();
(e.shiftKey ? last : first).focus();
}
});
});
</script>