AI CSS Animations

Email Inbox Skeleton

A two-pane mail client: message list on the left, reading pane on the right. The selection steps down the list and the pane shimmers again each time it lands, like opening the next email.

Also known as: mail app skeleton, inbox list placeholder, email client loading.

HTML

<div class="ldr-skeleton-inbox" role="status" aria-label="Loading content">
  <div class="ldr-skeleton-inbox-list">
    <div class="ldr-skeleton-inbox-row"><span></span><span></span><span></span></div>
    <div class="ldr-skeleton-inbox-row"><span></span><span></span><span></span></div>
    <div class="ldr-skeleton-inbox-row"><span></span><span></span><span></span></div>
    <div class="ldr-skeleton-inbox-row"><span></span><span></span><span></span></div>
  </div>
  <div class="ldr-skeleton-inbox-pane"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
</div>

CSS

.ldr-skeleton-inbox {
  --s: var(--ldr-size, 48px);
  --c: var(--ldr-color, #3b82f6);
  --row: calc(var(--s) * 0.7);
  display: grid;
  grid-template-columns: calc(var(--s) * 2.9) 1fr;
  width: calc(var(--s) * 7.4);
  overflow: hidden;
  border: 1px solid color-mix(in srgb, var(--ldr-color, #3b82f6) 18%, transparent);
  border-radius: calc(var(--s) / 7);
}
.ldr-skeleton-inbox-list {
  position: relative;
  border-right: 1px solid color-mix(in srgb, var(--ldr-color, #3b82f6) 14%, transparent);
}
.ldr-skeleton-inbox-list::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: var(--row);
  background: color-mix(in srgb, var(--ldr-color, #3b82f6) 10%, transparent);
  box-shadow: inset calc(var(--s) / 16) 0 0 var(--c);
  animation: ldr-skeleton-inbox-select 4.8s cubic-bezier(0.65, 0, 0.35, 1) infinite;
}
.ldr-skeleton-inbox-row {
  position: relative;
  display: grid;
  grid-template-columns: 1fr auto;
  align-content: center;
  gap: calc(var(--s) / 12) calc(var(--s) / 6);
  box-sizing: border-box;
  height: var(--row);
  padding: 0 calc(var(--s) / 5);
}
.ldr-skeleton-inbox-row + .ldr-skeleton-inbox-row { border-top: 1px solid color-mix(in srgb, var(--ldr-color, #3b82f6) 10%, transparent); }
.ldr-skeleton-inbox-row span {
  height: calc(var(--s) / 9);
  border-radius: var(--s);
  background: color-mix(in srgb, var(--ldr-color, #3b82f6) 16%, transparent);
}
.ldr-skeleton-inbox-row span:nth-child(1) { width: 62%; height: calc(var(--s) / 7); background: color-mix(in srgb, var(--ldr-color, #3b82f6) 28%, transparent); }
.ldr-skeleton-inbox-row span:nth-child(2) { width: calc(var(--s) * 0.4); }
.ldr-skeleton-inbox-row span:nth-child(3) { grid-column: 1 / -1; width: 88%; }
.ldr-skeleton-inbox-row:nth-child(2) span:nth-child(1) { width: 48%; }
.ldr-skeleton-inbox-row:nth-child(3) span:nth-child(3) { width: 70%; }
.ldr-skeleton-inbox-row:nth-child(4) span:nth-child(1) { width: 56%; }
.ldr-skeleton-inbox-row:nth-child(odd)::before {
  content: "";
  position: absolute;
  top: calc(var(--row) / 2 - var(--s) / 5);
  left: calc(var(--s) / 14);
  width: calc(var(--s) / 12);
  height: calc(var(--s) / 12);
  border-radius: 50%;
  background: var(--c);
}
.ldr-skeleton-inbox-pane {
  display: grid;
  grid-template-columns: auto 1fr;
  align-content: start;
  gap: calc(var(--s) / 9) calc(var(--s) / 6);
  padding: calc(var(--s) / 4);
}
.ldr-skeleton-inbox-pane span {
  grid-column: 1 / -1;
  height: calc(var(--s) / 9);
  border-radius: var(--s);
  background: linear-gradient(90deg, color-mix(in srgb, var(--ldr-color, #3b82f6) 16%, transparent) 30%, color-mix(in srgb, var(--ldr-color, #3b82f6) 34%, transparent) 50%, color-mix(in srgb, var(--ldr-color, #3b82f6) 16%, transparent) 70%) 100% 0 / 300% 100%;
  animation: ldr-skeleton-inbox-read 1.2s ease-out infinite;
}
.ldr-skeleton-inbox-pane span:nth-child(1) { width: 78%; height: calc(var(--s) / 5); margin-bottom: calc(var(--s) / 12); }
.ldr-skeleton-inbox-pane span:nth-child(2) {
  grid-column: 1;
  grid-row: span 2;
  width: calc(var(--s) * 0.4);
  height: calc(var(--s) * 0.4);
  border-radius: 50%;
}
.ldr-skeleton-inbox-pane span:nth-child(3) { grid-column: 2; width: 46%; height: calc(var(--s) / 7); align-self: end; }
.ldr-skeleton-inbox-pane span:nth-child(4) { grid-column: 2; width: 64%; margin-bottom: calc(var(--s) / 8); }
.ldr-skeleton-inbox-pane span:nth-child(6) { width: 94%; }
.ldr-skeleton-inbox-pane span:nth-child(7) { width: 58%; }
@keyframes ldr-skeleton-inbox-select {
  0%, 20%  { transform: translateY(0); }
  25%, 45% { transform: translateY(var(--row)); }
  50%, 70% { transform: translateY(calc(var(--row) * 2)); }
  75%, 95% { transform: translateY(calc(var(--row) * 3)); }
  100%     { transform: translateY(0); }
}
@keyframes ldr-skeleton-inbox-read {
  70%, 100% { background-position: 0% 0; }
}

@media (prefers-reduced-motion: reduce) {
  .ldr-skeleton-inbox, .ldr-skeleton-inbox *, .ldr-skeleton-inbox::before, .ldr-skeleton-inbox::after, .ldr-skeleton-inbox *::before, .ldr-skeleton-inbox *::after {
    animation-duration: 3s;
  }
}

Customizing this loader

  • Color: set --ldr-color on the element or a parent. Currently previewing #3b82f6.
  • Size: set --ldr-size. Every dimension in the snippet is derived from it. Currently previewing 64px.
  • Speed: change the duration on the animation shorthand. Delays on the children are relative, so they keep their rhythm.
  • Accessibility: the root carries role="status" and an aria-label so screen readers announce the wait. The reduced-motion rule at the end slows the animation for users who asked for less motion.

Similar loaders