Blinds

No dependencies
Code format
React + Tailwind
import type { CSSProperties } from 'react';

const T = 3.4;
const SLATS = 10;

const css = `
  .loader-blinds { --frame: light-dark(#cbced3, #393c41); --sky: light-dark(#fbfcfd, #e5e6e9); --sun: light-dark(#fbfcfd, #f1f2f3); --slat: light-dark(#6d7179, #2e3035); --slat-hi: color-mix(in oklab, var(--slat) 70%, light-dark(#fbfcfd, #121417)); --slat-lo: color-mix(in oklab, var(--slat) 75%, light-dark(#373a40, #f1f2f3)); position: relative; width: 196px; height: 176px; }
  /* The window: a frame round a bright sky with the sun low in one corner. */
  .loader-blinds__window { position: absolute; inset: 10px 0 0; box-sizing: border-box; border: 7px solid var(--frame); border-radius: 3px; background: radial-gradient(circle at 76% 70%, var(--sun) 0 12px, color-mix(in oklab, var(--sun) 50%, transparent) 13px, transparent 46px), linear-gradient(color-mix(in oklab, var(--sky) 88%, light-dark(#6d7179, #7d8087)), var(--sky)); box-shadow: inset 0 0 0 1px color-mix(in oklab, light-dark(#fbfcfd, #121417) 40%, transparent), 0 10px 16px -8px light-dark(rgba(0, 0, 0, 0.14), rgba(0, 0, 0, 0.45)); }
  .loader-blinds__window::after { content: ''; position: absolute; left: 50%; top: 0; bottom: 0; width: 5px; margin-left: -2.5px; background: var(--frame); }
  .loader-blinds__rail { position: absolute; left: -6px; right: -6px; top: 0; height: 14px; border-radius: 3px; background: linear-gradient(var(--slat-lo), var(--slat) 60%, var(--slat-hi)); box-shadow: 0 2px 4px light-dark(rgba(0, 0, 0, 0.09), rgba(0, 0, 0, 0.3)); z-index: 2; }
  /* The slats tilt about their middles, in a wave from the top; the gaps let the light through. */
  .loader-blinds__slats { position: absolute; left: 9px; right: 9px; top: 18px; display: grid; grid-auto-rows: 15.4px; perspective: 260px; z-index: 1; }
  .loader-blinds__slat { border-radius: 2px; background: linear-gradient(var(--slat-lo), var(--slat) 45%, var(--slat-hi)); box-shadow: 0 1px 0 color-mix(in oklab, light-dark(#fbfcfd, #121417) 35%, transparent); transform: rotateX(0deg) scaleY(1.06); animation: loader-blinds-tilt ${T}s cubic-bezier(0.5, 0, 0.3, 1) infinite; animation-delay: calc(var(--i) * 55ms); }
  /* The ladder cords the slats hang in, and the wand that turns them. */
  .loader-blinds__cords { position: absolute; left: 28%; right: 28%; top: 14px; bottom: 6px; border-left: 1px solid color-mix(in oklab, var(--slat-hi) 70%, transparent); border-right: 1px solid color-mix(in oklab, var(--slat-hi) 70%, transparent); z-index: 1; }
  .loader-blinds__wand { position: absolute; right: 14px; top: 12px; width: 3px; height: 92px; border-radius: 2px; background: linear-gradient(90deg, var(--slat-hi), var(--slat-lo), var(--slat-hi)); transform-origin: 50% 0; z-index: 3; animation: loader-blinds-wand ${T}s cubic-bezier(0.5, 0, 0.3, 1) infinite; }
  @keyframes loader-blinds-tilt {
    0%, 8% { transform: rotateX(0deg) scaleY(1.06); }
    36%, 58% { transform: rotateX(74deg) scaleY(1); }
    86%, 100% { transform: rotateX(0deg) scaleY(1.06); }
  }
  @keyframes loader-blinds-wand { 0%, 8% { transform: rotate(0deg); } 30% { transform: rotate(6deg); } 40%, 60% { transform: rotate(-1deg); } 80% { transform: rotate(-5deg); } 92%, 100% { transform: rotate(0deg); } }
  @media (prefers-reduced-motion: reduce) {
    .loader-blinds * { animation: none !important; }
    .loader-blinds__slat { transform: rotateX(60deg); }
  }
`;

export function Preview() {
  return (
    <>
      <style href="kit-loaders-blinds" precedence="kit">
        {css}
      </style>
      <div className="loader-blinds" role="status" aria-label="Loading">
        <span className="loader-blinds__window" aria-hidden="true" />
        <span className="loader-blinds__slats" aria-hidden="true">
          {Array.from({ length: SLATS }, (_, i) => (
            <span key={i} className="loader-blinds__slat" style={{ '--i': i } as CSSProperties} />
          ))}
        </span>
        <span className="loader-blinds__cords" aria-hidden="true" />
        <span className="loader-blinds__rail" aria-hidden="true" />
        <span className="loader-blinds__wand" aria-hidden="true" />
      </div>
    </>
  );
}

Free to use in any project, commercial included. MIT-0, no attribution required. Licence

Install with shadcn

npx shadcn@latest add @movin/loader-blinds
Source on GitHub

About this component

A window blind whose slats tilt open in a wave from the top, letting the light through, then close the same way.

Dependencies
None
Difficulty
Intermediate
Triggers
continuous, loop, 3d, delay stagger
Techniques
keyframes, 3d transform, perspective, transition delay stagger
Reduced motion
Respected
Theme
Light and dark (follows your page's color-scheme)

The root has role="status" and aria-label="Loading"; everything inside it is aria-hidden. Under prefers-reduced-motion the slats rest mostly open.

More loaders

See all
Bowl
Bubble
Carousel
Chase