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;
}