Why Are CSS Keyframe Animations Broken in Vue Components with Scoped Styling

Why are CSS keyframe animations broken in Vue components with scoped styling?

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-xxxxxxxx {
50% {
opacity: 1;
}
}

@keyframes blink-data-v-xxxxxxxx {
50% {
opacity: 1;
}
}
@-webkit-keyframes bulge-data-v-xxxxxxxx {
50% {
-webkit-transform: scale(1.05);
transform: scale(1.05);
}
}
@keyframes bulge-data-v-xxxxxxxx {
50% {
-webkit-transform: scale(1.05);
transform: scale(1.05);
}
}

.typing-indicator {
...
-webkit-animation: 2s bulge-data-v-xxxxxxxx infinite ease-out;
animation: 2s bulge-data-v-xxxxxxxx infinite ease-out;
}

.typing-indicator span:nth-of-type(1) {
-webkit-animation: 1s blink-data-v-xxxxxxxx infinite 0.3333s;
animation: 1s blink-data-v-xxxxxxxx infinite 0.3333s;
}
.typing-indicator span:nth-of-type(2) {
-webkit-animation: 1s blink-data-v-xxxxxxxx infinite 0.6666s;
animation: 1s blink-data-v-xxxxxxxx infinite 0.6666s;
}
.typing-indicator span:nth-of-type(3) {
-webkit-animation: 1s blink-data-v-xxxxxxxx infinite 0.9999s;
animation: 1s blink-data-v-xxxxxxxx infinite 0.9999s;
}

However it only get's transformed to:

@-webkit-keyframes blink-data-v-xxxxxxxx {
50% {
opacity: 1;
}
}

