📖react-bits 파헤치기·7/7단계
100%

CSS로 만드는 호버 & 커서 이펙트

Part 1에서는 텍스트 애니메이션을 다뤘다 — CSS @keyframes부터 Canvas 2D, WebGL Shader까지. Part 2에서는 마우스/터치 입력에 반응하는 인터랙션을 다룬다. 인터랙션 애니메이션 28개를 분석해보니, 기술 선택의 기준은 "얼마나 정밀한 제어가 필요한가"였다. CSS만으로 충분한 것부터 WebGL 유체 시뮬레이션까지 — 이 시리즈에서는 그 스펙트럼을 하나씩 올라간다.

이번 편은 라이브러리 없이 CSS + 기본 DOM API만으로 가능한 인터랙션이다. 5개 컴포넌트를 세 가지 카테고리로 나눈다:

카테고리컴포넌트핵심 기술JS 역할
Pure CSS 호버GlareHoverCSS gradient + background-positionprops → CSS 변수 전달만
Pure CSS 호버StarBorderCSS @keyframes + radial-gradientprops → inline style만
마그넷 효과MagnetgetBoundingClientRect + transform마우스 좌표 → 오프셋 계산
마그넷 효과MagnetLinesCSS Grid + 동적 회전마우스 좌표 → 각도 계산
무한 루프LogoLoopResizeObserver + rAF속도 보간 + 무한 스크롤

Pure CSS 호버 — GlareHover, StarBorder

💡 CSS만으로 호버 효과를 만드는 두 가지 전략

CSS 호버 효과의 핵심은 :hover 의사 클래스와 transition의 조합이다. JavaScript가 하는 일은 React props를 CSS 커스텀 속성으로 전달하는 것뿐 — 실제 애니메이션은 CSS가 처리한다.

두 가지 전략이 있다:

  1. background-position 이동 (GlareHover) — :hover에서 gradient의 위치를 바꾸고, transition이 보간한다
  2. @keyframes 무한 반복 (StarBorder) — 항상 돌아가는 애니메이션을 overflow: hidden으로 보이는 영역만 노출한다

GlareHover — CSS gradient 반사 효과

카드에 마우스를 올리면 빛이 대각선으로 스와이프된다 — 글레어 크기와 각도를 조절해보자.

Hover Me
250%
0.5
-45deg
650ms

GlareHover의 JSX는 73줄이지만, 애니메이션 로직은 0줄이다. React 컴포넌트가 하는 유일한 일은 props를 CSS 커스텀 속성으로 변환하는 것이다.

1단계: hex → rgba 변환

// hex 색상 문자열을 rgba()로 변환 — CSS 변수에 투명도를 포함시키기 위함
const hex = glareColor.replace('#', '');
let rgba = glareColor;
if (/^[0-9A-Fa-f]{6}$/.test(hex)) {
  const r = parseInt(hex.slice(0, 2), 16);
  const g = parseInt(hex.slice(2, 4), 16);
  const b = parseInt(hex.slice(4, 6), 16);
  rgba = `rgba(${r}, ${g}, ${b}, ${glareOpacity})`;
}

#ffffff를 rgba(255, 255, 255, 0.5)로 변환한다. CSS에서 hex 색상에 직접 opacity를 적용할 수 없기 때문이다. glareOpacity prop이 rgba의 네 번째 인자로 들어간다.

2단계: CSS 커스텀 속성 전달

const vars = {
  '--gh-width': width,           // '500px'
  '--gh-height': height,         // '500px'
  '--gh-bg': background,         // '#000'
  '--gh-br': borderRadius,       // '10px'
  '--gh-angle': `${glareAngle}deg`,        // '-45deg'
  '--gh-duration': `${transitionDuration}ms`, // '650ms'
  '--gh-size': `${glareSize}%`,             // '250%'
  '--gh-rgba': rgba,             // 'rgba(255, 255, 255, 0.5)'
  '--gh-border': borderColor     // '#333'
};

