All components

Media Monks Hover

JavaScript animation component · Published 2026-07-27 · by vanguardia.dev

Open live demo ↗ Raw prompt (.md)

What it does

A dark film-studio work-list where hovering each project flips the whole panel to that project's pastel tint and lifts every row's opacity via CSS transitions, while a tilted strip of film-still previews behind the text repositions (top/left) and swaps its active card to reveal the matching project, plus an arrow icon that slides in from the left edge of a pill button on the hovered row.

How it's built

Categoryhover
Techvanilla JS
Complexitypage
Performance costlight
Mobile-safedesktop-first

hover css-transitions vanilla-js work-list image-reveal portfolio phosphor-icons editorial interactive

Rebuild it with AI

To reproduce this animation in your own project, copy the prompt below into Claude Code, Cursor or any AI coding agent. The prompt is validated — it describes the exact structure, timing and easing, so the agent rebuilds the effect faithfully and you can then adapt colors, copy and layout to your design.

The full prompt

Work-List Hover — Pastel Panel Flip + Tilted Preview Reveal

Goal

Build a full-viewport, dark "just went live" work list: an intro line, three big project rows separated by hairlines, and a pill CTA. The star effect is a whole-panel hover flip: hovering any row (a) flips the entire #141414 panel to that row's pastel background and inverts all the text/CTA from white to dark navy, (b) slides a tilted diagonal strip of three half-transparent GIF preview cards (sitting behind the text) to a new top/left position so the card matching the hovered project glides center-stage and rotates a touch further, and (c) reveals a round arrow-pill on the hovered row where the arrow does a conveyor swap — the resting arrow exits right while a fresh arrow slides in from the left. Everything is CSS transition driven; JS only toggles a class and writes two inline overlay offsets.

Tech

Vanilla HTML/CSS/JS, shipped as index.html + styles.css + an ES-module script.js (<script type="module" src="./script.js">). No GSAP, no npm dependencies, no smooth scroll. All motion is pure CSS transition; JS only adds/removes classes and sets overlay.style.top / overlay.style.left. Arrow glyphs use the Phosphor Icons web font via CDN in <head> (<script src="https://unpkg.com/@phosphor-icons/web"></script>, then <i class="ph-bold ph-arrow-right">) — any right-arrow icon font or inline SVG is an acceptable substitute. Must run in a fresh Vite project with zero installs.

Layout / HTML

Class/ID names are load-bearing — the CSS and JS query them.

<div class="container">
  <div class="work">
    <div class="overlay">
      <div class="prev" id="prev-1"><img src="prev-1.gif" alt="" /></div>
      <div class="prev" id="prev-2"><img src="prev-2.gif" alt="" /></div>
      <div class="prev" id="prev-3"><img src="prev-3.gif" alt="" /></div>
    </div>

    <p>Just went live with:</p>

    <div class="work-item" id="w-1">
      <div class="work-item-name"><h1>Aster</h1></div>
      <div class="work-item-icon">
        <div class="icon-holder i-1"><i class="ph-bold ph-arrow-right"></i></div>
        <div class="icon-holder i-2"><i class="ph-bold ph-arrow-right"></i></div>
      </div>
    </div>

    <div class="work-item" id="w-2">
      <div class="work-item-name"><h1>Launching Nova Into The World</h1></div>
      <div class="work-item-icon">
        <div class="icon-holder i-1"><i class="ph-bold ph-arrow-right"></i></div>
        <div class="icon-holder i-2"><i class="ph-bold ph-arrow-right"></i></div>
      </div>
    </div>

    <div class="work-item" id="w-3">
      <div class="work-item-name"><h1>Standby x Northwind</h1></div>
      <div class="work-item-icon">
        <div class="icon-holder i-1"><i class="ph-bold ph-arrow-right"></i></div>
        <div class="icon-holder i-2"><i class="ph-bold ph-arrow-right"></i></div>
      </div>
    </div>

    <div class="cta"><button>View all work</button></div>
  </div>
</div>

Key points:

  • Exactly three .work-item rows with ids #w-1, #w-2, #w-3. The project titles are arbitrary placeholder names — pick one short single word (row 1), one long title that wraps to two lines at the big type size (row 2), and one medium two-part title (row 3). No real brand or client names.
  • Each row holds a .work-item-name > h1 on the left and a .work-item-icon pill on the right, gapped apart.
  • Inside every .work-item-icon there are two .icon-holders (.i-1 then .i-2), each containing one identical right-arrow glyph. The two-arrow duplication is what enables the conveyor swap.
  • .overlay is the first child of .work and holds the three .prev GIF cards; it lives behind the text (z-index: 0).
  • The intro <p> reads "Just went live with:"; the CTA is a single pill <button> reading "View all work".

