How to Animate a Skeleton Screen with a CSS Mask

Viewed 2458

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.

3 Answers

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>

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>

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>

Related