About this snippet

Hover a card and the colored overlay enters from the side your cursor came in on. Move away and it leaves toward the side you exited.

The direction comes from the cursor position measured against the card center. Math.atan2 turns that offset into an angle, which is rounded into four quadrants for top, right, bottom and left. The width to height ratio is factored in first, so the quadrants stay correct on cards that are not square.

Before the slide runs, the overlay is snapped to its starting edge with the transition switched off, then a forced reflow through offsetWidth commits that position. Without the reflow the browser batches both changes into one paint and the overlay simply slides from wherever it already was.

Size
3.7 KB
Uses
HTML, CSS, JavaScript
Loads
placehold.co
Views
217
<div class="direction-grid">
  
  <div class="dir-card" id="card-1">
    <img src="https://placehold.co/400x400/1e293b/ffffff?text=Project+Alpha" alt="Project" class="card-image">
    <div class="dir-overlay">
      <h3>Project Alpha</h3>
      <p>Seamlessly integrated robust APIs.</p>
    </div>
  </div>
  
  <div class="dir-card" id="card-2">
    <img src="https://placehold.co/400x400/334155/ffffff?text=Project+Beta" alt="Project" class="card-image">
    <div class="dir-overlay">
      <h3>Project Beta</h3>
      <p>Award winning spatial UI design.</p>
    </div>
  </div>
  
</div>
*, *::before, *::after {
  box-sizing: border-box;
}

body {
  font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
  margin: 0;
  background: #f8fafc;
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  padding: 20px;
}

.direction-grid {
  display: flex;
  gap: 30px;
  flex-wrap: wrap;
  justify-content: center;
}

.dir-card {
  position: relative;
  width: 300px;
  height: 300px;
  border-radius: 20px;
  overflow: hidden;
  box-shadow: 0 15px 30px rgba(0, 0, 0, 0.1);
  cursor: pointer;
}

.card-image {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.dir-overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(59, 130, 246, 0.9);
  backdrop-filter: blur(8px);
  color: #ffffff;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  text-align: center;
  padding: 20px;
  /* Start hidden securely */
  transform: translate(-100%, 0);
  transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}

.dir-overlay h3 {
  margin: 0 0 10px 0;
  font-size: 1.75rem;
  letter-spacing: 1px;
}

.dir-overlay p {
  margin: 0;
  font-size: 1.1rem;
  opacity: 0.9;
}
const cards = document.querySelectorAll(".dir-card");

function getDirection(e, item) {
  const rect = item.getBoundingClientRect();
  const w = rect.width;
  const h = rect.height;
  
  /* Calculate the exact mouse position relative to the center of the element safely */
  const x = (e.clientX - rect.left - (w / 2)) * (w > h ? (h / w) : 1);
  const y = (e.clientY - rect.top - (h / 2)) * (h > w ? (w / h) : 1);
  
  /* Math.atan2 returns the angle, we map it cleanly to 4 quadrants (0: Top, 1: Right, 2: Bottom, 3: Left) */
  const direction = Math.round((Math.atan2(y, x) * (180 / Math.PI) + 180) / 90 + 3) % 4;
  return direction;
}

cards.forEach(function(card) {
  const overlay = card.querySelector(".dir-overlay");
  
  card.addEventListener("mouseenter", function(e) {
    const dir = getDirection(e, card);
    
    /* Pre-position the overlay instantly without transition */
    overlay.style.transition = "none";
    
    if (dir === 0) overlay.style.transform = "translate(0, -100%)";
    else if (dir === 1) overlay.style.transform = "translate(100%, 0)";
    else if (dir === 2) overlay.style.transform = "translate(0, 100%)";
    else overlay.style.transform = "translate(-100%, 0)";
    
    /* Force a reflow safely so the CSS engine registers the new position */
    void overlay.offsetWidth;
    
    /* Restore transition and slide into the center */
    overlay.style.transition = "transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1)";
    overlay.style.transform = "translate(0, 0)";
  });
  
  card.addEventListener("mouseleave", function(e) {
    const dir = getDirection(e, card);
    
    /* Slide out smoothly toward the exit direction */
    if (dir === 0) overlay.style.transform = "translate(0, -100%)";
    else if (dir === 1) overlay.style.transform = "translate(100%, 0)";
    else if (dir === 2) overlay.style.transform = "translate(0, 100%)";
    else overlay.style.transform = "translate(-100%, 0)";
  });
});
Esc
↑↓ moveEnter select

Start from a template

Open an .html file, or HTML, CSS, JS, Markdown, Less or TypeScript files. You can also drop them here. Styles and scripts go into their panes.

Share

Source

Keyboard shortcuts

Save