Reference/At-rules/@starting-style

김동현·2026년 3월 28일

mdn 학습 번역 - CSS

목록 보기
184/190

@starting-style

Baseline 2024 | Newly available

Chrome, Edge, Firefox, Safari에서 지원돼요.

2024년 8월부터 이 기능은 최신 기기와 브라우저 버전에서 작동해요. 이 기능은 구형 기기나 브라우저에서는 작동하지 않을 수 있어요.

@starting-style CSS at-rule은 요소가 첫 번째 스타일 업데이트를 받을 때, 즉 이전에 로드된 페이지에서 요소가 처음 표시될 때 전환하고 싶은 요소에 설정된 프로퍼티의 시작 값을 정의하는 데 사용돼요.

처음 화면에 나타나거나 display: none 상태에서 벗어날 때 부드러운 애니메이션 효과를 줄 수 있게 해주는 아주 고마운 규칙인 @starting-style입니다.

보통 CSS 트랜지션은 이미 화면에 있는 요소의 상태가 바뀔 때만 작동하잖아요? 하지만 이 규칙을 사용하면 "요소가 처음 태어날 때의 스타일"을 지정할 수 있어서, 등장 애니메이션을 자바스크립트 없이 아주 깔끔하게 구현할 수 있어요. 자, 원문 내용을 하나도 빠짐없이 구어체로 친절하게 설명해 드릴게요!


구문 (Syntax)

@starting-style 앳 규칙은 두 가지 방식으로 사용할 수 있어요.

  1. 독립적인 블록으로 사용하는 경우: 이 방식은 시작 스타일 선언과 이를 적용할 요소를 선택하는 하나 이상의 규칙 집합을 포함합니다.

    @starting-style {
      /* 규칙 집합 (rulesets) */
    }
  2. 기존 규칙 집합 안에 중첩해서 사용하는 경우: 이 방식은 해당 규칙 집합에 의해 이미 선택된 요소에 대해 시작 속성값을 정의하는 하나 이상의 선언을 포함합니다.

    selector {
      /* 기존 규칙 집합 */
      /* ... */
    
      @starting-style {
        /* 선언 (declarations) */
      }
    }

설명 (Description)

예기치 않은 동작을 방지하기 위해, CSS 트랜지션은 기본적으로 요소의 초기 스타일 업데이트 시점이나, display 타입이 none에서 다른 값으로 변경될 때는 트리거되지 않아요. 이런 '첫 번째 스타일 트랜지션'을 가능하게 하려면 @starting-style 규칙이 필요합니다. 이 규칙은 이전 상태가 없는 요소에 대해 시작 스타일을 제공하여, 어떤 속성값으로부터 변화(transition)를 시작할지 정의해 줍니다.

@starting-style은 특히 탑 레이어(top layer)에 표시되는 요소(예: 팝오버(popovers)나 모달 <dialog>), display: none으로 바뀌거나 그 반대로 바뀌는 요소, 그리고 DOM에 처음 추가되거나 제거되는 요소의 등장(entry) 및 퇴장(exit) 트랜지션을 만들 때 아주 유용해요.

참고: @starting-style은 CSS 트랜지션에만 해당되는 내용이에요. CSS 애니메이션을 사용해서 이런 효과를 구현할 때는 @starting-style이 필요하지 않습니다. 예시는 CSS 애니메이션 사용하기를 참고하세요.

@starting-style을 사용하는 두 가지 방법(독립형 또는 중첩형)을 좀 더 자세히 알아볼까요?

예를 들어, 팝오버가 나타날 때(즉, 탑 레이어에 추가될 때) 애니메이션을 주고 싶은 상황을 생각해 봅시다. 팝오버가 열렸을 때의 스타일을 지정하는 "원래의 규칙"은 다음과 같을 거예요 (아래의 팝오버 애니메이션 예제를 참고하세요).

[popover]:popover-open {
  opacity: 1;
  transform: scaleX(1);
}

첫 번째 방법인 독립형 블록을 사용해서 애니메이션이 시작될 팝오버 속성의 시작값을 지정하려면, CSS에 다음과 같이 작성합니다.

@starting-style {
  [popover]:popover-open {
    opacity: 0;
    transform: scaleX(0);
  }
}

