Why are CSS keyframe animations broken in Vue components with scoped styling?
Posted By: Anonymous
I’m trying to implement a CSS typing indicator in Vue. Without Vue, it looks like this:
.typing-indicator {_x000D_
background-color: #E6E7ED;_x000D_
width: auto;_x000D_
border-radius: 50px;_x000D_
padding: 20px;_x000D_
display: table;_x000D_
margin: 0 auto;_x000D_
position: relative;_x000D_
-webkit-animation: 2s bulge infinite ease-out;_x000D_
animation: 2s bulge infinite ease-out;_x000D_
}_x000D_
.typing-indicator:before, .typing-indicator:after {_x000D_
content: '';_x000D_
position: absolute;_x000D_
bottom: -2px;_x000D_
left: -2px;_x000D_
height: 20px;_x000D_
width: 20px;_x000D_
border-radius: 50%;_x000D_
background-color: #E6E7ED;_x000D_
}_x000D_
.typing-indicator:after {_x000D_
height: 10px;_x000D_
width: 10px;_x000D_
left: -10px;_x000D_
bottom: -10px;_x000D_
}_x000D_
.typing-indicator span {_x000D_
height: 15px;_x000D_
width: 15px;_x000D_
float: left;_x000D_
margin: 0 1px;_x000D_
background-color: #9E9EA1;_x000D_
display: block;_x000D_
border-radius: 50%;_x000D_
opacity: 0.4;_x000D_
}_x000D_
.typing-indicator span:nth-of-type(1) {_x000D_
-webkit-animation: 1s blink infinite 0.3333s;_x000D_
animation: 1s blink infinite 0.3333s;_x000D_
}_x000D_
.typing-indicator span:nth-of-type(2) {_x000D_
-webkit-animation: 1s blink infinite 0.6666s;_x000D_
animation: 1s blink infinite 0.6666s;_x000D_
}_x000D_
.typing-indicator span:nth-of-type(3) {_x000D_
-webkit-animation: 1s blink infinite 0.9999s;_x000D_
animation: 1s blink infinite 0.9999s;_x000D_
}_x000D_
_x000D_
@-webkit-keyframes blink {_x000D_
50% {_x000D_
opacity: 1;_x000D_
}_x000D_
}_x000D_
_x000D_
@keyframes blink {_x000D_
50% {_x000D_
opacity: 1;_x000D_
}_x000D_
}_x000D_
@-webkit-keyframes bulge {_x000D_
50% {_x000D_
-webkit-transform: scale(1.05);_x000D_
transform: scale(1.05);_x000D_
}_x000D_
}_x000D_
@keyframes bulge {_x000D_
50% {_x000D_
-webkit-transform: scale(1.05);_x000D_
transform: scale(1.05);_x000D_
}_x000D_
}_x000D_
html {_x000D_
display: table;_x000D_
height: 100%;_x000D_
width: 100%;_x000D_
}_x000D_
_x000D_
body {_x000D_
display: table-cell;_x000D_
vertical-align: middle;_x000D_
}
_x000D_
<div class="typing-indicator">_x000D_
<span></span>_x000D_
<span></span>_x000D_
<span></span>_x000D_
</div>
_x000D_
_x000D_
_x000D_
– source: http://jsfiddle.net/Arlina/gtttgo93/
The problem is that the animation does not work when adding the scoped
attribute to the component’s style definition (<style lang="scss" scoped>
). I believe it may be related to keyframes that should be declared globally.
The element with .typing-indicator
is in the template of the component with scoped styling.
Does anyone have an idea of how I can allow my component to have scoped styling while making the keyframe animations work?
Solution
Problem
The problem is down to how the Webpack loader for Vue (vue-loader), incorrectly, parses animation names when adding IDs to scoped selectors and other identifiers. This is important because vue-loader’s CSS scoping uses unique attributes added to elements to replicate the behaviour of CSS scoping. While your keyframe names get IDs appended, references to keyframes in animation rules in scoped styles do not.
Your CSS:
@-webkit-keyframes blink {
50% {
opacity: 1;
}
}
@keyframes blink {
50% {
opacity: 1;
}
}
@-webkit-keyframes bulge {
50% {
-webkit-transform: scale(1.05);
transform: scale(1.05);
}
}
@keyframes bulge {
50% {
-webkit-transform: scale(1.05);
transform: scale(1.05);
}
}
.typing-indicator {
...
-webkit-animation: 2s bulge infinite ease-out;
animation: 2s bulge infinite ease-out;
}
.typing-indicator span:nth-of-type(1) {
-webkit-animation: 1s blink infinite 0.3333s;
animation: 1s blink infinite 0.3333s;
}
.typing-indicator span:nth-of-type(2) {
-webkit-animation: 1s blink infinite 0.6666s;
animation: 1s blink infinite 0.6666s;
}
.typing-indicator span:nth-of-type(3) {
-webkit-animation: 1s blink infinite 0.9999s;
animation: 1s blink infinite 0.9999s;
}
Should get transformed to:
@-webkit-keyframes blink-data-v-✘✘✘✘✘✘xx {
50% {
opacity: 1;
}
}
@keyframes blink-data-v-✘✘✘✘✘✘xx {
50% {
opacity: 1;
}
}
@-webkit-keyframes bulge-data-v-✘✘✘✘✘✘xx {
50% {
-webkit-transform: scale(1.05);
transform: scale(1.05);
}
}
@keyframes bulge-data-v-✘✘✘✘✘✘xx {
50% {
-webkit-transform: scale(1.05);
transform: scale(1.05);
}
}
.typing-indicator {
...
-webkit-animation: 2s bulge-data-v-✘✘✘✘✘✘xx infinite ease-out;
animation: 2s bulge-data-v-✘✘✘✘✘✘xx infinite ease-out;
}
.typing-indicator span:nth-of-type(1) {
-webkit-animation: 1s blink-data-v-✘✘✘✘✘✘xx infinite 0.3333s;
animation: 1s blink-data-v-✘✘✘✘✘✘xx infinite 0.3333s;
}
.typing-indicator span:nth-of-type(2) {
-webkit-animation: 1s blink-data-v-✘✘✘✘✘✘xx infinite 0.6666s;
animation: 1s blink-data-v-✘✘✘✘✘✘xx infinite 0.6666s;
}
.typing-indicator span:nth-of-type(3) {
-webkit-animation: 1s blink-data-v-✘✘✘✘✘✘xx infinite 0.9999s;
animation: 1s blink-data-v-✘✘✘✘✘✘xx infinite 0.9999s;
}
However it only get’s transformed to:
@-webkit-keyframes blink-data-v-✘✘✘✘✘✘xx {
50% {
opacity: 1;
}
}
@keyframes blink-data-v-✘✘✘✘✘✘xx {
50% {
opacity: 1;
}
}
@-webkit-keyframes bulge-data-v-✘✘✘✘✘✘xx {
50% {
-webkit-transform: scale(1.05);
transform: scale(1.05);
}
}
@keyframes bulge-data-v-✘✘✘✘✘✘xx {
50% {
-webkit-transform: scale(1.05);
transform: scale(1.05);
}
}
.typing-indicator {
...
-webkit-animation: 2s bulge infinite ease-out;
animation: 2s bulge infinite ease-out;
}
.typing-indicator span:nth-of-type(1) {
-webkit-animation: 1s blink infinite 0.3333s;
animation: 1s blink infinite 0.3333s;
}
.typing-indicator span:nth-of-type(2) {
-webkit-animation: 1s blink infinite 0.6666s;
animation: 1s blink infinite 0.6666s;
}
.typing-indicator span:nth-of-type(3) {
-webkit-animation: 1s blink infinite 0.9999s;
animation: 1s blink infinite 0.9999s;
}
Something to note: in the actual transformation, references to keyframe names in animation rules are missing the
-data-v-✘✘✘✘✘✘xx
at the end. This is the bug.
Currently (as of 47c3317) the animation name in shorthand animation rule declarations is identified by getting the first value out of splitting the animation rule by any whitespace character[1]. However the formal definition for the animation property states the animation name could appear anywhere within the rule definition.
<single-animation> = <time> || <single-timing-function> || <time> || <single-animation-iteration-count> || <single-animation-direction> || <single-animation-fill-mode> || <single-animation-play-state> || [ none | <keyframes-name> ]
– animation
formal syntax[2]
Therefore, while your animation declarations are valid, vue-loader is not able to parse it.
Workaround
The current workaround for this is to move your animation names to the beginning of animation rule declarations. Your keyframe declarations do not need changing, they remain inside the scoped stylesheet. Your animation declarations should now look like this:
.typing-indicator {
...
-webkit-animation: bulge 2s infinite ease-out;
animation: bulge 2s infinite ease-out;
}
.typing-indicator span:nth-of-type(1) {
-webkit-animation: blink 1s infinite 0.3333s;
animation: blink 1s infinite 0.3333s;
}
.typing-indicator span:nth-of-type(2) {
-webkit-animation: blink 1s infinite 0.6666s;
animation: blink 1s infinite 0.6666s;
}
.typing-indicator span:nth-of-type(3) {
-webkit-animation: blink 1s infinite 0.9999s;
animation: blink 1s infinite 0.9999s;
}
References
- [1] vue-loader/lib/style-compiler/plugins/scope-id.js#L67 @ 47c3317
- [2] Definition for
animation
in the Editor’s Draft of W3C specification CSS Animations Level 1
Answered By: Anonymous
Disclaimer: This content is shared under creative common license cc-by-sa 3.0. It is generated from StackExchange Website Network.