2

I'm trying to add a subtle shimmer animation to the skeleton that looks like this one. I currently have a screen that looks like this (See on CodePen):

Skeleton screen

I'm trying to write a skeleton component that can accept an SVG like so:

<div class="skeleton" aria-busy="true">
  <svg width="233" height="68" viewBox="0 0 233 68" xmlns="http://www.w3.org/2000/svg">
    <g opacity="0.8">
      <rect x="79" y="32" width="154" height="11" rx="2" fill="black" fill-opacity="0.07"/>
      <rect width="179" height="20" rx="2" fill="black" fill-opacity="0.07"/> 
      <rect x="79" y="52" width="84" height="11" rx="2" fill="black" fill-opacity="0.07"/>
      <rect y="26" width="67" height="42" rx="2" fill="black" fill-opacity="0.07"/>
    </g>
  </svg>
</div>

Here is the CSS I'm using to animate a shimmer above the SVG:

.skeleton {
  overflow: hidden;
  position: relative;
}

.skeleton::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  background: linear-gradient(to right, rgb(243, 242, 241) 0%, rgb(237, 235, 233) 50%, rgb(243, 242, 241) 100%) 0px 0px / 90% 100% no-repeat rgb(243, 242, 241);
  transform: translateX(-100%);
  
  animation-name: skeleton-animation;
  animation-duration: 2s;
  animation-timing-function: ease-in-out;
  animation-direction: normal;
  animation-iteration-count: infinite;
}

@keyframes skeleton-animation {
  0% {
    transform: translateX(-100%);
  }
  100% {
    transform: translateX(100%);
  }
}

I'm trying to figure out how to use some sort of mask as described here, so that the animation shimmer only takes place over the SVG.

Muhammad Rehan Saeed
  • 35,627
  • 39
  • 202
  • 311

3 Answers3

4

One way could be to use the SVG as a mask-image and let the linear gradient background-position be updated:

example (update with your own gradient colors).:

.skeleton {
  overflow: hidden;
  position: relative;
  width: 233px;
  height: 68px;
  background: linear-gradient(to right, rgb(143, 142, 141) 0%, rgb(237, 235, 233) 50%, rgb(143, 142, 141) 100%) 0px 0px / 100% 100% rgb(243, 242, 241);
  -webkit-mask-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjMzIiBoZWlnaHQ9IjY4IiB2aWV3Qm94PSIwIDAgMjMzIDY4IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPg0KICAgIDxnIG9wYWNpdHk9IjAuOCI+DQogICAgICA8cmVjdCB4PSI3OSIgeT0iMzIiIHdpZHRoPSIxNTQiIGhlaWdodD0iMTEiIHJ4PSIyIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIxIi8+DQogICAgICA8cmVjdCB3aWR0aD0iMTc5IiBoZWlnaHQ9IjIwIiByeD0iMiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMSIvPiANCiAgICAgIDxyZWN0IHg9Ijc5IiB5PSI1MiIgd2lkdGg9Ijg0IiBoZWlnaHQ9IjExIiByeD0iMiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMSIvPg0KICAgICAgPHJlY3QgeT0iMjYiIHdpZHRoPSI2NyIgaGVpZ2h0PSI0MiIgcng9IjIiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjEiLz4NCiAgICA8L2c+DQogIDwvc3ZnPg==");
  margin: 1em;
  animation: linearAnim 2s infinite linear
}

@keyframes linearAnim {
  0% {
    background-position: 0px 0px;
  }
  100% {
    background-position: 230px 0px;
  }
}