참고: @starting-style 앳 규칙과 "원래의 규칙"은 동일한 명시도(specificity)를 가집니다. 따라서 시작 스타일이 제대로 적용되도록 하려면 @starting-style 규칙을 원래의 규칙 뒤에 작성하세요. 만약 원래의 규칙보다 앞에 작성하면, 원래의 스타일이 시작 스타일을 덮어써 버릴 수 있습니다.

두 번째 방법인 중첩 방식을 사용해서 팝오버의 시작 스타일을 지정하려면, 원래의 규칙 안에 @starting-style 블록을 넣으면 됩니다.

[popover]:popover-open {
  opacity: 1;
  transform: scaleX(1);

  @starting-style {
    opacity: 0;
    transform: scaleX(0);
  }
}

시작 스타일은 정확히 언제 사용되나요? (When exactly are starting styles used?)

요소가 DOM에 처음 렌더링될 때, 혹은 display: none에서 가시적인 값으로 전환될 때 @starting-style에 정의된 스타일로부터 트랜지션이 시작된다는 점을 이해하는 것이 중요해요. 하지만 초기 가시 상태에서 다시 다른 상태로 전환될 때는 더 이상 @starting-style을 사용하지 않습니다. 이미 DOM에서 보이는 상태가 되었기 때문이죠. 대신 해당 요소의 기본 상태에 존재하는 스타일로 다시 트랜지션하게 됩니다.

결과적으로 이런 상황에서는 관리해야 할 세 가지 스타일 상태가 있는 셈이에요. 바로 시작 스타일 상태, 트랜지션된 상태, 그리고 기본 상태입니다. 이 때문에 "나타날 때"와 "사라질 때"의 트랜지션 효과를 서로 다르게 만드는 것도 가능해요. 이에 대한 증명은 아래의 시작 스타일이 사용되는 시점 데모 예제에서 확인하실 수 있습니다.


형식적인 구문 (Formal syntax)

@starting-style = 
  @starting-style { <rule-list> }  

예제 (Examples)

기본적인 @starting-style 사용법 (Basic @starting-style usage)

요소가 처음 렌더링될 때 background-color를 투명(transparent)에서 초록색(green)으로 부드럽게 변화시킵니다.

#target {
  transition: background-color 1.5s;
  background-color: green;
}

@starting-style {
  #target {
    background-color: transparent;
  }
}

요소의 display 값이 none으로 바뀌거나 그 반대로 바뀔 때 opacity에 트랜지션 효과를 줍니다.

#target {
  transition-property: opacity, display;
  transition-duration: 0.5s;
  display: block;
  opacity: 1;

  @starting-style {
    opacity: 0;
  }
}

#target.hidden {
  display: none;
  opacity: 0;
}

시작 스타일이 사용되는 시점 데모 (Demonstration of when starting styles are used)

이 예제에서는 버튼을 누르면 <div> 요소가 생성되고, 여기에 showing이라는 클래스가 부여된 후 DOM에 추가됩니다.

showing 클래스는 @starting-stylebackground-color: red를 가지고 있으며, 최종적으로 변할 스타일인 background-color: blue를 가집니다. 기본 div 규칙에는 background-color: yellow가 정의되어 있고, 여기서 트랜지션 설정도 이루어집니다.

<div>가 DOM에 처음 추가될 때, 배경색이 빨간색에서 파란색으로 변하는 것을 볼 수 있습니다. 잠시 후 자바스크립트를 통해 <div>에서 showing 클래스를 제거하면, 이때는 빨간색이 아니라 파란색에서 노란색(기본 스타일)으로 변합니다. 이는 시작 스타일이 요소가 DOM에 처음 렌더링될 때만 사용된다는 것을 증명해요. 일단 나타난 후에는 요소에 설정된 기본 스타일로 돌아가게 됩니다.

또 한 번의 시간이 지난 후, DOM에서 <div>를 완전히 제거하여 예제의 초기 상태로 되돌립니다.

HTML

<button>Display <code>&lt;div&gt;</code></button>

CSS

div {
  width: 200px;
  height: 100px;
  border: 1px solid black;
  margin-top: 10px;
}

div::after {
  content: "class: " attr(class);
  position: relative;
  top: 3px;
  left: 3px;
}

