1

Imagine we have a UserCard component:

function UserCard(props) {
  const { avatarUrl, name, loading } = props
  return (
    <div className='UserCard'>
      <Avatar image={avatarUrl} />
      <Name name={name} />
    </div>
  )
}

Now we want to implement a wireframe loading for it.

  • We can't design a wireframe copy of Avatar and Name components - their authors may and will change their size and shape, we don't want our skeleton to fall out of sync
  • We can't insert our loader inside Avatar and Name components - we don't have control over them or their css
  • We can control the space between Avatar and Name and wrap them from the outside
  • Avatar and Name may have some transparent portions / border-radius and we want to draw our skeleton animation over their visible part

Ideally the usage of our Loader should look somewhat like this:

function UserCard(props) {
  const { avatarUrl, name, loading } = props
  return (
    <div className='UserCard'>
      <Loader loading={loading}>
        <Avatar image={avatarUrl} />
      </Loader>
      <Loader loading={loading}>
        <Name name={loading ? 'some text to control the size of the skeleton' : name} />
      </Loader>
    </div>
  )
}

What are the ways to achieve it? What if we have similar restrictions but want to add skeleton loading to the UserCard. From outside we know it's a single div wrapping other divs and we want to show a skeleton instead of each of those child divs.

R. Richards
  • 24,603
  • 10
  • 64
  • 64
grabantot
  • 2,111
  • 20
  • 31
  • Just had an idea, setting css `> * {background: ...}` to show loader and `> * > * { visibility: hidden }` to hide children may work. Going to play with it. – grabantot Jul 26 '22 at 17:10

1 Answers1

0

Eventually the best I came up with is this:

// Usage:
// @use 'styles/loading';
// .loading {
//   @extend .wireframe;
// }
.wireframe {
  background-color: var(--colors-borderColor2);
  border-radius: 4px;
  position: relative;
  color: transparent; // hide text
  overflow: hidden;
  white-space: nowrap;

  > * {
    visibility: hidden; // hide children
  }

  &::after {
    display: block;
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    transform: translateX(-100%);
    background: linear-gradient(
      90deg,
      transparent,
      rgba(255, 255, 255, 0.4),
      transparent
    );
    animation: loading 1.5s infinite;
  }

  @keyframes loading {
    100% {
      transform: translateX(100%);
    }
  }
}

The components that should be wrapped are targeted by a css selector and skeleton shape and size can be adjusted below the @extend.

So our UserCard component would look like this:

import { stylesheet } from 'astroturf'

const styles = stylesheet`
  @use 'styles/loading';
  .UserCard {
    //...
  }
  
  .UserCard:global(.loading) {
    > * {
      @extend .wireframe;
      
     &:first-child {
       border-radius: 50%; // make avatar round
     }
    }
  }
`

function UserCard(props) {
  const { avatarUrl, name, loading } = props
  return (
    <div className={cx('UserCard', { loading })>
      <Avatar image={avatarUrl} />
      <Name name={name} />
    </div>
  )
}

Not ideal but works for me.

grabantot
  • 2,111
  • 20
  • 31