2

I have a popup element which is hidden by default and only shows up programmatically when the script assigns a specific class to its container and populates the popup text.

In css/stylesheet.css:

.error-message {
    opacity: 0;
    visibility: hidden;
    transition: all 0.2s ease;
}

.container.with-error .error-message {
    opacity: 1;
    visibility: visible;
}

In index.html:

<link rel="stylesheet" href="css/stylesheet.css">
<div class="container">
    <div class="error-message">This text will be changed by a script.</div>
</div>

According to this simple style declaration, the .error-message element should always be invisible, unless it is preceded by a .container.with-error, in which case it becomes visible, and its appearance is always animated because of transition property.

However, the .error-message triggers its transition when the page is loaded, resulting in a flash which I believe it should not do.

Related behavior I have observed:

  • The flash does not appear if the style is declared in an inline <style> tag
  • The flash appears if every style but transition: all is declared in an inline <style> tag
  • The flash does not appear if the style is loaded from a Base-64 encoded Data URL like this: <link href="data:text/css;base64,...">
  • The flash does not appear if the style loaded from <link rel="stylesheet"> is retrieved from cache.

I've created a demo that reproduces this bug every time. To simulate requesting a remote stylesheet without cache, a blob:// Object URL is generated from the style instead. The inline demo is available at the end of this question, but for best results, use JSBin. Use F5 to see the bug in action.

I'm curious how to fix this and what causes this issue as this is clearly not intended behavior.