9개의 CSS 커스텀 속성이 style prop으로 전달된다. React 쪽 작업은 여기서 끝이다.

3단계: CSS가 하는 모든 일

/* ::before 가상 요소 = 반사광 레이어 */
.glare-hover::before {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(
    var(--gh-angle),          /* -45deg → 좌상단에서 우하단 방향 */
    hsla(0, 0%, 0%, 0) 60%,  /* 0~60%: 투명 */
    var(--gh-rgba) 70%,       /* 60~70%: rgba(255,255,255,0.5) 반사광 */
    hsla(0, 0%, 0%, 0) 100%  /* 70~100%: 투명 */
  );
  transition: var(--gh-duration) ease;  /* 650ms */
  background-size: var(--gh-size) var(--gh-size);  /* 250% 250% */
  background-position: -100% -100%;   /* 초기: 좌상단 밖 → 보이지 않음 */
}

/* 호버 시: 우하단으로 이동 → transition이 650ms 동안 보간 */
.glare-hover:hover::before {
  background-position: 100% 100%;
}

데이터 흐름:

React props → CSS 커스텀 속성 (--gh-*)
  → ::before의 linear-gradient (반사광 모양 결정)
  → :hover → background-position 변경 (-100% -100% → 100% 100%)
  → CSS transition이 650ms 동안 보간
  → 대각선 스와이프 효과
속성초기값호버 시시각적 효과
background-position-100% -100%100% 100%좌상단 밖 → 우하단 밖으로 이동
background-size250% 250%(변경 없음)gradient가 요소보다 2.5배 큼
transition650ms ease(변경 없음)부드러운 보간

background-size: 250%가 핵심이다. 요소보다 gradient가 훨씬 크기 때문에, -100%에서 100%로 이동하는 동안 반사광의 좁은 밴드만 요소를 가로지른다. 100%면 gradient 전체가 보여서 반사 효과가 사라진다.

4단계: playOnce 모드

/* transition을 평소에 제거하고, 호버 시에만 적용 */
.glare-hover--play-once::before {
  transition: none;     /* 마우스 빼도 즉시 리셋 (되감기 없음) */
}
.glare-hover--play-once:hover::before {
  transition: var(--gh-duration) ease;  /* 호버 시에만 애니메이션 */
}

기본 모드에서는 마우스를 빼면 반사광이 되감긴다. playOnce를 켜면 마우스를 빼는 순간 transition: none이므로 즉시 원위치로 돌아가고, 다시 올리면 처음부터 재생된다.


StarBorder — CSS @keyframes 테두리 빛

버튼 테두리에 빛이 왕복한다 — 속도와 두께를 조절해보자.

6s
1px

StarBorder는 GlareHover와 달리 항상 돌아가는 애니메이션이다. :hover가 아니라 @keyframes로 무한 반복한다.

1단계: 레이어 구조

star-border-container (overflow: hidden)
├── border-gradient-bottom (z-index: 0) ← 하단 빛
├── border-gradient-top    (z-index: 0) ← 상단 빛
└── inner-content          (z-index: 1) ← 실제 콘텐츠
border-gradient-bottomz-index: 0 · 하단 빛 · 300% 너비 타원
border-gradient-topz-index: 0 · 상단 빛 · 300% 너비 타원
inner-contentz-index: 1 · 실제 콘텐츠 · overflow 차단
border-gradient-bottomz-index: 0 · 하단 빛 · 300% 너비 타원
border-gradient-topz-index: 0 · 상단 빛 · 300% 너비 타원
inner-contentz-index: 1 · 실제 콘텐츠 · overflow 차단

inner-content가 z-index: 1로 빛 레이어를 가린다. 빛은 inner-content의 테두리 바깥 틈으로만 보인다. 이 틈의 크기가 thickness prop이다:

style={{ padding: `${thickness}px 0` }}