/* demo purpose only */
.skeleton + .skeleton {

  background: linear-gradient(to right, rgba(143, 142, 141,0.75) 0%, rgba(237, 235, 233, 0.75) 50%, rgba(143, 142, 141, 0.75) 100%) 0px 0px / 100% 100% rgba(243, 242, 241, 0.5);
  }
  .skeleton.blue {
  background-color:blue;
  }
  .skeleton.red {
  background-color:red;
  }
  .skeleton.yellow {
  background-color:yellow;
  }
  .skeleton.green {
  background-color:green;
  }
  .flex {display:flex;align-items:center;justify-content:center;box-sizing:border-box;padding-top:0.6em;font-size:10px;color:#fff8;}
body {background:#bee; display:grid;grid-template-columns:repeat(auto-fill, 300px);}
<div class="skeleton" aria-busy="true"></div>
<div class="skeleton blue" aria-busy="true"></div>
<div class="skeleton red" aria-busy="true"></div>
<div class="skeleton yellow" aria-busy="true"></div>
<div class="skeleton green flex" aria-busy="true">On its way ...</div>

here is a similar question with different method CSS animation, only render overlay on specific elements


edit

if the requirement is to have your SVG inside the HTMl, you may give it an ID and do the encoding part on the fly

// https://www.w3docs.com/snippets/javascript/how-to-encode-and-decode-strings-with-base64-in-javascript.html

let svg = document.getElementById("mask").outerHTML
let sk =document.querySelector(".skeleton");
let encodedString = btoa(svg);
sk.setAttribute("style", " -webkit-mask-image: url('data:image/svg+xml;base64," + encodedString + "')");
.skeleton {
  overflow: hidden;
  position: relative;
  width: 233px;
  height: 68px;
  background: linear-gradient( to right, rgb(143, 142, 141) 0%, rgb(237, 235, 233) 50%, rgb(143, 142, 141) 100%) 0 0 /  200% 100%   rgb(243, 242, 241);
  margin: 1em;
  animation: linearAnim 1.25s infinite linear;
}

@keyframes linearAnim {
 
  100% {
    background-position: -200% 0;
  }
}

#mask {
  display: none;
}
<div class="skeleton" aria-busy="true">
<svg id="mask" width="233" height="68" viewBox="0 0 233 68" xmlns="http://www.w3.org/2000/svg">
    <g opacity="0.5">
      <rect x="79" y="32" width="154" height="11" rx="2" fill="black" fill-opacity="0.5"/>
      <rect width="179" height="20" rx="2" fill="black" fill-opacity="0.5"/> 
      <rect x="79" y="52" width="84" height="11" rx="2" fill="black" fill-opacity="0.5"/>
      <rect y="26" width="67" height="42" rx="2" fill="black" fill-opacity="0.5"/>
    </g>
  </svg>
</div>
G-Cyrillus
  • 101,410
  • 14
  • 105
  • 129
  • Since I'm trying to build a component, I'd like for users of my component to pass an arbitrary SVG file in via a slot e.g. `...` instead of hard coded in CSS. – Muhammad Rehan Saeed Sep 16 '20 at 12:38
  • 1
    @MuhammadRehanSaeed you may give a try to `-webkit-mask-image: url("#SVG_ID");}` , but you'll find out you better off use clip-path from an clipPath SVG https://developer.mozilla.org/fr/docs/Web/CSS/clip-path. from ther , bg position animation works the same. You would have to rethink the SVG way to build its shape – G-Cyrillus Sep 16 '20 at 13:07
  • Your edit is very close to what I'm looking for. The only issues are that animating `background-position` is slow and you're also having to set a specific width in the animation. Is there a way to solve these issues? – Muhammad Rehan Saeed Sep 29 '20 at 09:39
  • 1
    @MuhammadRehanSaeed animation-timing can be reset to your needs, background-size and background-position can also be used to tune it . I modified the second snippet. For the size, your svg is `width="233" height="68"` which the size of the mask :) – G-Cyrillus Sep 29 '20 at 19:51
2

If you don't need to use SVG, then this would be a little easier in HTML. But here's how to do it with SVGs.

.shimmer-rect {
  animation-name: skeleton-animation;
  animation-duration: 2s;
  animation-timing-function: linear;
  animation-direction: normal;
  animation-iteration-count: infinite;
}

