Skip to content

Instantly share code, notes, and snippets.

@cibulka
Last active July 20, 2023 09:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cibulka/ed4922d15d93dbbbba213ca43f349f32 to your computer and use it in GitHub Desktop.
Save cibulka/ed4922d15d93dbbbba213ca43f349f32 to your computer and use it in GitHub Desktop.
Guide how to prevent CLS with skeleton elements

CLS - Suggestions how to fix the issue

CLS stands for “content layout shift”: When the elements are added dynamically during the page load (or their size changes), it can cause the page to jump unexpectedly.

This guide shows how to fix this issue with practical code examples written in React.

Simple example

const data = fetch(‘https://example.com');
if (!data) return null;
return (
  <div>
    Element showing the data
  </div>
);

Before the data are loaded, the page does not show anything (meaning there is 0px area dedicated for the element). After the data are loaded, usually in 1-2s, the content is abruptly put on the place of the element, adding extra 16px (or similar) which causes the page to shift. That is especially unpleasant when the dynamically added content is above the user's scroll position.

The example above could be fixed like this:

const data = fetch(‘https://example.com');
if (!data) {
  return (
    <div>Loading ...</div>
  );
};
return (
  <div>
    Element showing the data
  </div>
);

As element with "loading" has same height as the element with the data (both are on 1 line), the page remains the same size and does not "jump" unexpectedly during the loading.

Spinners

The elements that are being dynamically added to the page are rarely a single line of text. Also it simply would not look very nice if UI was full of "Loading..." texts.

For this reason I recommend to use "spinner elements" - made as components with dimmensions1 and type provided as a parameter, and use them in place of UI that dynamically changes during the page load.

Here is how Facebook does it:

If you look closer to the page, you see that there are only 3 types of elements: circle, line, rectangle.

Components (JSX)

function Skeleton(props) {
  let className;
  switch (props.type) {
    case 'circle':
      className = 'skeleton-circle';
      break;
    case 'square':
      className = 'skeleton-square';
      break;
    case 'line':
      className = 'skeleton-line';
      break;
    default:
      throw new Error(`Unknown type ${props.type}.`);
  }

  return (
    <div 
      className={className} 
      style={{ height: props.height, width: props.width }} 
    />
  );
}

Components (CSS)

@keyframes pulse {
  50% {
    opacity: .5;
  }
}

.skeleton-circle, .skeleton-square, .skeleton-line:after {
  animation: pulse 2s cubic-bezier(.4,0,.6,1) infinite;
  background: #e5e7eb;
}

.skeleton-circle {
  aspect-ratio: 1 / 1;
  border-radius: 100%;
}

.skeleton-line {
  display: flex; 
  align-items: center;
}

.skeleton-line:after {
  content: '';
  display: block;
  width: 100%;
  height: 4px;
}

Implementation

This is how a spinner would be implemented in the previous example.

const data = await fetch('https://example.com');
if (!data) {
  return <Square height={100} />
}
return (
  <div style={{ height: 100 }}>
    Element showing data
  </div>
);

I've also made a JSFiddle where you can see the examples in practice: https://jsfiddle.net/cibulka/fowpj3zd/80/

Hope this helps! 💪

Footnotes

  1. There are a bit more elegant CSS approaches than providing hard-coded height. Provide the specific dimensions, however, is very simple. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment