14장 웹 표준 우선
동작은 먼저 HTML과 CSS 표준에 맡기고, JavaScript는 표준이 하지 못하는 일에만 씁니다. 그래서 같은 클래스 조합이 어느 프레임워크에서도 같은 화면을 만듭니다.
왜 필요한가
오버레이와 위치 계산을 구현마다 다른 방식으로 만들면 같은 종류의 UI가 다르게 움직입니다. 드롭다운 메뉴·분할 버튼·셀렉트가 세 가지 위치 계산 방식을 따로 쓰던 때, 한쪽에만 대피 규칙이 빠져 모바일에서 메뉴가 화면 밖으로 밀려났습니다. 그 방식이 프레임워크 JavaScript라면 프레임워크를 바꿀 때 동작을 다시 만들어야 하고, AI 에이전트는 화면마다 조금씩 다른 JS 헬퍼를 만들어 냅니다. 표준 메커니즘은 어느 프레임워크에서나 같습니다.
흉내 낸 구현은 접근성도 놓칩니다. 이전의 비모달 시트는 화면 전체를 덮고 배경을 어둡게 깔아 모달처럼 보였습니다. 하지만 포커스 가두기, 배경 비활성(inert), aria-modal이 없었습니다. 눈에 보이는 것과 보조기술이 아는 것이 달랐습니다.
원칙
HTML/CSS-First. DS는 HTML의 class 조합만으로 동작합니다. HTML 조각은 별도 빌드나 설정 없이 복사해 붙여도 동작합니다(copy-paste ready).
네이티브 메커니즘을 먼저 씁니다.
필요한 동작 표준 메커니즘 모달 창 <dialog>+ Invoker Commands(command="show-modal"·commandfor)비모달 팝업·메뉴 popover속성 +popovertarget제자리에서 펼치고 접기 <details>·<summary>트리거에 붙여 띄우기 CSS anchor positioning( anchor-name,anchor(),position-try-fallbacks)모달을 닫는 수단 고르기(바깥 클릭·Esc) <dialog>의closedby속성프레임워크 JS로 표준을 대체하지 않습니다. floating-ui 같은 위치 계산 라이브러리나 손으로 쓴 헬퍼를 들여와 표준이 할 일을 대신 하게 하지 않습니다.
Chromium을 우선합니다. 우선 대상 엔진은 Chromium(데스크톱 Chrome·Edge, Android Chrome)입니다. 표준 API가 일부 엔진(예: WebKit)에서 아직 완전하지 않은 부분은 표준의 현재 상태로 받아들입니다. 지원하지 않는 브라우저에는 CSS 대체 규칙(
@supports)이나 표준의 안전한 기본 동작을 주고, JS로 우회하지 않습니다.외부 라이브러리는 공식 API만 씁니다. 내부 자식 요소 선택자, 문서에 없는 이벤트, 소스를 읽고 추론한 동작, 공개되지 않은 CSS 변수에 기대지 않습니다. 메이저 업그레이드에서 예고 없이 깨지기 때문입니다. 스타일이 맞지 않으면 공식 테마·prop·slot 안에서 비슷하게만 맞춥니다.
프레임워크 독립과 교체 가능한 primitive
| 층 | 맡는 일 |
|---|---|
DS(leysys-design CSS) | 색, 간격, 글자, 그림자, 모서리, 애니메이션 |
| Primitive UI(Reka UI 등) | 키보드 조작, 포커스 관리, 위치 계산, ARIA·상태 속성 |
| 제품 | DS 클래스와 primitive의 조립, 업무 로직 |
DS CSS는 class 속성만 읽으므로 Vue·React·Svelte 어디서든 같게 동작합니다. 공식 Vue wrapper는 leysys-design이 제공하고(15장), React·Svelte wrapper는 후순위입니다.
Primitive UI는 동작과 접근성만 제공하고 스타일은 맡기는 라이브러리(headless primitive)입니다. shadcn/ui가 Radix UI와 Base UI 중 하나를 고르게 하듯, primitive는 바꿀 수 있는 층입니다. 그래서 DS CSS는 상태를 읽을 때 W3C ARIA 속성과 여러 primitive가 공통으로 쓰는 속성(data-state, data-side, data-align, data-orientation)을 씁니다. [data-reka-*]처럼 특정 라이브러리 전용 속성은 선택자로 쓰지 않습니다. 2026-10-04 KST에 EDS·MDS·Print DS CSS를 검색한 결과 그런 선택자는 0건이었습니다. 제품의 권장 조합은 Vue 3, Tailwind CSS v4, Reka UI(Drawer만 vaul-vue)입니다.
실제 예
오버레이 네 개를 한 구조로
Dialog·AlertDialog·Sheet·Drawer는 HTML/CSS에서 같은 구조이고 클래스만 다릅니다. 모달과 비모달은 속성 하나와 트리거만 다릅니다.
html
<!-- 모달: 맨 위 층, 배경 비활성, 포커스 가두기를 브라우저가 처리 -->
<dialog id="edit" class="e-sheet e-sheet-right" closedby="any" aria-labelledby="edit-title">…</dialog>
<button command="show-modal" commandfor="edit">편집</button>
<!-- 비모달: popover 속성만 더하고, 배경은 그대로 조작할 수 있음 -->
<dialog id="filter" class="e-sheet e-sheet-right" popover>…</dialog>
<button popovertarget="filter">필터</button><dialog>는 기본이 비모달입니다. 모달로 열면 브라우저가 맨 위 층(top layer)에 그리고, 배경을 비활성으로 만들고, 포커스를 가둡니다. 직접 만들 필요가 없습니다. 배경을 어둡게 하는 규칙은 모달(:modal::backdrop)에만 적용합니다.
모달을 무엇으로 닫을지는 JS 클릭 핸들러가 아니라 closedby 속성으로 정합니다. HTML 표준은 바깥(배경)을 눌러 닫는 light dismiss와 Esc 같은 닫기 요청(close request)을 따로 다룹니다. any는 둘 다, closerequest는 닫기 요청만 받고, none은 둘 다 받지 않아 command="close" 버튼처럼 화면이 둔 수단으로만 닫힙니다. 속성이 없는 모달은 closerequest처럼 동작합니다. 일반 콘텐츠 모달(Dialog·Sheet·Drawer)은 any를 씁니다. 확인 대화상자(AlertDialog)는 삭제·서명 확인이 잘못 누른 바깥 클릭으로 사라지지 않도록 light dismiss를 주지 않습니다. 기본은 속성을 생략하거나 closerequest를 적어 닫기 요청만 받고, 응답을 반드시 받아야 하는 잠금 모드만 none을 씁니다. 이미 closerequest·none을 명시한 곳은 의도한 선택으로 보고 유지합니다. 클릭 좌표를 JS로 판정하는 안은 표준 속성을 헬퍼로 대신하는 것이라 기각했습니다.
펼칠 것인가, 띄울 것인가
MDS 행동 메뉴(Action Menu)는 처음에 <details> 안에서 목록을 position: absolute로 띄웠습니다. 부모에 overflow: hidden이 있으면 목록이 잘렸고, z-index로는 잘림을 고칠 수 없었습니다. 다시 따져 보니 행동 메뉴는 본문을 제자리에서 펼치는 disclosure가 아니라, 버튼에 붙어 잠깐 뜨는 목록이었습니다. 그래서 popover로 바꿔 top layer에 그리고 CSS anchor positioning으로 버튼에 붙였습니다.
css
.m-action-menu { anchor-scope: --m-action-menu-trigger; } /* 인스턴스마다 기준점을 격리 */
.m-action-menu-button { anchor-name: --m-action-menu-trigger; }
.m-action-menu-menu[popover] {
position-anchor: --m-action-menu-trigger;
top: anchor(bottom);
inset-inline-start: anchor(start);
position-try-fallbacks: flip-inline, flip-block, flip-block flip-inline; /* 공간이 없으면 뒤집기 */
}
@supports not (anchor-name: --a) {
/* anchor positioning이 없는 브라우저는 화면 가운데에 고정해 넘치지 않게 함 */
}JS 위치 계산 헬퍼는 넣지 않았습니다. 같은 방식이 EDS 드롭다운 메뉴와 분할 버튼에도 하나의 규칙으로 쓰입니다.
표준을 탓하기 전에 측정합니다
드롭다운 메뉴가 화면 밖으로 밀려난 결함을 처음에는 WebKit의 anchor() 지원 문제로 진단했습니다. 기능을 따로 떼어 측정하자 WebKit과 Chromium 모두 정상이었습니다. 진짜 원인은 화면 가장자리의 좁은 버튼에 넓은 메뉴를 가운데 정렬한 기하였고, 정렬을 start로 바꿔 해결했습니다. 표준의 한계를 받아들이는 것과 측정 없이 표준을 탓하는 것은 다릅니다.
표준과 primitive가 만날 때도 공식 API로
native 모달 시트 안에서 Reka UI 기반 셀렉트를 열면 목록이 보이지 않았습니다. 모달은 top layer에 그려지는데, 목록은 body로 옮겨(portal) 그려졌기 때문입니다. top layer는 z-index로 이길 수 없습니다. 해결에는 Reka Portal이 공식 문서로 제공하는 to prop을 썼습니다. 목록을 열 때 가장 가까운 dialog나 [popover] 조상 안으로 옮기도록 composable 한 곳에서 정했습니다. 함께, 열린 오버레이의 정지 상태에는 transform을 두지 않아(none) 목록의 좌표가 틀어지지 않게 했습니다. 사용처마다 prop을 붙이는 안과 팝업을 직접 구현하는 안은 기각했습니다.
경계와 예외
- 네이티브 우선은 오버레이와 위치 계산 같은 메커니즘의 원칙입니다. DS 폼 컨트롤을 건너뛰는 근거가 아닙니다. 날짜는
EDatePicker를 쓰고 raw<input type="date">를 조립하지 않습니다. Android에서 운영체제 선택기가 떠서 제품의 모습이 달라지기 때문입니다. - Vue wrapper 경로에서는 위치 계산도 primitive가 맡습니다. HTML/CSS 경로와 같은 결과가 나오도록 정렬 기본값을 맞춥니다(드롭다운은 둘 다
start). 끌기·스냅이 본질인 Drawer는 vaul-vue를 씁니다. - 디자인 시스템이 생기기 전에 만든 프로토타입 제품의 결함은 그 자리에서 JS 헬퍼로 덮지 않습니다. DS 컴포넌트와 표준 구조에 맞게 다시 씁니다.
- 새 의존성은 브라우저 표준 API, Leysys 패키지, 승인된 라이브러리 순서로 판단합니다. 상세 기준은 웹 라이브러리 선택 기준을 따릅니다.
정리
- DS는 class 조합만으로 동작하며, 동작은 먼저 HTML·CSS 표준에 맡깁니다.
- 모달은
<dialog>, 비모달은popover, 펼침은<details>, 위치는 CSS anchor positioning입니다. - Chromium을 우선하며, 표준의 현재 한계는 CSS 대체 규칙으로 받아들이고 JS로 우회하지 않습니다.
- Primitive UI는 바꿀 수 있는 층이며, DS CSS는 상태를 공통 상태 속성과 ARIA로 읽습니다.
- 외부 라이브러리는 공식 문서에 있는 API만 씁니다.
정본
leysys-designAGENTS.md§프로젝트 핵심 목표·§제품 레포 재작성 원칙·§행동 규칙 — HTML/CSS-First, 네이티브 우선, 외부 라이브러리 공식 API 규칙leysys-designdocs/decisions/OVERLAY-UNIFIED-DIALOG-ELEMENT-2026-05-30.md— 오버레이 네 개의<dialog>구조leysys-designdocs/decisions/OVERLAY-LIGHT-DISMISS-CLOSEDBY-2026-06-10.md—closedby로 닫기 제어leysys-designdocs/decisions/EXECUTABLE-UI-CONTRACT-PHASE4-ALERT-DIALOG-COVERAGE-2026-08-22.md— 확인 대화상자의 기본·잠금 모드 닫기leysys-designdocs/decisions/MDS-ACTION-MENU-NATIVE-POPOVER-2026-08-14.md— popover와 anchor positioningleysys-designdocs/decisions/DROPDOWN-MENU-ANCHOR-POSITIONING-FIX-2026-06-03.md— Chromium 우선과 측정leysys-designdocs/decisions/TOP-LAYER-POPUP-PORTAL-POLICY-2026-06-10.md— top layer와 primitive 팝업leysys-designdocs/process/PRIMITIVE-UI-INTEGRATION.md— DS와 primitive의 책임 분담leysys-designdocs/specs/DS-GOTCHAS.md§14 — 네이티브 우선의 경계- 웹 라이브러리 선택 기준 — Leysys Docs 공개 표준