@keyframes skeleton-animation {
  0% {
    transform: translateX(0px);
  }
  100% {
    transform: translateX(2000px);
  }
}

.skeleton {
  margin-bottom: 2em;
}
<!-- Include this once somewhere in your HTML -->
<svg width="0" height="0" style="position: absolute">
  <defs>
    <linearGradient id="shimmer" gradientUnits="userSpaceOnUse" x2="2000" spreadMethod="repeat">
      <stop offset="15%" stop-color="rgb(237,235,233)"/>
      <stop offset="25%" stop-color="rgb(243,242,241)"/>
      <stop offset="35%" stop-color="rgb(237,235,233)"/>
    </linearGradient>
    <mask id="shimmer-mask">
      <rect x="79" y="32" width="154" height="11" rx="2" fill="white"/>
      <rect width="179" height="20" rx="2" fill="white"/> 
      <rect x="79" y="52" width="84" height="11" rx="2" fill="white"/>
      <rect y="26" width="67" height="42" rx="2" fill="white"/>
    </mask>
    <g id="skel" mask="url(#shimmer-mask)">
      <rect class="shimmer-rect" x="-2000" width="3000" height="100%" fill="url(#shimmer)"/>
    </g>
  </defs>
</svg>


<div class="skeleton" aria-busy="true">
  <svg width="233" height="68" viewBox="0 0 233 68"> <use xlink:href="#skel"/> </svg>
</div>

<div class="skeleton" aria-busy="true">
  <svg width="233" height="68" viewBox="0 0 233 68"> <use xlink:href="#skel"/> </svg>
</div>

<div class="skeleton" aria-busy="true">
  <svg width="233" height="68" viewBox="0 0 233 68"> <use xlink:href="#skel"/> </svg>
</div>
Paul LeBeau
  • 97,474
  • 9
  • 154
  • 181
  • I'm trying to build a reusable Vue/React component e.g. `...`. Is it possible to move some of the shimmer logic up into the CSS and leave the SVG alone? – Muhammad Rehan Saeed Sep 16 '20 at 12:45
  • If you mean the ``, then no, that can't be moved to CSS. And I don't think there are any browsers that support CSS gradients on SVG elements. However the `` only needs to appear once on the page. Just put it somewhere in your page inside a ``. It'll reduce the amount of HTML a bit. – Paul LeBeau Sep 16 '20 at 18:28
  • In fact, most of the SVG only needs to appear once on the page. And you can use a minimal little SVG for each instance of the skeleton. See my updated answer. – Paul LeBeau Sep 16 '20 at 18:35
0

The easy way to create a css skeleton shimmer:

Create a simple class which you then apply to an HTML table and then you can use the HTML to shape / size your skeleton, like so:

@keyframes preloadAnimation {
    from {background-position: 100% 0}
    to {background-position: -80% 0}
}

.shimmer{background: linear-gradient(to right, #dedede 8%, #f3f3f3 18%, #dedede 33%);
     background-size: 200% 50px;
     animation: preloadAnimation 2s infinite;
     border-collapse:collapse;
     width:100%
   }

.shimmer td{background:transparent;border:2px solid white;height:18px}
.shimmer td.white{background-color:white}
<table class=shimmer>
          <tr><td style='height:30px'></td><td colspan=2 class=white></td></tr>
          <tr><td colspan=3 style='height:12px' class=white></td></tr>
          <tr><td colspan=2></td><td class=white></td></tr>
          <tr><td colspan=3 style='height:12px' class=white></td></tr>
          <tr><td></td><td></td><td></td></tr>
          <tr><td></td><td></td><td></td></tr>
          <tr><td colspan=3 style='height:12px' class=white></td></tr>
          <tr><td style='height:30px'></td><td colspan=2 class=white></td></tr>
        </table>
Vincent
  • 1,741
  • 23
  • 35