div {
  background-color: yellow;
  transition: background-color 3s;
}

div.showing {
  background-color: skyblue;
}

@starting-style {
  div.showing {
    background-color: red;
  }
}

JavaScript

const btn = document.querySelector("button");

btn.addEventListener("click", () => {
  btn.disabled = true;
  const divElem = document.createElement("div");
  divElem.classList.add("showing");
  document.body.append(divElem);

  setTimeout(() => {
    divElem.classList.remove("showing");

    setTimeout(() => {
      divElem.remove();
      btn.disabled = false;
    }, 3000);
  }, 3000);
});

결과 (Result)

MDN Playground에서 예제 실행하기


팝오버 애니메이션하기 (Animating a popover)

이 예제에서는 CSS 트랜지션을 사용해서 팝오버(popover)에 애니메이션을 줍니다. transition 속성을 사용해서 기본적인 등장 및 퇴장 애니메이션을 구현했어요.

HTML

popover 속성으로 선언된 <div> 요소와, popovertarget 속성으로 팝오버를 제어하는 <button> 요소를 포함합니다.

<button popovertarget="mypopover">Show the popover</button>
<div popover="auto" id="mypopover">I'm a Popover! I should animate.</div>

CSS

여기서는 opacitytransform(가로 크기 조절) 두 가지 속성을 애니메이션화해서 팝오버가 서서히 나타나고 사라지며, 가로로 커지거나 작아지게 만들려고 해요.

html {
  font-family: "Helvetica", "Arial", sans-serif;
}

/* 팝오버가 열렸을 때의 최종 상태 */
[popover]:popover-open {
  opacity: 1;
  transform: scaleX(1);
}

/* 팝오버의 기본 상태 (닫혔을 때) */
[popover] {
  font-size: 1.2rem;
  padding: 10px;

  /* 퇴장 애니메이션의 최종 상태 */
  opacity: 0;
  transform: scaleX(0);

  /* display와 overlay 속성에도 트랜지션을 적용해요 */
  transition:
    opacity 0.7s,
    transform 0.7s,
    overlay 0.7s allow-discrete,
    display 0.7s allow-discrete;
}

/* 등장 애니메이션을 위해 시작 스타일을 지정해요 */
/* 반드시 [popover]:popover-open 규칙 뒤에 쓰세요 */
@starting-style {
  [popover]:popover-open {
    opacity: 0;
    transform: scaleX(0);
  }
}

/* 팝오버 배경(backdrop)에 대한 트랜지션 */
[popover]::backdrop {
  background-color: transparent;
  transition:
    display 0.7s allow-discrete,
    overlay 0.7s allow-discrete,
    background-color 0.7s;
}

[popover]:popover-open::backdrop {
  background-color: rgb(0 0 0 / 25%);
}

/* 의사 요소에는 중첩(&)이 지원되지 않으므로 독립형 블록으로 지정하세요 */
@starting-style {
  [popover]:popover-open::backdrop {
    background-color: transparent;
  }
}

이를 구현하기 위해, 팝오버의 기본 닫힌 상태([popover])에 시작 상태를 설정하고, 열린 상태(:popover-open 의사 클래스)에 종료 상태를 설정했습니다.

그다음 두 상태 사이를 부드럽게 이어줄 transition 속성을 설정했어요. 애니메이션의 시작점은 @starting-style 규칙 안에 포함시켜서 등장 애니메이션이 작동하게 만들었죠.

애니메이션이 적용되는 요소가 나타날 때 탑 레이어(top layer)로 올라가고 사라질 때(즉, display: none이 될 때) 탑 레이어에서 내려오기 때문에, 양방향 애니메이션이 제대로 작동하려면 몇 가지 추가 단계가 필요해요.

  • transition 목록에 display를 추가해서, 등장 및 퇴장 애니메이션이 진행되는 동안 요소가 계속 보이도록(즉, display: block 같은 가시적인 상태 유지) 해야 합니다. 이게 없으면 퇴장 애니메이션이 안 보이고 팝오버가 그냥 툭 하고 사라져 버려요. 이때 단축 속성 안에 transition-behavior: allow-discrete 값을 넣어줘야 애니메이션이 활성화됩니다.
  • overlay 속성도 트랜지션 목록에 추가해서, 애니메이션이 완전히 끝날 때까지 요소가 탑 레이어에서 내려오는 것을 미뤄야 해요. 지금 같은 간단한 애니메이션에서는 큰 차이가 안 날 수도 있지만, 복잡한 경우에는 이걸 안 해주면 요소가 오버레이에서 너무 빨리 빠져나가서 애니메이션이 뚝 끊기거나 효과가 없을 수 있거든요. 마찬가지로 transition-behavior: allow-discrete가 필요합니다.

