13

I'm trying to implement a CSS typing indicator in Vue. Without Vue, it looks like this:

.typing-indicator {
  background-color: #E6E7ED;
  width: auto;
  border-radius: 50px;
  padding: 20px;
  display: table;
  margin: 0 auto;
  position: relative;
  -webkit-animation: 2s bulge infinite ease-out;
          animation: 2s bulge infinite ease-out;
}
.typing-indicator:before, .typing-indicator:after {
  content: '';
  position: absolute;
  bottom: -2px;
  left: -2px;
  height: 20px;
  width: 20px;
  border-radius: 50%;
  background-color: #E6E7ED;
}
.typing-indicator:after {
  height: 10px;
  width: 10px;
  left: -10px;
  bottom: -10px;
}
.typing-indicator span {
  height: 15px;
  width: 15px;
  float: left;
  margin: 0 1px;
  background-color: #9E9EA1;
  display: block;
  border-radius: 50%;
  opacity: 0.4;
}
.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;
}

@-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);
  }
}
html {
  display: table;
  height: 100%;
  width: 100%;
}

body {
  display: table-cell;
  vertical-align: middle;
}
<div class="typing-indicator">
  <span></span>
  <span></span>
  <span></span>
</div>

– 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?

0

2 Answers 2

15

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

Sign up to request clarification or add additional context in comments.

5 Comments

Thank you. In your explanation, I don't see where should the keyframes be located. What do you mean by "move your animation names to the beginning of animation rule declarations" ? I finally declared my keyframes in another style block separated form the scoped one. It works but could impact the website using my plugin. The solution is to choose long (and hope unique) name for animation (GUID)
@PierreClocher: Look at the animation declarations for .typing-indicator and .typing-indicator span:nth-of-type(n). They currently follow the pattern: animation: <animation-duration> <animation-name> <...other animation values> (eg 2s bulge infinite ease-out and 1s blink infinite 0.3333s). What you need to do is put animation-name at the beginning of the rule declaration: animation: <animation-name> <animation-duration> <...other animation values> (eg bulge 2s infinite ease-out and blink 1s infinite 0.3333s). Your @keyframe declarations can remain in the scoped style.
Found this answer after experiencing problems with spin.js, where the animation name is built dynamically by the JavaScript parser according to settings passed in via a JSON object. The proposed work-around is therefore not possible without a re-write of the entire plug-in. What needs to be done instead (as hinted by @PierreClocher), is to define the animation @keyframes declaration at a higher level. I built them into my webkit CSS compilation so that they are then included in the overall CSS file for the entire page.
@cartbeforehorse: yes, moving the keyframe declarations into the global scope will workaround the issue as well. However, for this specific question it doesn't satisfy the original goal of keeping the keyframe declarations within the scope of a specific Vue component.
@cartbeforehorse For spin.js with Vue v3, I'm using this in a custom Spinner component. I found it was as simple as adding the following: <style lang="scss">@import '~spin.js/spin.css';</style>
0

I met the same problem and the first answer did tell me why it does not work, but the workaround part did not quite fix my issue... this is my code:

/* Animations */
@keyframes moveOut1 {
    from {
        transform: translateY(0) scale(0);
    }
    3% {
        transform: translateY(0.2em) scale(1);
    }
    97% {
        transform: translateY(7.3em) scale(1);
    }
    to {
        transform: translateY(7.5em) scale(0);
    }
}
@keyframes moveOut2 {
    from {
        transform: rotate(60deg) translateY(0) scale(0);
    }
    3% {
        transform: rotate(60deg) translateY(0.2em) scale(1);
    }
    97% {
        transform: rotate(60deg) translateY(7.3em) scale(1);
    }
    to {
        transform: rotate(60deg) translateY(7.5em) scale(0);
    }
}
@keyframes moveOut3 {
    from {
        transform: rotate(120deg) translateY(0) scale(0);
    }
    3% {
        transform: rotate(120deg) translateY(0.2em) scale(1);
    }
    97% {
        transform: rotate(120deg) translateY(7.3em) scale(1);
    }
    to {
        transform: rotate(120deg) translateY(7.5em) scale(0);
    }
}
@keyframes moveOut4 {
    from {
        transform: rotate(180deg) translateY(0) scale(0);
    }
    3% {
        transform: rotate(180deg) translateY(0.2em) scale(1);
    }
    97% {
        transform: rotate(180deg) translateY(7.3em) scale(1);
    }
    to {
        transform: rotate(180deg) translateY(7.5em) scale(0);
    }
}
@keyframes moveOut5 {
    from {
        transform: rotate(240deg) translateY(0) scale(0);
    }
    3% {
        transform: rotate(240deg) translateY(0.2em) scale(1);
    }
    97% {
        transform: rotate(240deg) translateY(7.3em) scale(1);
    }
    to {
        transform: rotate(240deg) translateY(7.5em) scale(0);
    }
}
@keyframes moveOut6 {
    from {
        transform: rotate(300deg) translateY(0) scale(0);
    }
    3% {
        transform: rotate(300deg) translateY(0.2em) scale(1);
    }
    97% {
        transform: rotate(300deg) translateY(7.3em) scale(1);
    }
    to {
        transform: rotate(300deg) translateY(7.5em) scale(0);
    }
}
@keyframes ripple {
    from,
    to {
        width: 0.2em;
    }
    33% {
        width: 2.4em;
    }
}

so I asked a friend and the solution he provided me with is simply to place the css code out side and then import it into the vue component via

<style>
@import url(./{css_file_name}.css);
</style>

but I do not understand the mechanism behind this... but to me, it's fine as long as it works.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.