20 CSS Image Hover Effects 02 / 20

CSS Hover Reveal Text Over Image

Portfolio-grid technique where a title and description fade and slide in over an image on mouseover via layered opacity and translateY transitions.

Pure CSS MIT licensed
Live Demo Open in tab
Open in playground

The code

<div class="ih-02">
  <div class="ih-02__grid">
    <div class="ih-02__card">
      <div class="ih-02__img ih-02__img--1">
        <span class="ih-02__emoji">🌌</span>
      </div>
      <span class="ih-02__badge">Portfolio</span>
      <div class="ih-02__overlay">
        <p class="ih-02__tag">Astrophotography</p>
        <p class="ih-02__name">Nebula Series Vol. 3</p>
        <p class="ih-02__sub">Shot over 14 nights in the Chilean Atacama desert</p>
      </div>
    </div>
    <div class="ih-02__card">
      <div class="ih-02__img ih-02__img--2">
        <span class="ih-02__emoji">🏙️</span>
      </div>
      <span class="ih-02__badge">Architecture</span>
      <div class="ih-02__overlay">
        <p class="ih-02__tag">Urban Design</p>
        <p class="ih-02__name">Midnight Cityscapes</p>
        <p class="ih-02__sub">Long exposure study of downtown light trails</p>
      </div>
    </div>
    <div class="ih-02__card">
      <div class="ih-02__img ih-02__img--3">
        <span class="ih-02__emoji">🔮</span>
      </div>
      <span class="ih-02__badge">Abstract</span>
      <div class="ih-02__overlay">
        <p class="ih-02__tag">Digital Art</p>
        <p class="ih-02__name">Prism Light Study</p>
        <p class="ih-02__sub">Generative colour diffraction experiments</p>
      </div>
    </div>
  </div>
</div>
.ih-02, .ih-02 *, .ih-02 *::before, .ih-02 *::after {
  margin: 0; padding: 0; box-sizing: border-box;
}
.ih-02 ::selection { background: #06b6d4; color: #000; }

.ih-02 {
  --accent: #06b6d4;
  --accent2: #a78bfa;
  --bg: #09090f;
  --text: #f1f5f9;
  --muted: #94a3b8;
  --duration: 0.4s;
  --ease: cubic-bezier(0.16, 1, 0.3, 1);
  font-family: 'Inter', system-ui, sans-serif;
  background: var(--bg);
  padding: 40px 24px;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

.ih-02__grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
  max-width: 780px;
  width: 100%;
}

.ih-02__card {
  position: relative;
  border-radius: 12px;
  overflow: hidden;
  aspect-ratio: 3/4;
  cursor: pointer;
}

/* Gradient placeholder images */
.ih-02__img {
  width: 100%; height: 100%;
  transition: transform var(--duration) var(--ease);
  display: flex; align-items: center; justify-content: center;
}
.ih-02__img--1 { background: linear-gradient(160deg,#0f0c29,#302b63,#24243e); }
.ih-02__img--2 { background: linear-gradient(160deg,#0f2027,#203a43,#2c5364); }
.ih-02__img--3 { background: linear-gradient(160deg,#1a0533,#4a044e,#2d1b69); }

.ih-02__card:hover .ih-02__img {
  transform: scale(1.08);
}

.ih-02__emoji {
  font-size: 56px;
  opacity: 0.35;
  transition: opacity var(--duration) var(--ease);
}
.ih-02__card:hover .ih-02__emoji {
  opacity: 0.1;
}

/* The overlay fades in and text slides up */
.ih-02__overlay {
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0.3) 50%, transparent 100%);
  opacity: 0;
  transition: opacity var(--duration) var(--ease);
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding: 20px 16px;
}

.ih-02__card:hover .ih-02__overlay {
  opacity: 1;
}

.ih-02__tag {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 6px;
  transform: translateY(12px);
  transition: transform var(--duration) var(--ease);
}

.ih-02__card:hover .ih-02__tag { transform: translateY(0); }

.ih-02__name {
  font-size: 15px;
  font-weight: 700;
  color: var(--text);
  line-height: 1.3;
  transform: translateY(16px);
  transition: transform calc(var(--duration) * 1.1) var(--ease);
}

.ih-02__card:hover .ih-02__name { transform: translateY(0); }

.ih-02__sub {
  font-size: 12px;
  color: var(--muted);
  margin-top: 4px;
  transform: translateY(20px);
  opacity: 0;
  transition: transform calc(var(--duration) * 1.2) var(--ease),
              opacity calc(var(--duration) * 1.2) var(--ease);
}

.ih-02__card:hover .ih-02__sub {
  transform: translateY(0);
  opacity: 1;
}

/* Static label when not hovered (accessibility) */
.ih-02__badge {
  position: absolute;
  top: 12px; left: 12px;
  background: rgba(0,0,0,0.5);
  backdrop-filter: blur(8px);
  border: 1px solid rgba(255,255,255,0.1);
  border-radius: 6px;
  padding: 4px 8px;
  font-size: 10px;
  color: var(--muted);
  font-weight: 600;
  letter-spacing: 0.05em;
}

@media (prefers-reduced-motion: reduce) {
  .ih-02__img,
  .ih-02__overlay,
  .ih-02__tag,
  .ih-02__name,
  .ih-02__sub,
  .ih-02__emoji {
    transition: none;
  }
  .ih-02__overlay { opacity: 1; }
  .ih-02__tag, .ih-02__name, .ih-02__sub { transform: none; opacity: 1; }
}

How this works

A semi-transparent gradient overlay div is stacked above the image using position: absolute; inset: 0. Its opacity transitions from 0 to 1 on :hover. Child text elements start with transform: translateY(12–20px) and opacity: 0, then transition to their natural position, creating a staggered "wipe up" entrance without JavaScript.

The stagger is achieved by giving each text element a slightly longer transition-duration multiplied by a factor (×1.1, ×1.2), so deeper elements arrive later naturally. This is entirely declarative CSS — no animation-delay or keyframes needed.

Customize

  • Adjust stagger depth by changing the duration multipliers: calc(var(--duration) * 1.3) for a more dramatic cascade effect.
  • For a horizontal slide instead of vertical, swap translateY(16px) to translateX(-16px) on the text elements.
  • Replace the gradient overlay with a solid rgba(0,0,0,0.6) for high-contrast editorial cards where readability is critical.
  • Add a persistent always-visible title strip at the bottom outside the overlay so screen readers and keyboard users see content without hover.
  • Chain with a slight scale(1.04) on the image itself to add depth — keep the image in its own stacking context with overflow: hidden on the card.

Watch out for

  • Avoid animating display or visibility for the overlay — these properties cannot be interpolated. Use opacity + pointer-events: none instead.
  • The translateY start values must match the overlay height padding: if the overlay grows, increase the starting offset so text does not appear mid-frame.
  • In Firefox, mix-blend-mode on overlay children can interact unexpectedly with the gradient — test and flatten to a solid if colour shifts appear.

Browser support

ChromeSafariFirefoxEdge
49+ 9+ 41+ 49+

Uses only opacity, transform, and CSS transitions — universally supported in all modern engines.

Search CodeFronts

Loading…