Incentive

@starting-style solves a real problem we have faced: browsers refuse to animate/transition an element the moment it appears on the page (display:none to block/grid/flex). For instance, of you’re showing a modal for the first time or revealing something that was hidden, the browser has no “before” state to animate from, hence it just pops in instantly.

This at-rule lets you tell the browser: “here’s what this element should look like the instant it loads (from display:none). Transition from here to the regular show state.” So instead of reaching for JavaScript hacks (like those setTimeout tricks to delay animations) or using Keyframe Animation (which may be slightly overkill for this), you can now write pure CSS that smoothly fades, slides, or animates elements in and out (toggling between hidden and visible).


Transition without @starting-style

Traditionally, without @starting-style, when we need to add transition for certain components from its initial state (display:none) to its target state, we will need to either use JavaScript or Keyframe animation:

Transition Based on JavaScript

Transition using JavaScript involves enforcing to use classname to control the “visual” presence of component, and toggling “display” property along with these controlling classnames:

2026-07-18T204953

1
2
3
4
5
6
7
8
9
<div class="example-section">
    <h2>JavaScript Transition</h2>
    <button id="js-open-btn">Open JS Popover</button>
    <div id="js-popover" popover="manual" class="popover-box">
        <h3>JS Animated Popover</h3>
        <p>I animate using JavaScript class toggling.</p>
        <button id="js-close-btn" class="close-btn">Close</button>
    </div>
</div>
1
2
3
4
5
6
7
8
9
#js-popover {
    transition: opacity 0.3s ease, transform 0.3s ease; /* Basic transition styles */
    opacity:    0;
    transform:  scale(0.9);
}
#js-popover.is-open { /* .is-open <-- Class added by JS after display:block is applied */
    opacity:   1;
    transform: scale(1);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const jsPopover = document.getElementById('js-popover');
const jsOpenBtn = document.getElementById('js-open-btn');
const jsCloseBtn = document.getElementById('js-close-btn');

// [OPEN LOGIC]
jsOpenBtn.addEventListener('click', () => {
    // This add display:block to the popover element, but since .is-open is not 
    // added just yet , it will stil be opacity:0 and transform:scale(0.9)
    jsPopover.showPopover();
    // We need a slight timeout/delay for the browser to apply display:block before adding the class for transition
    // (alternatively you can also use requestAnimationFrame which allows you run function on next frame)
    
    // [Method-1] timeout/delay (delay 100ms)
    setTimeout(
        ()=>{jsPopover.classList.add('is-open');}, 
        100
    );
    
    // [Method-2] requestAnimationFrame (next, next frame)
    requestAnimationFrame(() => {
        requestAnimationFrame(
            () => { jsPopover.classList.add('is-open');}
        );
    });
});

// [CLOSE LOGIC]
jsCloseBtn.addEventListener('click', () => {
    // First remove the class to trigger the exit transition, but since
    // the popover close is not triigered yet, the display:block still applies 
    jsPopover.classList.remove('is-open');

    // Wait for the transition to finish before actually hiding the popover
    jsPopover.addEventListener('transitionend', function handler(e) {
        if (e.target === jsPopover) {
            jsPopover.hidePopover();
            jsPopover.removeEventListener('transitionend', handler);
        }
    });
});

Outcome:

2026-07-18T205909

How transition based on JavaScript works:

  • When opening:
    • First, call .showPopover() to make the popper target display:block, but visually it is still hidden due to CSS
    • Second, wait for either a fixed timeframe, or for certain number of frames of render.
    • Third, add CSS class .is-open the browser will automatically transition the opacity and transform property to appear
  • When closing:
    • First, remove the class .is-open the browser will automatically transition the opacity and transform property to make the popover dialog to disappear
    • Second, add a event listener that listens for transition finish
    • Third, when the transition is finished (when dialog target visually disappears), the callback function for the listener will be triggered to call .hidePopover() to make the popover target become display:none

Transition Based on CSS Keyframe

When you implement transition using CSS Keyframe, you’re basically declaring an entrance and exit animation for the component based on its :popover-open pseudo-class, that only plays forward once (one iteration), and only when the component appear on the page:

2026-07-18T205756

1
2
3
4
5
6
7
8
9
<div class="example-section">
    <h2>Keyframes Transition</h2>
    <button popovertarget="keyframe-popover">Open Keyframe Popover</button>
    <div id="keyframe-popover" popover class="popover-box">
        <h3>Keyframe Popover</h3>
        <p>I animate in using CSS keyframes, but snap closed instantly.</p>
        <button popovertarget="keyframe-popover" popovertargetaction="hide" class="close-btn">Close</button>
    </div>
</div>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/* Entry animation works well with keyframes when the popover opens */
#keyframe-popover:popover-open {
    animation: popIn 2.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
#keyframe-popover:not(:popover-open) {
    animation: popOut 2.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
