nnuer
강희태@gmlxo9917

썸네일 위를 빛이 훑고 지나가다 카드마다 차례로 걷히는 로딩

로딩 인터랙션

카드 썸네일 자리에 옅은 빛줄기가 반복해서 훑고 지나가다가, 이미지가 준비되면 카드마다 110ms씩 늦게 0.5초에 걸쳐 걷힙니다. 다시 보기로 반복할 수 있습니다.

#shimmer#skeleton#placeholder#cards2026. 10. 17. 업데이트

블록 신고

운영자가 확인한 뒤 필요하면 블록을 숨깁니다.

사유
Claude Code · Claude Opus 5.5 · React (TSX)
결과 코드펼치기
import { useEffect, useRef, useState } from "react";

const ART = "https://nnuer.site/brand/landing";
const CARDS = [
  { img: "kind-block.webp", title: "유리 질감 가격표 섹션", bg: "from-[#ede9fe] to-[#f5f3ff]" },
  { img: "kind-interaction.webp", title: "눌리면 튀어 오르는 버튼", bg: "from-[#fce7f3] to-[#fdf2f8]" },
  { img: "kind-graphic.webp", title: "광택 있는 3D 폴더 아이콘", bg: "from-[#cffafe] to-[#ecfeff]" },
];

/** A sweeping shimmer covers each thumbnail while it loads, then fades away card by card */
function Card({ img, title, bg, index, run }: { img: string; title: string; bg: string; index: number; run: number }) {
  const sheen = useRef<HTMLSpanElement>(null);
  const [loaded, setLoaded] = useState(false);

  useEffect(() => {
    setLoaded(false);
    // Hold the placeholder a little so the effect can be seen even when the image is cached
    const t = setTimeout(() => setLoaded(true), 900 + index * 110);
    return () => clearTimeout(t);
  }, [run, index]);

  useEffect(() => {
    if (matchMedia("(prefers-reduced-motion: reduce)").matches) return;
    const sweep = sheen.current?.animate([{ backgroundPosition: "100% 0" }, { backgroundPosition: "-150% 0" }], {
      duration: 1100,
      iterations: Infinity,
      easing: "linear",
    });
    return () => sweep?.cancel();
  }, []);

  return (
    <div className="overflow-hidden rounded-[18px] border border-black/[0.07] bg-white shadow-[0_24px_50px_-34px_rgba(60,40,120,0.35)]">
      <span className={`relative grid aspect-video place-items-center overflow-hidden bg-gradient-to-br ${bg}`}>
        <img src={`${ART}/${img}`} alt="" className="h-3/5 w-auto object-contain" />
        <span
          ref={sheen}
          aria-hidden
          className={`absolute inset-0 bg-[linear-gradient(100deg,#eceaf3_30%,#f7f6fb_50%,#eceaf3_70%)] bg-[length:300%_100%] transition-opacity duration-500 ${
            loaded ? "opacity-0" : "opacity-100"
          }`}
        />
      </span>
      <span className="block truncate px-3.5 pb-3.5 pt-3 text-[14px] font-semibold text-[#111114]">{title}</span>
    </div>
  );
}

export default function ShimmerThumbnails() {
  const [run, setRun] = useState(0);
  return (
    <section className="flex w-full flex-col items-center bg-[#fbfbfc] px-4 py-20">
      <div className="grid w-full max-w-[900px] grid-cols-1 gap-4 sm:grid-cols-3">
        {CARDS.map((c, i) => (
          <Card key={c.title} {...c} index={i} run={run} />
        ))}
      </div>
      <button
        type="button"
        onClick={() => setRun((n) => n + 1)}
        className="mt-10 rounded-full border border-black/10 bg-white px-4 py-2 text-[13px] font-semibold text-[#5c5c68] hover:text-[#111114] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#7c3aed]"
      >
        다시 보기
      </button>
    </section>
  );
}

적용 사이트 0

이 블록이 실제로 쓰인 사이트입니다.

로그인하고 내 사이트 추가

아직 등록된 사이트가 없습니다. 이 블록을 쓴 사이트가 있다면 알려 주세요.

로그인하고 재현 결과 올리기

재현 후기

v1 · 0개

아직 이 버전의 프롬프트로 직접 만들어 본 사람이 없습니다. 첫 번째로 재현해 보세요.