참고: 팝오버가 열릴 때 뒤에 나타나는 ::backdrop에도 트랜지션을 넣어서 배경이 부드럽게 어두워지도록 만들었어요.

결과 (Result)

MDN Playground에서 예제 실행하기

참고: 팝오버는 보일 때마다 display: none에서 display: block으로 바뀌기 때문에, 등장 트랜지션이 일어날 때마다 항상 @starting-style에서 :popover-open 스타일로 변화하게 됩니다. 반대로 팝오버가 닫힐 때는 :popover-open 상태에서 기본 [popover] 상태로 변화하게 되고요.

참고: <dialog> 요소와 그 배경이 나타나고 사라질 때 트랜지션을 주는 또 다른 예제는 <dialog> 참조 페이지의 다이얼로그 요소 트랜지션하기 섹션에서 확인하실 수 있습니다.


DOM 추가 및 제거 시 요소 트랜지션하기 (Transitioning elements on DOM addition and removal)

이 예제는 버튼을 누르면 <section> 컨테이너에 새로운 요소들을 추가합니다. 각 요소 안에는 닫기 버튼이 있고, 이걸 누르면 해당 요소가 제거되죠. 이 과정을 통해 요소가 DOM에 추가되거나 제거될 때 트랜지션을 사용하는 방법을 보여줍니다.

HTML

<button>Create new column</button>
<section></section>

JavaScript

const btn = document.querySelector("button");
const sectionElem = document.querySelector("section");

btn.addEventListener("click", createColumn);

function randomBackground() {
  function randomNum() {
    return Math.floor(Math.random() * 255);
  }
  const baseColor = `${randomNum()} ${randomNum()} ${randomNum()}`;
  return `linear-gradient(to right, rgb(${baseColor} / 0), rgb(${baseColor} / 0.5))`;
}

function createColumn() {
  const divElem = document.createElement("div");
  divElem.style.background = randomBackground();

  const closeBtn = document.createElement("button");
  closeBtn.textContent = "✖";
  closeBtn.setAttribute("aria-label", "close");
  divElem.append(closeBtn);
  sectionElem.append(divElem);

  closeBtn.addEventListener("click", () => {
    divElem.classList.add("fade-out");

    setTimeout(() => {
      divElem.remove();
    }, 1000);
  });
}

"Create new column" 버튼을 클릭하면 createColumn() 함수가 실행되어 무작위 배경색을 가진 <div>와 닫기 버튼을 만듭니다. 닫기 버튼을 클릭하면:
1. <div>fade-out 클래스를 추가하여 퇴장 애니메이션을 시작합니다.
2. setTimeout()을 사용해 애니메이션이 끝날 때까지 1초(1000ms) 기다린 후 DOM에서 요소를 완전히 제거합니다.

CSS

각 컬럼이 추가되고 제거될 때 opacityscale이 변화하도록 트랜지션을 설정했습니다.

html * {
  box-sizing: border-box;
  font-family: sans-serif;
}

body {
  margin: 0;
  display: flex;
  flex-direction: column;
  height: 100vh;
  gap: 10px;
}

body > button {
  margin: 10px 10px 0 10px;
}

section {
  display: flex;
  flex: 1;
  gap: 10px;
  margin: 10px;
}

div {
  flex: 1;
  border: 1px solid gray;
  position: relative;
  opacity: 1;
  scale: 1 1;

  /* opacity, scale, display에 트랜지션을 적용해요 */
  transition:
    opacity 0.7s,
    scale 0.7s,
    display 0.7s allow-discrete,
    all 0.7s allow-discrete;
}

/* 등장 애니메이션 시작 상태 지정 */
@starting-style {
  div {
    opacity: 0;
    scale: 1 0;
  }
}