@keyframes popIn {
    0%    { opacity: 0; transform: translateY(20px) scale(0.95); }
    100%  { opacity: 1; transform: translateY(0)    scale(1);    }
}
@keyframes popOut {
    0%    { display:block; opacity: 1; transform: translateY(0)    scale(1);    }
    100%  { display:none;  opacity: 0; transform: translateY(20px) scale(0.95); }
}

(* Notice that:

  • we’re adding 0%{display:block} and 100%{display:none} for the exit animation, because if we don’t do so, the display property would immediately be switched to “none” when the popover is closed;
  • And “display” just happen to be a discrete property that if you would add it to an keyframe animation, the change only happens on the last frame (during the popOut animation, display:block remains unchanged from 0% ~ 99%; The display:block → none change only happens exactly when hitting 100% in the popOut animation). More about what this discrete animation mean can be found on Kevin’s video: https://youtu.be/vmDEHAzj2XE?t=361

Outcome:

2026-07-18T210102

Transition for Backdrop

(Transition for area outside the Popup Model) As you might have realised, we have got transition for the model popup holding the content, but the background of the model is still switching instantly as we toggle the popup; To add transition for the backdrop, need to apply similar technique to the ::backdrop pseudo class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/** JavaScript Approach **/
#js-popover::backdrop {
    transition: opacity 1.0s ease;
    opacity: 0;
    backdrop-filter: blur(0px);
}
#js-popover.is-open::backdrop {
    opacity: 1;
    backdrop-filter: blur(10px);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/** CSS Keyframe Approach **/
@keyframes backdropFadeIn {
    0%   { opacity: 0; backdrop-filter: blur(0px); }
    100% { opacity: 1; backdrop-filter: blur(10px);}
}
@keyframes backdropFadeOut {
    0%   { display: block; opacity: 1; backdrop-filter: blur(10px);}
    100% { display: none;  opacity: 0; backdrop-filter: blur(0px); }
}
#keyframe-popover:popover-open::backdrop{
    animation: backdropFadeIn 1.0s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
#keyframe-popover:not(:popover-open)::backdrop{
    animation: backdropFadeOut 1.0s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}

2026-07-18T211004


Transition with @starting-style

With @starting-style you can use regular transition property to add transition for certain components from its initial state (display:none);

2026-07-18T213451

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<div class="example-section">
    <h2>3. @starting-style (Modern CSS)</h2>
    <button popovertarget="starting-style-popover">Open @starting-style Popover</button>
    <div id="starting-style-popover" popover class="popover-box">
        <h3>Modern CSS Popover</h3>
        <p>I animate smoothly in both directions with purely modern CSS.</p>
        <button popovertarget="starting-style-popover" popovertargetaction="hide"
            class="close-btn">Close</button>
    </div>
</div>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#starting-style-popover {
    /* 1. Base transition, including display and overlay (allow-discrete) */
    transition:
        opacity 0.4s ease,
        transform 0.4s cubic-bezier(0.16, 1, 0.3, 1),
        overlay 0.4s allow-discrete,
        display 0.4s allow-discrete;

    /* 2. State when popover is closed (exit state) */
    opacity: 0;
    transform: translateY(-20px);
}

/* 3. State when popover is open */
#starting-style-popover:popover-open {
    opacity: 1;
    transform: translateY(0);
}

/* 4. State BEFORE the popover opens (entry state) */
@starting-style {
    #starting-style-popover:popover-open {
        opacity: 0;
        transform: translateY(-20px);
    }
}

(*Note that in order for the exit transition to work:

  • you will also need to have transition-behavior being allow-discrete for display

  • you can either declare this with the transition declarations:

    1
    2
    3
    4
    5
    
      transition:
      	opacity 0.4s ease,
      	transform 0.4s cubic-bezier(0.16, 1, 0.3, 1),
    +	overlay 0.4s allow-discrete,
    +	display 0.4s allow-discrete;
    
  • or you can declare it separately separate from the transition declaration:

    1
    2
    3
    4
    5
    6
    
      transition:
      	opacity 0.4s ease,
      	transform 0.4s cubic-bezier(0.16, 1, 0.3, 1),
    +	overlay 0.4s ease,
    +	display 0.4s ease;
    + transition-behavior: allow-discrete;
    

Outcome:

2026-07-18T212332

Similarly let’s declare transition for the backdrop pesudo element:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#starting-style-popover::backdrop {
    /* 1. Base transition, including display and overlay (allow-discrete) */
    transition:
        opacity 0.4s ease,
        overlay 0.4s allow-discrete,
        display 0.4s allow-discrete;
    
    /* 2. State when popover is closed (exit state) */
    opacity: 0;
}

/* 3. State when popover is open */
#starting-style-popover:popover-open::backdrop {
    opacity: 1;
}

/* 4. State BEFORE the popover opens (entry state) */
@starting-style {
    #starting-style-popover:popover-open::backdrop {
        opacity: 0;
    }
}

Outcome:

2026-07-18T212400


Reference

Example Code