- Published on
display: none 을 transition 하기 — allow-discrete 와 @starting-style (Transition to and from display: none)
- Authors

- Name
- Nostrss
- Github
- Github
출처: Transition to and from display: none — Kevin Powell. 영상의 내용을 기록으로 남겨두기 위해 ai로 번역해서 블로그를 작성했다.
문제 — display 는 discrete 속성이라 transition 이 안 된다
모달이나 드롭다운을 열고 닫을 때 부드럽게 페이드 인/아웃 시키고 싶다. 그런데 display: none → display: grid 로 바뀌는 요소에는 transition: opacity 2s 를 아무리 걸어도 동작하지 않는다.
이유는 display 가 discrete(이산) 속성이기 때문이다. opacity 처럼 0 과 1 사이의 중간값이 존재하는 속성과 달리, display 는 none 아니면 grid 다. 중간 상태가 없으니 브라우저는 그냥 즉시 값을 바꿔버리고, 그 순간 요소가 사라지므로 함께 걸어둔 opacity transition 도 보일 틈이 없다.
영상에서는 <dialog> 를 invoker commands (command/commandfor 속성) 로 열고 닫는 예제를 쓰는데, invoker commands 는 모든 브라우저에 들어갔지만 아직 신생이라 불안하면 JavaScript 로 열어도 똑같이 동작한다. 그리고 dialog 만의 이야기가 아니다 — display: none 에서 다른 display 값으로 전환되는 모든 요소에 해당한다.
아래 코드는 화면을 직접 본 것이 아니라 자막을 기반으로 재구성한 예시다.
해결 1 — transition-behavior: allow-discrete
첫 번째 한 줄. discrete 속성도 transition 대상에 포함시키겠다고 선언한다.
dialog {
transition:
opacity 2s,
display 2s;
transition-behavior: allow-discrete;
}
이렇게 하면 닫힐 때 페이드아웃이 동작한다. transition 이 도는 2초 동안 display 가 grid 로 유지되다가, 다른 속성들의 transition 이 끝난 뒤에 none 으로 바뀌기 때문이다.
하지만 열릴 때는 여전히 즉시 나타난다. display: none 상태의 요소는 렌더링 자체가 안 되어 있어서, grid 가 되는 순간 "이전 상태" 라는 게 없다. 브라우저 입장에서는 출발점을 모르니 transition 할 수가 없다.
해결 2 — @starting-style
두 번째 한 줄. 요소가 처음 렌더링될 때의 시작 상태를 지정하는 새 at-rule 이다.
dialog[open] {
opacity: 1;
@starting-style {
opacity: 0;
}
}
이제 열릴 때는 opacity: 0 에서 출발해 페이드인되고, 닫힐 때는 allow-discrete 덕에 페이드아웃된다. 양방향 완성이다.
순서가 중요하다
@starting-style 은 open 상태 규칙 뒤에 와야 한다. cascade 에서 마지막 규칙이 이기기 때문에, 앞에 두면 open 상태 스타일이 시작 스타일을 덮어써서 동작하지 않는다. media query 와 똑같은 원리다.
/* 1. 닫힌 상태 (기본) */
dialog { opacity: 0; transition: ...; transition-behavior: allow-discrete; }
/* 2. 열린 상태 */
dialog[open] { opacity: 1; }
/* 3. 닫힘 → 열림 전환의 시작점 — 반드시 2번 뒤에 */
dialog[open] {
@starting-style { opacity: 0; }
}
순서가 직관적이지 않으니 주석을 남겨두는 게 좋다.
진입과 퇴장 애니메이션을 다르게
@starting-style 의 진짜 재미는 여기다. 시작 상태와 끝 상태를 따로 지정할 수 있으니 들어올 때와 나갈 때 움직임을 다르게 만들 수 있다.
dialog {
translate: 0 50px; /* 닫힐 때: 아래로 내려가며 사라짐 */
transition:
opacity 2s,
translate 2s,
display 2s,
overlay 2s;
transition-behavior: allow-discrete;
}
dialog[open] {
translate: 0 0;
@starting-style {
opacity: 0;
translate: 0 -50px; /* 열릴 때: 위에서 내려오며 등장 */
}
}
위에서 내려오며 나타나고, 아래로 빠지며 사라진다. transition 대상에 overlay 도 넣었는데, dialog 와 popover 는 top layer 에서 렌더링되기 때문에 top layer 에서 빠지는 시점도 transition 이 끝날 때까지 붙잡아 두려는 것이다.
backdrop 도 같이 페이드
::backdrop 은 그냥 두면 즉시 나타났다 즉시 사라진다. 가장 쉬운 방법은 opacity 만 갖고 노는 것이다.
dialog::backdrop {
opacity: 0;
transition: opacity 2s;
}
dialog[open]::backdrop {
opacity: 1;
}
opacity 가 서서히 변하면서 backdrop 의 어두운 필터도 함께 부드럽게 전환된다.
언제 쓰면 좋을까
- 모달 / 다이얼로그 —
<dialog>+show-modalinvoker command 조합에 그대로 얹으면 JavaScript 애니메이션 라이브러리 없이 진입·퇴장 애니메이션이 된다. - popover, 드롭다운 메뉴, 토스트 —
display: none으로 숨기는 모든 UI. popover 도 top layer 를 쓰므로overlaytransition 까지 같은 패턴이 적용된다. - 기존에 JS 로 클래스 토글 + setTimeout 으로 display 를 지연 제거하던 코드 — 그 해킹이 하던 일이 정확히
allow-discrete가 하는 일이다. CSS 두 줄로 대체할 수 있다.
정리
display는 discrete 속성이라 기본적으로 transition 이 안 된다 — 중간값이 없어 즉시 바뀐다.transition-behavior: allow-discrete는 transition 이 끝날 때까지display값을 붙잡아 줘서 닫힐 때 애니메이션을 가능하게 한다.@starting-style은 요소가 처음 렌더링될 때의 시작 상태를 지정해 열릴 때 애니메이션을 가능하게 한다 — open 상태 규칙보다 뒤에 써야 한다 (cascade).- 시작 상태와 퇴장 상태를 다르게 주면 진입·퇴장 애니메이션을 각각 디자인할 수 있고,
::backdrop은 opacity transition 으로 함께 처리한다.