/* 퇴장 애니메이션 상태 (JS에서 클래스 추가 시) */
.fade-out {
  opacity: 0;
  display: none;
  scale: 1 0;
}

div > button {
  font-size: 1.6rem;
  background: none;
  border: 0;
  text-shadow: 2px 1px 1px white;
  border-radius: 15px;
  position: absolute;
  top: 1px;
  right: 1px;
  cursor: pointer;
}

DOM에 추가되거나 제거될 때 애니메이션을 구현하기 위해 다음과 같은 작업을 했습니다.

  • div { ... } 규칙에 트랜지션이 끝났을 때의 최종 상태를 정의했습니다.
  • @starting-style 블록 안에 트랜지션이 시작될 초기 상태를 정의했습니다.
  • .fade-out 규칙 안에 퇴장 애니메이션 상태를 정의했습니다. 이때 요소를 즉시 UI에서 없애기 위해 display: none도 함께 설정했습니다.
  • div { ... } 규칙의 transition 목록에 displayallow-discrete를 포함시켜서 사라지는 동안에도 애니메이션이 보이도록 했습니다.

결과 (Result)

MDN Playground에서 예제 실행하기


@starting-style은 예전에 복잡한 자바스크립트나 @keyframes 없이는 불가능했던 자연스러운 등장을 가능하게 해주는 혁신적인 도구예요. 특히 팝오버나 다이얼로그와 결합하면 UX 품질을 획기적으로 높일 수 있답니다!


공식문서에 대한 추가적 설명


1. 한 줄 요약: "무대에 오르기 직전, 커튼 뒤에서의 모습"

@starting-style요소가 화면에 처음 짜잔! 하고 나타날 때의 '시작점'을 정해주는 CSS 규칙입니다.

2. 왜 이게 필요해졌을까요? (우리가 겪던 고통)

우리가 CSS로 transition(부드러운 전환)을 주려면 반드시 A(시작) 상태B(끝) 상태가 있어야 해요. 브라우저가 그 중간 과정을 부드럽게 계산해주니까요.

그런데 아주 치명적인 문제가 있었습니다.
1. display: none 이었다가 display: block으로 바뀔 때
2. 자바스크립트로 document.createElement를 해서 DOM에 처음 추가될 때

이 두 가지 경우에는 요소가 화면에 아예 없다가 갑자기 생겨나는 거라 'A(시작) 상태'라는 게 존재하지 않아요. 그래서 opacity: 0에서 1로 부드럽게 나타나게 하고 싶어도, 중간 과정 없이 그냥 띡! 하고 나타나 버렸죠.

이걸 해결하려고 예전에는 자바스크립트로 setTimeout을 쓰거나, requestAnimationFrame을 써서 억지로 시간차를 두는 아주 더러운(?) 코드를 짜야만 했어요.

이제 @starting-style이 이 문제를 깔끔하게 해결합니다. 브라우저에게 "얘가 화면에 처음 나타날 때는 이 스타일에서부터 시작해!"라고 명시적으로 알려주는 거예요.


3. 실전 예시로 이해하기

예시 1: display: none에서 나타나는 요소 (가장 흔한 케이스)

버튼을 눌러서 숨겨져 있던 메뉴를 부드럽게 나타나게 하고 싶다고 해볼게요.

/* 1. 메뉴의 기본 상태 (무대에 완전히 올라온 상태: B 상태) */
.menu {
  display: block; /* 화면에 보임 */
  opacity: 1;     /* 완전 선명함 */
  transform: translateY(0); /* 제자리 */
  
  /* transition 설정 */
  /* display 속성도 애니메이션이 되게 하려면 allow-discrete가 꼭 필요해요! */
  transition: opacity 0.5s, transform 0.5s, display 0.5s allow-discrete;
}

/* 2. 메뉴가 숨겨졌을 때의 상태 (퇴장할 때의 상태) */
.menu.hidden {
  display: none;
  opacity: 0;
  transform: translateY(-20px);
}

/* 3. ★ 핵심: 메뉴가 처음 화면에 등장할 때의 '시작점' (커튼 뒤 상태: A 상태) */
@starting-style {
  .menu {
    opacity: 0;
    transform: translateY(-20px); /* 위에서 살짝 내려오면서 투명하게 시작! */
  }
}