<!doctype html>
<html lang="en">
    <head>
        <script>
            /* jshint browser: true, esversion: 6 */

            window.onload = function() {

                // Log all transition events
                window.ontransitioncancel = appendToTransitionLog;
                window.ontransitionstart = appendToTransitionLog;
                window.ontransitionrun = appendToTransitionLog;
                window.ontransitionend = appendToTransitionLog;

                // Simulates loading a stylesheet from a remote location
                // Works the same way as if #simulated-stylesheet's content
                // was hosted and served from <link rel=stylesheet> without cache
                //
                // Keep in mind that this bug does not appear
                // if the style is injected or loaded from cache!
                createFakeStylesheet();
            };
            
            function createFakeStylesheet() {
                var styleContent = document.getElementById("simulated-stylesheet").text;
                var styleBlob = new Blob([styleContent], {type: "text/css"});
                var styleURL = URL.createObjectURL(styleBlob);

                var linkElement = document.createElement("link");
                linkElement.rel = "stylesheet";
                linkElement.href = styleURL;

                document.head.appendChild(linkElement);
            }

            // Functions below handle transition events logging

            // Template import helper
            function importTemplateFromId(id) {
                return document.importNode(document.getElementById(id).content, true);
            }

            // Returns a string like "div.class1.class2" to describe an element
            function describeElement(element) {
                var tagName = element.tagName.toLowerCase();
                var classes = element.classList.toString().split(" ").filter(className => className != "").map(className => "." + className).join("");
                return tagName + classes;
            }

            // Returns a matching log group wrapper.
            // The wrapper is created if the group does not exists.
            // Used for grouping transition events by element descriptor
            function getLogWrapper(logContainer, elementText) {
                var matchingWrapper = logContainer.querySelector(".wrapper[data-for-element=\"" + elementText + "\"] .logs");

                if (matchingWrapper) {
                    return matchingWrapper;
                }

                var wrapperTemplate = importTemplateFromId("wrapper-template");
                var wrapperName = wrapperTemplate.querySelector(".name");
                var wrapperElement = wrapperTemplate.querySelector(".wrapper");
                wrapperName.textContent = elementText;
                wrapperElement.dataset.forElement = elementText;

                return logContainer.appendChild(wrapperElement).querySelector(".logs");
            }

            // Logs a transition event.
            // Logs are grouped by each event type (start, run, end)
            // and target element's descriptor (see describeElement)
            function appendToTransitionLog(transitionEvent) {
                var eventType = transitionEvent.type;

                var eventProperty = transitionEvent.propertyName;

                var logContainer = document.getElementById("log-" + eventType);
                var elementText = describeElement(transitionEvent.target);
                var logWrapper = getLogWrapper(logContainer, elementText);

                var logEntry = document.createElement("span");
                logEntry.textContent = eventProperty;
                logEntry.className = "entry";
                logWrapper.appendChild(logEntry);
            }
        </script>

        <style>
            #edit-with-js-bin {
                display: none!important;
            }
            
            .log {
                font-size: 14px;
            }

            .log .wrapper {
                padding-left: 16px;
            }

            .wrapper .name {
                text-decoration: underline;
            }

            .wrapper .logs {
                padding-left: 12px;
            }

            .wrapper .entry {
                display: inline-block;
                color: grey;
                padding: 8px 4px;
            }

            .wrapper .entry:nth-child(2n) {
                color: lightgrey;
            }

            body {
                font-family: monospace;
                font-size: 0;
            }

            .side {
                display: inline-block;
                font-size: 14px;
                vertical-align: top;
                width: 50%;
                height: 100%;
            }
        </style>

        <template id="wrapper-template">
            <div class="wrapper" data-for>
                <span class="name"></span>
                <div class="logs"></div>
            </div>
        </template>

        <script id="simulated-stylesheet" type="text/css">
            .remote {
                background: crimson;
                color: white;
                display: inline-block;
                margin: 8px;
                padding: 8px;
            }

            .remote.transparent {
                opacity: 0;
                visibility: hidden;
            }

            .remote.transition-some {
                transition: opacity, visibility 1s ease;
            }

            .remote.transition-all {
                transition: all 1s ease;
            }
        </script>

        <style>
            .transition-all-inline {
                transition: all 1s ease;
            }

            .local {
                background: green;
                color: white;
                display: inline-block;
                margin: 8px;
                padding: 8px;
            }

            .local.transparent {
                opacity: 0;
                visibility: hidden;
            }

            .local.transition-some {
                transition: opacity, visibility 1s ease;
            }

            .local.transition-all {
                transition: all 1s ease;
            }

            .mock {
                background: orangered;
                transition: all 1s ease;
            }
            .mock:hover {
                background: orange;
            }
        </style>
    </head>

    <body>
        <div class="side left">
            <div>
                <u>.remote</u> <div class="remote">I'm always styled.</div>
            </div>
            <div>
                .remote<u>.transparent</u> <div class="remote transparent">I'm always transparent.</div>
            </div>
            <div>
                .remote.transparent<u>.transition-some</u> <div class="remote transparent transition-some">I'm invisible!</div>
            </div>
            <div>
                .remote.transparent<u>.transition-all</u> <div class="remote transparent transition-all">I will briefly flash when the page loads.</div>
            </div>
            <div>
                .remote.transparent<u>.transition-all-inline</u> <div class="remote transparent transition-all-inline">I will briefly flash when the page loads.</div>
            </div>
        </div>
        <div class="side right">
            <div>
                <u>.local</u> <div class="local">I'm always styled.</div>
            </div>
            <div>
                .local<u>.transparent</u> <div class="local transparent">I'm always transparent.</div>
            </div>
            <div>
                .local.transparent<u>.transition-some</u> <div class="local transparent transition-some">I'm invisible!</div>
            </div>
            <div>
                .local.transparent<u>.transition-all</u> <div class="local transparent transition-all">I'm invisible!</div>
            </div>
            <div>
                .local.transparent<u>.transition-all-inline</u> <div class="local transparent transition-all-inline">I'm invisible!</div>
            </div>
            <div>
                .local.mock <div class="local mock">Use me to debug transition events!</div>
            </div>
        </div>
        <div class="log">
            <div>
                <b>ontransitionstart</b>
                <div id="log-transitionstart"></div>
            </div>
            <div>
                <b>ontransitionrun</b>
                <div id="log-transitionrun"></div>
            </div>
            <div>
                <b>ontransitionend</b>
                <div id="log-transitionend"></div>
            </div>
            <div>
                <b>ontransitioncancel</b>
                <div id="log-ontransitioncancel"></div>
            </div>
        </div>
    </body>
</html>

EDIT: The flash appears regardless of what property is being transitioned.

This still creates the same effect:

.error-message {
    transition: opacity 0.2s ease;
}
user7401478
  • 1,372
  • 1
  • 8
  • 25
  • It's a Chromium bug. Firefox handles it correctly. For now you'll have to live without the `all` property. – user8977154 Oct 13 '19 at 03:18
  • @user8977154 I'm getting the same issue on the latest version of Firefox -- check JSBin for a reproducible test case. – user7401478 Oct 13 '19 at 14:32
  • @Kaiido The problem is the flash occurs without any Javascript interaction, by just having this element present on page it triggers its transition. – user7401478 Oct 13 '19 at 14:33
  • @user8977154 I've verified it and the flash still occurs regardless of what property transition is applied to, even if it's `opacity` instead of `all`. – user7401478 Oct 13 '19 at 20:55

1 Answers1

0

It's a normal behavior of transition (not a bug of any specific browser).

The issue in your case is that you already have elements (that flashes) in the DOM tree. That also means the elements have an initial state and any new state performs a transition. If a new style applied and it has a transition property a browser will show you animation between the initial state and the new state with a transition property. (Note that the initial state of the element that flashes is not transparent).

The possible fix for that is to add hidden elements when new styles already exist. Or refactor your styles to have a transparent initial state.

DenisKolodin
  • 13,501
  • 3
  • 62
  • 65