Pure CSSMIT licensed

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.

Live Demo
Plain CSSTailwind v4
Try it

Build UI like this from scratch: Flexbox, Grid, animations & responsive CSS

ZTM · The CSS Bootcamp · 30‑day money-back guarantee · Affiliate link

10% off at checkout with code
Check ZTM Course

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;
  }
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Image Hover Effect from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: CSS Hover Reveal Text Over Image
Source: https://codefronts.com/motion/css-image-hover-effects/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.
## HTML
```html
<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>
```
## CSS
```css
.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.

Make it yours

  • 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.

Gotchas — read before shipping

  • 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
76+9+103+76+

Floor set by backdrop-filter as this demo is written. The core technique itself goes back further — Chrome 49+.

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

Techniques used in this demo

Search CodeFronts

Loading…