Styling

  • Global reset: * { margin:0; padding:0; box-sizing:border-box; }. img { width:100%; height:100%; object-fit:cover; }.
  • Font: a clean neutral grotesque sans for everything (original uses a proprietary "Helvetica Now" variable face — Helvetica / Arial / Inter reads right).
  • .work: position:relative; width:100vw; height:100vh; background:#141414; padding:6em 4em; overflow:hidden; transition: background-color 0.5s ease; — this is the panel that flips color. overflow:hidden clips the oversized tilted preview strip.
  • .overlay: position:absolute; top:0%; left:13.25%; width:100%; height:100%; pointer-events:none; z-index:0; display:flex; justify-content:center; align-items:center; gap:10em; opacity:0.5; transition:1s; — the previews sit half-transparent behind the text, and the whole strip tweens its top/left over 1s.
  • .prev: position:relative; width:500px; height:600px; background:gray; transform:rotate(10deg) scale(1); transition:transform 0.5s ease; — large portrait (~5:6) cards, all tilted 10°.
  • .prev.active: transform:rotate(12.5deg) scale(1); — the active card rotates a touch further (10°→12.5° over 0.5s).
  • Diagonal stagger via per-card top: #prev-1 { top:-500px; }, #prev-2 { top:0; }, #prev-3 { top:500px; } — inside the centered flex row this makes a descending diagonal staircase of three cards.
  • p (intro line): position:relative; color:#fff; font-size:30px; margin-bottom:1em; z-index:2; transition:0.5s;.
  • h1 (titles): color:#fff; opacity:0.5; font-size:80px; font-weight:500; letter-spacing:-0.025em; transition:0.5s; — dim white by default.
  • .work-item: position:relative; width:100%; padding:2em 0; display:flex; align-items:center; gap:2em; border-bottom:1px solid rgba(255,255,255,0.5); cursor:pointer; z-index:2; transition:0.5s;. The last row has no divider: .work-item#w-3 { border-bottom:none; }.
  • .work-item-icon: position:relative; width:70px; height:70px; border-radius:100%; background:#0b1925; color:#fff; overflow:hidden; left:-20px; opacity:0; transition:0.2s; — a dark-navy round pill, hidden by default and nudged 20px left. overflow:hidden clips the sliding arrows.
  • .icon-holder: position:absolute; width:70px; height:70px; border-radius:100%; display:flex; justify-content:center; align-items:center; transition:0.5s;.
  • .i-1 { transition-delay:0.2s; left:-70px; } — parked one pill-width off to the left (out of the clipped pill).
  • .i-2 { left:0px; } — the visible/centered arrow at rest.
  • button: position:relative; background:#fff; color:#0b1925; border:none; padding:2em 4em; margin:2em 0; font-size:15px; border-radius:100px; font-weight:500; z-index:2; transition:0.5s; — a white pill, dark text.

Three pastel background classes (added to .work on hover — note the class *names* are arbitrary and do NOT match the actual hues; use the hex values):

.bg-color-red   { background-color: rgb(164, 250, 249); }  /* pale aqua / cyan */
.bg-color-blue  { background-color: rgb(246, 250, 164); }  /* pale yellow      */
.bg-color-green { background-color: rgb(250, 164, 164); }  /* pale pink/salmon */

Hovered-panel inversions (all gated behind a .hovered class on .work, so they tween together with the background over their 0.5s transitions):