작동 원리:

  • .hidden 클래스를 떼는 순간, 브라우저는 요소를 화면에 그립니다.
  • 이때 곧바로 opacity: 1이 되는 게 아니라, @starting-style에 적어둔 opacity: 0, translateY(-20px)에서부터 출발해서 기본 .menu 스타일로 0.5초 동안 부드럽게 이동합니다.

예시 2: 최신 팝오버(Popover)나 다이얼로그(Dialog) 띄우기

요즘 HTML5의 <dialog> 태그나 popover 속성을 쓰면 요소가 화면의 최상단(Top Layer)에 뜹니다. 얘네들도 기본적으로 display: none 상태로 숨어있다가 나타나는 거라 @starting-style이 찰떡궁합이에요.

/* 1. 팝오버의 기본 상태 (닫혀있을 때) */
.my-modal {
  /* 닫힐 때(퇴장) 어떻게 변할지 적어줍니다 */
  opacity: 0;
  transform: scale(0.8); /* 작아지면서 사라짐 */
  
  transition: all 0.4s allow-discrete;
}

/* 2. 팝오버가 열렸을 때 (화면에 보일 때) */
.my-modal:popover-open {
  opacity: 1;
  transform: scale(1); /* 원래 크기 */
}

/* 3. 팝오버가 처음 '열리기 시작'할 때의 상태 (등장 시작점) */
@starting-style {
  .my-modal:popover-open {
    opacity: 0;
    transform: scale(0.8); /* 작고 투명한 상태에서 출발해라! */
  }
}

이렇게 하면 팝오버를 열 때 작은 크기에서 투명하게 나타나면서 제 크기를 찾고, 닫을 때는 다시 작아지면서 투명하게 사라집니다. 자바스크립트 타이머(setTimeout) 하나 없이 오직 CSS만으로 완벽한 등장/퇴장 애니메이션을 구현한 거예요!


예시 3: 자바스크립트로 DOM을 추가할 때

장바구니에 아이템을 담을 때, 자바스크립트로 리스트 요소를 생성해서 append() 하잖아요? 그때도 쓸 수 있어요.

JavaScript

const newItem = document.createElement('li');
newItem.className = 'cart-item';
newItem.textContent = '새로운 상품';
document.querySelector('.cart-list').append(newItem);

CSS

/* 1. 장바구니 아이템의 기본 상태 (다 나타났을 때) */
.cart-item {
  opacity: 1;
  background-color: white;
  transition: all 0.5s ease-out;
}

/* 2. DOM에 방금 막 append 되었을 때의 시작 상태! */
@starting-style {
  .cart-item {
    opacity: 0;
    background-color: yellow; /* 처음에만 노란색으로 번쩍! 하고 흰색으로 변함 */
  }
}

4. 주의사항 (선배의 팁)

  1. 등장에만 관여해요: @starting-style은 요소가 '나타날 때(Entry)'의 시작점만 정해줍니다. 요소가 사라질 때(Exit)의 애니메이션은 기존처럼 클래스를 붙이거나(hidden), 닫힌 상태의 CSS에 적어주면 됩니다.
  2. 명시도 주의: 보통 @starting-style 블록은 일반 CSS 규칙보다 아래쪽(코드의 끝부분)에 적는 것이 안전해요. 같은 선택자라면 나중에 적힌 게 우선순위를 가지니까요.
  3. allow-discrete를 잊지 마세요: display: nonedisplay: block 사이를 애니메이션 하려면 transition: display 0.5s allow-discrete; 처럼 allow-discrete 키워드를 꼭 넣어줘야 합니다. 브라우저가 "아, 애니메이션 끝날 때까지 display: none 하는 걸 참아줄게!" 라고 이해하게 만드는 마법의 키워드거든요.

이제 @starting-style이 왜 필요한지 확실히 감이 오시죠? 복잡한 자바스크립트 로직을 CSS 몇 줄로 덜어낼 수 있는 정말 훌륭한 스펙이랍니다. 실무에서 모달이나 토스트 알림 띄울 때 꼭 한번 써보세요!

profile
프론트에_가까운_풀스택_개발자

0개의 댓글