padding: 1px 0이면 컨테이너와 inner-content 사이에 상하 1px 틈이 생기고, 그 틈으로 빛이 보인다.

2단계: radial-gradient 빛

// 점 형태의 빛 — 중심에서 바깥으로 10%까지만 불투명
background: `radial-gradient(circle, ${color}, transparent 10%)`,
animationDuration: speed  // '6s'

radial-gradient(circle, white, transparent 10%)에서 10%는 gradient 반지름의 10%까지만 색이 채워진다는 뜻이다. 나머지 90%는 투명하므로 결과적으로 중앙에 작은 점만 빛난다. 이 요소의 width: 300%와 조합하면, 실제 빛 점의 지름은 300% × 10% × 2 = 60% — 즉 요소 폭의 약 60% 크기가 된다. width를 키울수록 빛 점이 커지고, transparent 퍼센트를 높일수록 더 선명하게 날카로워진다.

50%
50%
50%
10%
background: radial-gradient(circle 100px at 50% 50%, #ffffff, transparent 10%);

3단계: @keyframes로 왕복

/* 하단 빛: 오른쪽에서 왼쪽으로 이동하며 투명해짐 */
@keyframes star-movement-bottom {
  0%   { transform: translate(0%, 0%); opacity: 1; }
  100% { transform: translate(-100%, 0%); opacity: 0; }
}

/* 상단 빛: 왼쪽에서 오른쪽으로 이동 (반대 방향) */
@keyframes star-movement-top {
  0%   { transform: translate(0%, 0%); opacity: 1; }
  100% { transform: translate(100%, 0%); opacity: 0; }
}

animation: star-movement-bottom linear infinite alternate에서 alternate가 핵심이다. 0% → 100% 후 자동으로 100% → 0%로 되돌아간다. 상단과 하단 빛이 반대 방향으로 이동하므로, 한쪽에서 빛이 사라지면 반대쪽에서 나타난다.

시간하단 빛 위치하단 opacity상단 빛 위치상단 opacity
0%translate(0%, 0%)1translate(0%, 0%)1
50%translate(-50%, 0%)0.5translate(50%, 0%)0.5
100%translate(-100%, 0%)0translate(100%, 0%)0
100% → 0%translate(-100%) → (0%)0 → 1translate(100%) → (0%)0 → 1

두 빛이 alternate로 왕복하면서 테두리 전체를 감싸는 효과를 만든다.


마그넷 효과 — Magnet, MagnetLines

💡 getBoundingClientRect + transform — 마우스 추적의 기본 패턴

앞서 GlareHover와 StarBorder는 JavaScript 로직이 거의 없었다. 마그넷 효과부터는 JavaScript가 매 mousemove 이벤트마다 좌표를 계산한다.

기본 패턴:

// 1. 요소의 화면 위치와 크기를 가져온다
const { left, top, width, height } = element.getBoundingClientRect();
const centerX = left + width / 2;
const centerY = top + height / 2;

// 2. 마우스와 요소 중심 사이의 거리를 계산한다
const distX = e.clientX - centerX;
const distY = e.clientY - centerY;

// 3. 거리에 따라 transform을 적용한다
element.style.transform = `translate3d(${distX / 2}px, ${distY / 2}px, 0)`;

getBoundingClientRect()는 요소의 화면 기준 위치를 반환한다 — 스크롤이나 부모 요소의 위치에 상관없이 브라우저 뷰포트 좌상단을 원점으로 한다. e.clientX/clientY도 동일한 좌표계이므로 직접 뺄셈할 수 있다.

Magnet — 마우스 방향으로 요소 끌어당기기

마우스를 요소 근처로 가져가면 끌려온다 — padding과 자석 강도를 조절해보자.

Drag me
100px
2

Magnet은 85줄의 간결한 컴포넌트다. CSS 파일이 없고, 모든 스타일이 inline으로 처리된다.

1단계: 감지 영역 판별

const { left, top, width, height } = magnetRef.current.getBoundingClientRect();
const centerX = left + width / 2;
const centerY = top + height / 2;

const distX = Math.abs(centerX - e.clientX);
const distY = Math.abs(centerY - e.clientY);

// 요소 경계 + padding 범위 안에 마우스가 있는지 판별
if (distX < width / 2 + padding && distY < height / 2 + padding) {
  // 범위 안 → 끌어당김
} else {
  // 범위 밖 → 원위치
}

감지 영역은 사각형이다. 요소 크기가 128x128px이고 padding: 100이면:

값계산결과
요소 반폭128 / 264px
감지 범위64 + 100164px (중심에서)
감지 영역 크기164 * 2328 x 328px

마우스가 중심에서 164px 이내에 들어오면 끌어당김이 시작된다.

2단계: 이동 거리 계산

const offsetX = (e.clientX - centerX) / magnetStrength;
const offsetY = (e.clientY - centerY) / magnetStrength;
setPosition({ x: offsetX, y: offsetY });

magnetStrength로 나누는 것이 핵심이다. 마우스가 중심에서 100px 떨어져 있고 magnetStrength: 2이면, 요소는 100/2 = 50px만 이동한다. 값이 클수록 약하게 끌린다.

magnetStrength마우스 거리 100px일 때 이동체감
1100px (마우스와 동일)달라붙음
250px적당한 끌림
520px미세한 끌림
1010px거의 안 움직임

3단계: transition으로 부드러운 이동

const transitionStyle = isActive ? activeTransition : inactiveTransition;
// isActive = true  → 'transform 0.3s ease-out'     (빠르게 따라감)
// isActive = false → 'transform 0.5s ease-in-out'   (천천히 원위치)

데이터 흐름:

window.mousemove → getBoundingClientRect() → 거리 판별
  → 범위 안?
    → offsetX = (mouseX - centerX) / magnetStrength
    → setPosition({ x: offsetX, y: offsetY })
    → transform: translate3d(offsetX, offsetY, 0)
    → transition: 0.3s ease-out (따라감)
  → 범위 밖?
    → setPosition({ x: 0, y: 0 })
    → transform: translate3d(0, 0, 0)
    → transition: 0.5s ease-in-out (원위치 복귀)

활성/비활성 상태에서 다른 transition을 적용하는 것이 자연스러운 느낌의 핵심이다. 끌려올 때는 빠르게(0.3s), 돌아갈 때는 여유롭게(0.5s).


MagnetLines — CSS Grid + 동적 회전

마우스를 움직이면 그리드의 모든 라인이 마우스 방향을 가리킨다 — 행/열 수와 색상을 바꿔보자.

9
9
-10deg

MagnetLines는 Magnet과 같은 getBoundingClientRect 패턴을 쓰지만, 하나의 요소가 아닌 N*N개의 요소가 각각 독립적으로 반응한다.

1단계: CSS Grid로 라인 배치

// rows * columns개의 span을 생성
const total = rows * columns;  // 9 * 9 = 81개
const spans = Array.from({ length: total }, (_, i) => (
  <span
    key={i}
    style={{
      '--rotate': `${baseAngle}deg`,  // 초기 각도 (예: -10deg)
      backgroundColor: lineColor,
      width: lineWidth,               // '1vmin'
      height: lineHeight              // '6vmin'
    }}
  />
));

CSS Grid가 81개의 span을 9x9 격자로 자동 배치한다:

gridTemplateColumns: `repeat(${columns}, 1fr)`,
gridTemplateRows: `repeat(${rows}, 1fr)`,

CSS에서 각 span은 transform: rotate(var(--rotate))로 회전한다. JavaScript가 --rotate를 바꾸면 즉시 반영된다.

2단계: 각도 계산 — 삼각함수

const onPointerMove = (pointer: { x: number; y: number }) => {
  items.forEach(item => {
    const rect = item.getBoundingClientRect();
    const centerX = rect.x + rect.width / 2;
    const centerY = rect.y + rect.height / 2;

    const b = pointer.x - centerX;  // x 거리 (밑변)
    const a = pointer.y - centerY;  // y 거리 (높이)
    const c = Math.sqrt(a * a + b * b) || 1;  // 빗변 (유클리드 거리)

    // acos(b/c) = 벡터와 x축 사이의 각도
    const r = ((Math.acos(b / c) * 180) / Math.PI)
              * (pointer.y > centerY ? 1 : -1);

    item.style.setProperty('--rotate', `${r}deg`);
  });
};

삼각함수로 포인터 방향의 각도를 계산한다:

+xrabcpointer (x, y)center (centerX, centerY)
a = pointer.y - centerY
b = pointer.x - centerX
c = √(a² + b²)
r = acos(b/c) × 180/π × ±1

r = acos(b / c) × 180 / π × (y방향 보정)

acos(b/c)는 x축과 빗변 사이의 각도(라디안)를 반환한다. 180/Math.PI로 도(degree)로 변환한다.

포인터 위치b (x)a (y)b/cacos(b/c)최종 각도
오른쪽 수평100010deg0deg
위 수직0-100090deg-90deg
왼쪽 수평-1000-1180deg180deg
아래 수직0100090deg90deg

pointer.y > centerY ? 1 : -1은 y축 방향을 보정한다. acos은 항상 0~180도를 반환하므로, 포인터가 위에 있으면 음수로 뒤집어야 전체 360도 범위를 표현할 수 있다.

3단계: 초기 상태 설정

// 마운트 시 그리드 중앙 라인을 기준으로 초기 각도 설정
if (items.length) {
  const middleIndex = Math.floor(items.length / 2);  // 81개 중 40번째
  const rect = items[middleIndex].getBoundingClientRect();
  onPointerMove({ x: rect.x, y: rect.y });
}

마운트 직후에 가상의 포인터를 그리드 중앙에 놓아 자연스러운 초기 상태를 만든다. 이 코드가 없으면 모든 라인이 baseAngle으로 일률적으로 기울어져 있어 어색하다.


무한 루프 — LogoLoop

💡 CSS가 아닌 rAF를 쓰는 이유

무한 스크롤 애니메이션은 CSS @keyframes로도 만들 수 있다:

@keyframes scroll {
  0% { transform: translateX(0); }
  100% { transform: translateX(-50%); }
}
.track { animation: scroll 10s linear infinite; }

하지만 이 방식에는 세 가지 한계가 있다:

  1. 호버 시 정지/감속이 부자연스럽다 — animation-play-state: paused는 즉시 멈추고, 감속 효과를 줄 수 없다
  2. 리사이즈 대응이 불가능하다 — 컨테이너 크기가 바뀌면 translateX(-50%)의 의미가 달라지는데, CSS만으로는 동적으로 복제 수를 조정할 수 없다
  3. 속도를 px/s 단위로 제어할 수 없다 — duration: 10s는 콘텐츠 길이에 상관없이 10초에 한 바퀴이므로, 콘텐츠가 길면 빨라지고 짧으면 느려진다

LogoLoop은 이 세 가지를 모두 해결하기 위해 requestAnimationFrame 기반 커스텀 루프를 사용한다.

LogoLoop — ResizeObserver + rAF 무한 스크롤

로고들이 무한히 흘러간다 — 속도와 방향을 바꿔보자.

  • React
  • Next.js
  • TypeScript
  • Tailwind
  • Vite
  • Node.js
  • React
  • Next.js
  • TypeScript
  • Tailwind
  • Vite
  • Node.js
120px/s

388줄의 가장 큰 컴포넌트다. 3개의 커스텀 훅으로 분리되어 있다.

1단계: useResizeObserver — 복제 수 동적 계산

const updateDimensions = useCallback(() => {
  const containerWidth = containerRef.current?.clientWidth ?? 0;
  const sequenceWidth = seqRef.current?.getBoundingClientRect()?.width ?? 0;

  if (sequenceWidth > 0) {
    setSeqWidth(Math.ceil(sequenceWidth));
    // 뷰포트를 채우기 위해 필요한 복제 수 + 여유분 2개
    const copiesNeeded = Math.ceil(containerWidth / sequenceWidth)
                       + ANIMATION_CONFIG.COPY_HEADROOM;
    setCopyCount(Math.max(ANIMATION_CONFIG.MIN_COPIES, copiesNeeded));
  }
}, []);

ResizeObserver가 컨테이너나 시퀀스의 크기 변경을 감지하면 updateDimensions를 호출한다.

컨테이너 폭시퀀스 폭필요 복제 수+ 여유분최종
800px400pxceil(800/400) = 22+2 = 44
1200px400pxceil(1200/400) = 33+2 = 55
300px400pxceil(300/400) = 11+2 = 33 (최소 2)

여유분 2개(COPY_HEADROOM)는 스크롤 중 빈 공간이 노출되지 않도록 한다. seqRef는 첫 번째 <ul>에만 연결되어 "한 세트"의 크기를 측정한다.

2단계: useAnimationLoop — 속도 보간 + modulo 래핑

const animate = (timestamp: number) => {
  const deltaTime = (timestamp - lastTimestampRef.current) / 1000;  // 초 단위

  // 지수 보간으로 속도를 부드럽게 변경
  const easingFactor = 1 - Math.exp(-deltaTime / ANIMATION_CONFIG.SMOOTH_TAU);
  velocityRef.current += (target - velocityRef.current) * easingFactor;

  // 오프셋 갱신 — modulo로 한 세트 범위 안에 유지
  let nextOffset = offsetRef.current + velocityRef.current * deltaTime;
  nextOffset = ((nextOffset % seqSize) + seqSize) % seqSize;
  offsetRef.current = nextOffset;

  track.style.transform = `translate3d(${-offsetRef.current}px, 0, 0)`;
  requestAnimationFrame(animate);
};

두 가지 핵심 기법이 있다:

지수 보간 (exponential smoothing):

velocityRef.current += (target - current) * (1 - e^(-dt/tau))

SMOOTH_TAU = 0.25일 때, 매 프레임마다 목표 속도의 약 63%씩 접근한다. 호버로 target이 0이 되면 현재 속도가 120 → 44 → 16 → 6 → 2 → ...처럼 감속한다. CSS animation-play-state: paused의 즉시 멈춤과 달리 관성 있는 감속이 된다.

modulo 래핑:

nextOffset = ((nextOffset % seqSize) + seqSize) % seqSize;

오프셋이 seqSize(한 세트의 폭)를 넘으면 0으로 되감긴다. 동일한 리스트가 여러 개 복제되어 있으므로, 오프셋이 0으로 점프해도 시각적으로 끊김이 없다. ((x % n) + n) % n은 음수 modulo를 양수로 변환하는 JavaScript 관용구다 — direction: 'right'일 때 음수 오프셋을 처리한다.

3단계: useImageLoader — 이미지 로드 대기

const useImageLoader = (seqRef, onLoad, dependencies) => {
  useEffect(() => {
    const images = seqRef.current?.querySelectorAll('img') ?? [];
    if (images.length === 0) {
      onLoad();  // 이미지 없으면 즉시 실행
      return;
    }
    let remainingImages = images.length;
    const handleImageLoad = () => {
      remainingImages -= 1;
      if (remainingImages === 0) onLoad();  // 모든 이미지 로드 완료
    };
    images.forEach(img => {
      if ((img as HTMLImageElement).complete) handleImageLoad();
      else img.addEventListener('load', handleImageLoad, { once: true });
    });
  }, dependencies);
};

이미지가 로드되기 전에 getBoundingClientRect()를 호출하면 크기가 0이다. useImageLoader가 모든 이미지 로드를 기다린 후 updateDimensions를 호출해서 정확한 크기로 복제 수를 계산한다.

4단계: 페이드 아웃 — CSS ::before/::after

.logoloop--fade::before {
  left: 0;
  background: linear-gradient(
    to right,
    var(--logoloop-fadeColor) 0%,
    rgba(0, 0, 0, 0) 100%
  );
}
.logoloop--fade::after {
  right: 0;
  background: linear-gradient(
    to left,
    var(--logoloop-fadeColor) 0%,
    rgba(0, 0, 0, 0) 100%
  );
}

양쪽 가장자리에 pointer-events: none + z-index: 10인 가상 요소로 페이드를 만든다. clamp(24px, 8%, 120px)으로 컨테이너 크기에 비례하되 최소 24px, 최대 120px의 페이드 폭을 유지한다.

참고: prefers-reduced-motion 접근성 대응
@media (prefers-reduced-motion: reduce) {
  .logoloop__track {
    transform: translate3d(0, 0, 0) !important;
  }
}

사용자가 시스템 설정에서 "모션 줄이기"를 켜면 트랙이 멈춘다. !important로 rAF가 설정하는 inline transform을 덮어쓴다. 접근성은 선택이 아닌 필수다.


CSS만으로 가능한 인터랙션의 범위

5개 컴포넌트를 살펴보면서 분명해진 것이 있다 — CSS의 한계는 "상태 전이"까지다.

기법가능한 것불가능한 것
CSS transitionA → B 상태 전환 보간다단계 시퀀싱
CSS @keyframes무한 반복 루프동적 속도 변경, 감속
CSS 커스텀 속성JS에서 값 전달프레임 단위 계산
:hover마우스 올림/뺌 감지근접 거리 감지, 이동 추적

GlareHover와 StarBorder는 CSS만으로 완성된다. Magnet과 MagnetLines는 좌표 계산에 JavaScript가 필요하지만, 렌더링은 CSS transform이 담당한다. LogoLoop은 rAF 기반 커스텀 루프가 필수적이지만, 페이드나 레이아웃은 CSS가 처리한다.

결론: CSS가 할 수 있는 것은 CSS에 맡기고, JavaScript는 CSS가 못하는 것만 한다. 이 원칙은 다음 편에서 GSAP이 등장해도 변하지 않는다.

다음 포스트에서는

CSS만으로는 부족한 순간 — GSAP으로 커서를 추적하고 트레일을 만든다. BlobCursor의 유체 시뮬레이션과 TargetCursor의 정밀한 위치 추적을 분석한다.

  • Pure CSS 호버 — GlareHover, StarBorder
  • 💡 CSS만으로 호버 효과를 만드는 두 가지 전략
  • GlareHover — CSS gradient 반사 효과
  • 1단계: hex → rgba 변환
  • 2단계: CSS 커스텀 속성 전달
  • 3단계: CSS가 하는 모든 일
  • 4단계: playOnce 모드
  • StarBorder — CSS @keyframes 테두리 빛
  • 1단계: 레이어 구조
  • 2단계: radial-gradient 빛
  • 3단계: @keyframes로 왕복
  • 마그넷 효과 — Magnet, MagnetLines
  • 💡 getBoundingClientRect + transform — 마우스 추적의 기본 패턴
  • Magnet — 마우스 방향으로 요소 끌어당기기
  • 1단계: 감지 영역 판별
  • 2단계: 이동 거리 계산
  • 3단계: transition으로 부드러운 이동
  • MagnetLines — CSS Grid + 동적 회전
  • 1단계: CSS Grid로 라인 배치
  • 2단계: 각도 계산 — 삼각함수
  • 3단계: 초기 상태 설정
  • 무한 루프 — LogoLoop
  • 💡 CSS가 아닌 rAF를 쓰는 이유
  • LogoLoop — ResizeObserver + rAF 무한 스크롤
  • 1단계: useResizeObserver — 복제 수 동적 계산
  • 2단계: useAnimationLoop — 속도 보간 + modulo 래핑
  • 3단계: useImageLoader — 이미지 로드 대기
  • 4단계: 페이드 아웃 — CSS ::before/::after
  • CSS만으로 가능한 인터랙션의 범위
  • 다음 포스트에서는
  • Pure CSS 호버 — GlareHover, StarBorder
  • 💡 CSS만으로 호버 효과를 만드는 두 가지 전략
  • GlareHover — CSS gradient 반사 효과
  • 1단계: hex → rgba 변환
  • 2단계: CSS 커스텀 속성 전달
  • 3단계: CSS가 하는 모든 일
  • 4단계: playOnce 모드
  • StarBorder — CSS @keyframes 테두리 빛
  • 1단계: 레이어 구조
  • 2단계: radial-gradient 빛
  • 3단계: @keyframes로 왕복
  • 마그넷 효과 — Magnet, MagnetLines
  • 💡 getBoundingClientRect + transform — 마우스 추적의 기본 패턴
  • Magnet — 마우스 방향으로 요소 끌어당기기
  • 1단계: 감지 영역 판별
  • 2단계: 이동 거리 계산
  • 3단계: transition으로 부드러운 이동
  • MagnetLines — CSS Grid + 동적 회전
  • 1단계: CSS Grid로 라인 배치
  • 2단계: 각도 계산 — 삼각함수
  • 3단계: 초기 상태 설정
  • 무한 루프 — LogoLoop
  • 💡 CSS가 아닌 rAF를 쓰는 이유
  • LogoLoop — ResizeObserver + rAF 무한 스크롤
  • 1단계: useResizeObserver — 복제 수 동적 계산
  • 2단계: useAnimationLoop — 속도 보간 + modulo 래핑
  • 3단계: useImageLoader — 이미지 로드 대기
  • 4단계: 페이드 아웃 — CSS ::before/::after
  • CSS만으로 가능한 인터랙션의 범위
  • 다음 포스트에서는
📚

react-bits 파헤치기

  1. ✅1. CSS만으로 글리치 텍스트 만들기
  2. ✅2. Motion: React에서 선언적으로 애니메이션하기
  3. ✅3. GSAP + ScrollTrigger: 타임라인으로 정밀하게 제어하기
  4. ✅4. Canvas 2D: 텍스트를 픽셀로 다루기
  5. ✅5. WebGL & Shader: GPU로 텍스트 렌더링하기
  6. ✅6. Variable Font & SVG: 브라우저 내장 기술의 가능성
  7. 🔍7. CSS로 만드는 호버 & 커서 이펙트
←Variable Font & SVG: 브라우저 내장 기술의 가능성

관련 포스트

이 포스트와 관련된 다른 글들을 확인해보세요

CSS로 만드는 호버 & 커서 이펙트

CSS custom properties와 마우스 좌표만으로 GlareHover, StarBorder, Magnet 효과를 구현한다. JavaScript 최소화 전략과 CSS의 실제 한계를 탐구한다.

2026년 2월 21일•13분
csshovercursor+2

Variable Font & SVG: 브라우저 내장 기술의 가능성

font-variation-settings로 마우스 근접도에 반응하는 텍스트, SVG textPath로 곡선 위를 흐르는 텍스트를 구현한다.

2026년 2월 23일•10분
variable-fontsvgcss+2

CSS만으로 글리치 텍스트 만들기

JavaScript 없이 CSS keyframes, pseudo-elements, custom properties만으로 글리치 효과를 구현한다. CSS 애니메이션의 가능성과 한계를 탐구한다.

2026년 2월 16일•5분
cssanimationkeyframes+2

Comments