@keyframes blink-data-v-xxxxxxxx {
50% {
opacity: 1;
}
}
@-webkit-keyframes bulge-data-v-xxxxxxxx {
50% {
-webkit-transform: scale(1.05);
transform: scale(1.05);
}
}
@keyframes bulge-data-v-xxxxxxxx {
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-xxxxxxxx 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

Why are @keyframes animations not working with Vue.js?

in the h2 styles: color: white; will override the animation color rules.

Either remove color: white; from the h2 styles, or add the class flash to the h2 element, instead of the a element.

Vue/Vue Router Scoped CSS Not Clearing on Navigation

In Home.vue component style add the scoped attribute to the style tag:

<style scoped>
.content {
min-height: 100vh;
background-size: cover;
background-image: url(https://assets.website-files.com/5e832e12eb7ca02ee9064d42/5f915422ccb28e626ad16e20_Group%20939.jpg);
}

.content:after {
content: '';
position: absolute;
height: 100%;
width: 100%;
background-color: rgba(0,0,0,.5);
top: 0;
left: 0;
}
</style>

vue enter transition not working properly

There are a few problems in your CSS.

CSS Transitions and CSS Animations

A transition can be implemented using either CSS Transitions or CSS Animations. Your CSS incorrectly mixes the two concepts in this case.

In particular, the slideIn keyframes and .section-enter/.section-enter-to rules are effectively performing the same task of moving .section into view. However, this is missing a transition rule with a non-zero time, required to animate the change, so the change occurs immediately. The same issue exists for the slideOut keyframes and leave rules.

.section-enter {
top: 100vh;
}
.section-enter-to {
top: 0;
}
.section-enter-active {
transition: .5s; /* MISSING RULE */
}

.section-leave {
top: 0;
}
.section-leave-to {
top: -100vh;
}
.section-leave-active {
transition: .5s; /* MISSING RULE */
}

Removing the keyframes, and adding the missing rules (as shown above) would result in a working CSS Transition.

demo 1

Using CSS Animations

Alternatively, you could use keyframes with CSS Animations, where the animation is applied only by the *-active rules, and no *-enter/*-leave rules are used. Note your question contained unnecessary quotes in animation-name: 'slideIn';, which is invalid syntax and would be silently ignored (no animation occurs). I use a simpler shorthand in the following snippet (animation: slideIn 1s;).

.section-enter-active {
animation: slideIn 1s;
}
.section-leave-active {
animation: slideOut 1s;
}

@keyframes slideIn {
from {
top: 100vh;
}
to {
top: 0;
}
}
@keyframes slideOut {
from {
top: 0;
}
to {
top: -100vh;
}
}

demo 2

Optimizing CSS Transitions

You could also tweak your animation performance by using translateY instead of transitioning top.

/* top initially 0 in .wrapper */

.section-leave-active,
.section-enter-active {
transition: .5s;
}
.section-enter {
transform: translateY(100%);
}
.section-leave-to {
transform: translateY(-100%);
}

demo 3

Vue.js style v-html with scoped css

As stated in my answer here:

New version of vue-loader (from version 12.2.0) allows you to use "deep scoped" css. You need to use it that way:

<style scoped> now support "deep" selectors that can affect child
components using the >>> combinator:

.foo >>> .bar { color: red; } will be compiled into:

.foo[data-v-xxxxxxx] .bar { color: red; }

More informations on the release page of vue-loader

Vue 3 nested transition not working, no error

There is an ul that just removes the entire transition-group
so when you delete the last element, you don't see the transition because <ul class="ml-4" v-if="item.entries.length > 0"> removes the entire group.

You could just delete the v-if="item.entries.length > 0" part and have it transtion-group render 0 items.

<template>
<ul>
<transition-group
appear
name="groups"
tag="div"
enter-active-class="bounceInDown"
leave-active-class="slideOutRight"
>
<li
v-for="(item, index) in groups"
class="my-2 px-4 py-2 rounded shadow"
:key="`root_${item.name}`"
>
<div class="flex justify-between">
<div>{{ item.name }}</div>
<div>
<a href="#" class="ml-2" @click.prevent="deleteGroup(item.id)">
<i class="fas fa-trash"></i>
</a>
</div>
</div>
<ul class="ml-4">
<transition-group
name="entries"
tag="div"
enter-active-class="bounceInDown"
leave-active-class="slideOutRight"
>
<EntryItem
v-for="entry in item.entries"
:key="`entry_${entry.name}`"
:entry="entry"
/>
</transition-group>
</ul>
</li>
</transition-group>
</ul>
</template>

Vue.js: “data-v-hash” attributes for scoped CSS

With your approach, specificity of selectors changes differently: the deeper the element is, the longer is its selector chain. Unequal specificity can open door to very subtle bugs - reproducible, yes, but still subtle. To add insult to injury, you won't be able to spot these bugs by looking at the code alone - you'll have to check the builds.

Still, if this is not a problem for your methodology and/or project scope, you can still employ this approach with vuejs-loader. Quoting the doc:

If you want a selector in scoped styles to be "deep", i.e. affecting
child components, you can use the >>> combinator:

<style scoped> 
.a >>> .b { /* ... */ }
</style>

The above will be
compiled into:

.a[data-v-f3f3eg9] .b { /* ... */ }

Some pre-processors, such as SASS,
may not be able to parse >>> properly. In those cases you can use the
/deep/ combinator instead - it's an alias for >>> and works exactly
the same.

Vuejs transition is not working to show a modal

It's because you have v-if="loading" on .modal-overlay. This doesn't give it a chance to evaluate the condition and transition the element.

Remove that and it should work:

Vue.createApp({
data() {
return {
loading: false
}
},
created() {
setInterval(() => {
this.loading = !this.loading
}, 2000)
}
}).mount('#app')
.dialog {
text-align: center;
border-radius: 10px;
width: 10%;
min-width: fit-content;
border: 2px solid #00f6f6;
background-color: #0f0f0f;
color: #efefef;
padding: 10px;
/* animation: enter 0.25s linear; */
overflow: hidden;
}

.modal-overlay {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.4);
}

.loading-enter-active {
animation: enter 0.2s ease-out;
}

.loading-leave-active {
animation: enter 0.2s ease-in reverse;
}

@keyframes enter {
0% {
opacity: 0%;
transform: scale(0);
}
100% {
opacity: 100%;
transform: scale(1);
}
}
<script src="https://unpkg.com/vue@next"></script>
<div id="app">
<teleport to="body">
<div class="modal-overlay">
<transition name="loading">
<div class="dialog" v-if="loading">
<h1>LOADING</h1>
</div>
</transition>
</div>
</teleport>
</div>


Related Topics



Leave a reply



Submit