.work.hovered button        { background:#0b1925; color:#fff; }  /* pill inverts */
.work.hovered h1            { color:#0b1925; opacity:1; }        /* titles go dark & full-opacity */
.work.hovered p             { color:#0b1925; }
.work.hovered .work-item    { border-bottom:1px solid #0b1925; } /* dividers go dark */
.work.hovered .work-item#w-3{ border-bottom:none; }

Per-row arrow pill (native CSS :hover on the individual .work-item):

.work-item:hover .work-item-icon   { opacity:1; left:0px; }   /* pill fades in, slides right 20px */
.work-item:hover .icon-holder.i-1  { left:0; }                /* fresh arrow slides in from left  */
.work-item:hover .icon-holder.i-2  { left:70px; }             /* resting arrow exits to the right  */

The effect (exhaustive — this replaces the GSAP section)

There is no GSAP and no JS tweening loop. Two CSS transition engines plus a tiny vanilla-JS controller produce the whole thing. Durations/eases to reproduce exactly:

  • Panel background: background-color 0.5s ease on .work.
  • Text/CTA/divider inversions: 0.5s (default ease) on p, h1, .work-item, button — they retint the moment .hovered is toggled.
  • Preview strip travel: transition: 1s on .overlay (drives top/left).
  • Active card extra tilt: transform 0.5s ease on .prev (10°→12.5°).
  • Arrow-pill reveal: 0.2s on .work-item-icon (opacity 0→1, left −20px→0).
  • Arrow conveyor: 0.5s on each .icon-holder, with .i-1 delayed 0.2s so the swap reads as sequential (out-going arrow leaves, then incoming arrow lands).
JS controller (runs inside DOMContentLoaded)
const workItems   = document.querySelectorAll(".work-item");
const work        = document.querySelector(".work");
const overlay     = document.querySelector(".overlay");
const prevElements= document.querySelectorAll(".prev");

// Initial state:
overlay.style.top  = "0%";
overlay.style.left = "13.25%";
document.querySelector("#prev-2").classList.add("active");   // middle card active at rest

function removeActiveClass() {
  prevElements.forEach((prev) => prev.classList.remove("active"));
}

For each .work-item (with its index 0/1/2), attach two listeners:

mouseover

  1. removeActiveClass(), then add active to #prev-${index+1} (so the hovered row's matching card gains the 12.5° tilt).
  2. Add hovered to .work, then set the panel's full class string and the overlay offsets per index (the class assignment work.className = "work bg-color-… hovered" both applies the pastel bg and keeps hovered):
  3. index 0 (row 1): overlay.style.top = "50%"; overlay.style.left = "50%"; and work.className = "work bg-color-red hovered";pale aqua panel; the diagonal strip slides down-right so prev-1 lands center.
  4. index 1 (row 2): overlay.style.top = "0%"; overlay.style.left = "13.25%"; and work.className = "work bg-color-blue hovered";pale yellow panel; overlay stays at its rest position with prev-2 center.
  5. index 2 (row 3): overlay.style.top = "-50%"; overlay.style.left = "-23.5%"; and work.className = "work bg-color-green hovered";pale pink panel; the strip slides up-left so prev-3 lands center.

mouseout → reset everything: remove hovered, work.className = "work" (back to #141414), overlay.style.top = "0%"; overlay.style.left = "13.25%";, removeActiveClass(), and re-add active to #prev-2.

Because the overlay is a 100%×100% absolutely-positioned box holding a diagonal strip of three oversized cards, shifting its top/left by these percentages glides a *different* card behind the centered text column, and the 1s transition makes that a smooth slide. The pastel flip (0.5s) and the arrow pill (native :hover) fire simultaneously since the cursor triggers both the JS mouseover and the CSS :hover at once.

No ScrollTrigger, no timelines, no stagger arrays, no rAF loop, no scroll behavior — the only triggers are per-row mouseover / mouseout (JS) and per-row :hover (CSS), plus the one on-load initial state.

Assets / images

  • 3 looping GIF preview cards, portrait ~5:6 (rendered at 500×600), shown object-fit: cover at opacity: 0.5 behind the text. Each should be dark, cinematic, editorial motion footage — moody, low-key lit, low-contrast — so text stays legible over them. One per project row; the middle one (prev-2) is the default/at-rest card. Suggested distinct subjects: an overhead aerial of figures on a marked ground; a moody dusk tracking shot along a long wall; a low-key interior looking outward past objects toward a colored glow. No brands, logos, or recognizable client work. Static JPGs work as a fallback if GIFs aren't available.

Behavior notes

  • Desktop-only effect (large 500×600 previews). Responsive at max-width: 900px: .work { padding:4em 1em; }, .work-item { gap:1em; }, p { font-size:16px; }, h1 { font-size:30px; }, .work-item-icon { transform:scale(0.75); }, button { padding:1em 2em; }.
  • No reduced-motion handling in the original; transitions are short (≤1s) and gentle.
  • Only one row is ever "hovered" at a time; leaving a row instantly returns the panel to dark, prev-2 active, overlay at rest.
  • Dark UI at rest: #141414 ground, white 30px intro line, 80px dim-white titles (opacity 0.5), 50%-white hairline dividers, white CTA pill with #0b1925 text.