호버 리빌

Hover Reveal

평소엔 숨어 있다가 커서를 올리면 추가 정보나 액션이 슬라이드/페이드로 드러나는 패턴. 화면을 깔끔하게 유지하면서 세부 조작을 담아둡니다.

다른 이름: Reveal on hover
···
html
<div class="hrv" id="hv">
  <div class="bgimg"></div>
  <div class="overlay" id="ov"><strong>More info</strong><small>Revealed on hover</small></div>
</div>
css
.fx-dot{position:absolute;left:0;top:0;width:12px;height:12px;margin:-6px 0 0 -6px;border-radius:50%;
  background:var(--accent);box-shadow:0 0 0 5px color-mix(in srgb,var(--accent) 18%,transparent),0 2px 6px rgba(0,0,0,.3);
  pointer-events:none;z-index:9999;transition:opacity .25s}
.hrv{position:relative;width:min(78%,260px);aspect-ratio:16/10;border-radius:14px;overflow:hidden;box-shadow:0 14px 30px rgba(0,0,0,.2)}
.bgimg{position:absolute;inset:0;background:linear-gradient(135deg,var(--accent-2),var(--accent))}
.overlay{position:absolute;inset:auto 0 0 0;padding:14px 16px;display:grid;gap:2px;color:#fff;
  background:linear-gradient(to top,rgba(0,0,0,.65),transparent);
  transform:translateY(100%);transition:transform .28s ease}
.overlay.show{transform:translateY(0)}
.overlay strong{font-size:clamp(12px,3.6vmin,15px)}
.overlay small{opacity:.85;font-size:clamp(10px,2.8vmin,12px)}
js
const fxDot=document.createElement('div');fxDot.className='fx-dot';document.body.appendChild(fxDot);
let fxAuto=true,fxX=innerWidth/2,fxY=innerHeight/2;
function fxHide(){fxDot.style.opacity='0';}
addEventListener('pointerdown',()=>{fxAuto=false;fxHide();},{capture:true});
addEventListener('pointermove',e=>{fxAuto=false;fxHide();fxX=e.clientX;fxY=e.clientY;},{capture:true});
function fxWalk(x,y,ms){
  return new Promise(res=>{
    const sx=fxX,sy=fxY,start=performance.now();
    function step(now){
      if(!fxAuto)return res();
      const p=Math.min(1,(now-start)/ms),e=1-Math.pow(1-p,3);
      fxX=sx+(x-sx)*e;fxY=sy+(y-sy)*e;
      fxDot.style.transform='translate('+fxX+'px,'+fxY+'px)';
      if(p<1)requestAnimationFrame(step);else res();
    }
    requestAnimationFrame(step);
  });
}
function fxWait(ms){
  return new Promise(res=>{
    const start=performance.now();
    function step(now){ if(!fxAuto)return res(); if(now-start<ms)requestAnimationFrame(step);else res(); }
    requestAnimationFrame(step);
  });
}

const hv=document.getElementById('hv');
const ov=document.getElementById('ov');
function render(x,y){
  const r=hv.getBoundingClientRect();
  ov.classList.toggle('show', x>r.left&&x<r.right&&y>r.top&&y<r.bottom);
}
let t=0;
(function loop(){
  if(fxAuto){
    t+=0.015;
    const r=hv.getBoundingClientRect();
    fxX=r.left+r.width/2+Math.sin(t)*r.width*0.42;
    fxY=r.top+r.height/2+Math.sin(t*0.6)*10;
    fxDot.style.transform='translate('+fxX+'px,'+fxY+'px)';
  }
  render(fxX,fxY);
  requestAnimationFrame(loop);
})();

평소엔 `transform: translateY(100%)`로 오버레이를 요소 아래로 숨겨두고, hover 시(또는 커서가 카드 영역 안에 있을 때) `translateY(0)`으로 슬라이드시킨다. `opacity`만 쓰는 것보다 위치까지 함께 움직이면 "미끄러져 나온다"는 방향성이 생긴다.

카드 전체가 아니라 특정 자식 요소(버튼 등)에만 hover가 걸리지 않도록, 이벤트는 최상위 카드에 하나만 두고 내부에서 상태를 전파하는 편이 관리하기 쉽다. transition을 0.2~0.3초로 주면 급하지 않게 느껴진다.

터치 기기에는 hover가 없으므로, 탭으로 토글하거나 처음부터 일부 정보를 노출해두는 대체 경로가 필요하다.

언제 쓰나

이미지 카드 위의 캡션·액션 버튼, 테이블 행의 편집 아이콘처럼 "평소엔 안 보여도 되는" 보조 정보에 씁니다.