23 / 30
All 30 in 30 Tailwind CSS Hover Effects
- 01Tailwind Underline Draw Hover Effect
- 02Tailwind Text Glitch Hover Effect
- 03Tailwind Letter Spacing Expand Hover Effect
- 04Tailwind Gradient Text Reveal Hover Effect
- 05Tailwind Split Text Hover Effect
- 06Tailwind Neon Glow Text Hover Effect
- 07Tailwind Magnetic Liquid Button Hover Effect
- 08Tailwind Border Draw Hover Effect
- 09Tailwind Shimmer Shine Button Hover Effect
- 10Tailwind Fill Wipe Button Hover Effect
- 11Tailwind 3D Press Button Hover Effect
- 12Tailwind Ripple Button Hover Effect
- 13Tailwind 3D Tilt Card Hover Effect
- 14Tailwind Card Flip Hover Effect
- 15Tailwind Glassmorphism Card Hover Effect
- 16Tailwind Spotlight Card Hover Effect
- 17Tailwind Slide Reveal Card Hover Effect
- 18Tailwind Stack Lift Card Hover Effect
- 19Tailwind Zoom Pan Image Hover Effect
- 20Tailwind Color Channel Split Hover Effect
- 21Tailwind Duotone Image Hover Effect
- 22Tailwind Curtain Reveal Image Hover Effect
- 23Tailwind Ken Burns Image Hover Effect
- 24Tailwind Image Tilt Depth Hover Effect
- 25Tailwind Underline Slide Nav Hover Effect
- 26Tailwind Highlight Fill Nav Hover Effect
- 27Tailwind Strikethrough Hover Link Effect
- 28Tailwind Inline Word Swap Hover Effect
- 29Tailwind Dot Trail Cursor Hover Effect
- 30Tailwind Magnetic Cursor Pull Hover Effect
Tailwind Ken Burns Image Hover Effect
Six documentary-style slow zoom and pan presets that run only while hovered. Each animation is paused at rest and resumed on hover — animation-play-state has no utility, so it rides as an arbitrary property, which is exactly the right call.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="thv-23 w-full min-h-svh bg-thv23-bg font-thv23-sans px-8 py-10">
<p class="text-center text-[0.78rem] tracking-[0.12em] uppercase text-thv23-dim mb-8">Ken Burns Hover — 6 Presets</p>
<div class="grid grid-cols-[repeat(auto-fit,minmax(230px,1fr))] gap-5 max-w-[860px] mx-auto">
<figure class="group relative m-0 aspect-[3/2] overflow-hidden rounded-xl shadow-[0_4px_20px_rgba(0,0,0,.5)] cursor-pointer">
<img src="https://picsum.photos/seed/kb1/400/260" alt="Coastline at dawn" class="block w-full h-full object-cover animate-thv23-zoom-in [animation-play-state:paused] group-hover:[animation-play-state:running] motion-reduce:[animation-play-state:paused]!">
<figcaption class="absolute bottom-2.5 left-2.5 px-2 py-1 rounded bg-black/65 backdrop-blur-[4px] text-[0.7rem] tracking-[0.08em] text-white pointer-events-none">Zoom In</figcaption>
</figure>
<figure class="group relative m-0 aspect-[3/2] overflow-hidden rounded-xl shadow-[0_4px_20px_rgba(0,0,0,.5)] cursor-pointer">
<img src="https://picsum.photos/seed/kb2/400/260" alt="Mountain ridge" class="block w-full h-full object-cover animate-thv23-zoom-out [animation-play-state:paused] group-hover:[animation-play-state:running] motion-reduce:[animation-play-state:paused]!">
<figcaption class="absolute bottom-2.5 left-2.5 px-2 py-1 rounded bg-black/65 backdrop-blur-[4px] text-[0.7rem] tracking-[0.08em] text-white pointer-events-none">Zoom Out</figcaption>
</figure>
<figure class="group relative m-0 aspect-[3/2] overflow-hidden rounded-xl shadow-[0_4px_20px_rgba(0,0,0,.5)] cursor-pointer">
<img src="https://picsum.photos/seed/kb3/400/260" alt="City skyline" class="block w-full h-full object-cover animate-thv23-pan-right [animation-play-state:paused] group-hover:[animation-play-state:running] motion-reduce:[animation-play-state:paused]!">
<figcaption class="absolute bottom-2.5 left-2.5 px-2 py-1 rounded bg-black/65 backdrop-blur-[4px] text-[0.7rem] tracking-[0.08em] text-white pointer-events-none">Pan Right</figcaption>
</figure>
<figure class="group relative m-0 aspect-[3/2] overflow-hidden rounded-xl shadow-[0_4px_20px_rgba(0,0,0,.5)] cursor-pointer">
<img src="https://picsum.photos/seed/kb4/400/260" alt="Forest path" class="block w-full h-full object-cover animate-thv23-pan-left [animation-play-state:paused] group-hover:[animation-play-state:running] motion-reduce:[animation-play-state:paused]!">
<figcaption class="absolute bottom-2.5 left-2.5 px-2 py-1 rounded bg-black/65 backdrop-blur-[4px] text-[0.7rem] tracking-[0.08em] text-white pointer-events-none">Pan Left</figcaption>
</figure>
<figure class="group relative m-0 aspect-[3/2] overflow-hidden rounded-xl shadow-[0_4px_20px_rgba(0,0,0,.5)] cursor-pointer">
<img src="https://picsum.photos/seed/kb5/400/260" alt="Desert dunes" class="block w-full h-full object-cover animate-thv23-drift-up [animation-play-state:paused] group-hover:[animation-play-state:running] motion-reduce:[animation-play-state:paused]!">
<figcaption class="absolute bottom-2.5 left-2.5 px-2 py-1 rounded bg-black/65 backdrop-blur-[4px] text-[0.7rem] tracking-[0.08em] text-white pointer-events-none">Drift Up</figcaption>
</figure>
<figure class="group relative m-0 aspect-[3/2] overflow-hidden rounded-xl shadow-[0_4px_20px_rgba(0,0,0,.5)] cursor-pointer">
<img src="https://picsum.photos/seed/kb6/400/260" alt="Harbour at night" class="block w-full h-full object-cover animate-thv23-diagonal [animation-play-state:paused] group-hover:[animation-play-state:running] motion-reduce:[animation-play-state:paused]!">
<figcaption class="absolute bottom-2.5 left-2.5 px-2 py-1 rounded bg-black/65 backdrop-blur-[4px] text-[0.7rem] tracking-[0.08em] text-white pointer-events-none">Diagonal</figcaption>
</figure>
</div>
</section><section class="thv-23 w-full min-h-svh bg-thv23-bg font-thv23-sans px-8 py-10">
<p class="text-center text-[0.78rem] tracking-[0.12em] uppercase text-thv23-dim mb-8">Ken Burns Hover — 6 Presets</p>
<div class="grid grid-cols-[repeat(auto-fit,minmax(230px,1fr))] gap-5 max-w-[860px] mx-auto">
<figure class="group relative m-0 aspect-[3/2] overflow-hidden rounded-xl shadow-[0_4px_20px_rgba(0,0,0,.5)] cursor-pointer">
<img src="https://picsum.photos/seed/kb1/400/260" alt="Coastline at dawn" class="block w-full h-full object-cover animate-thv23-zoom-in [animation-play-state:paused] group-hover:[animation-play-state:running] motion-reduce:[animation-play-state:paused]!">
<figcaption class="absolute bottom-2.5 left-2.5 px-2 py-1 rounded bg-black/65 backdrop-blur-[4px] text-[0.7rem] tracking-[0.08em] text-white pointer-events-none">Zoom In</figcaption>
</figure>
<figure class="group relative m-0 aspect-[3/2] overflow-hidden rounded-xl shadow-[0_4px_20px_rgba(0,0,0,.5)] cursor-pointer">
<img src="https://picsum.photos/seed/kb2/400/260" alt="Mountain ridge" class="block w-full h-full object-cover animate-thv23-zoom-out [animation-play-state:paused] group-hover:[animation-play-state:running] motion-reduce:[animation-play-state:paused]!">
<figcaption class="absolute bottom-2.5 left-2.5 px-2 py-1 rounded bg-black/65 backdrop-blur-[4px] text-[0.7rem] tracking-[0.08em] text-white pointer-events-none">Zoom Out</figcaption>
</figure>
<figure class="group relative m-0 aspect-[3/2] overflow-hidden rounded-xl shadow-[0_4px_20px_rgba(0,0,0,.5)] cursor-pointer">
<img src="https://picsum.photos/seed/kb3/400/260" alt="City skyline" class="block w-full h-full object-cover animate-thv23-pan-right [animation-play-state:paused] group-hover:[animation-play-state:running] motion-reduce:[animation-play-state:paused]!">
<figcaption class="absolute bottom-2.5 left-2.5 px-2 py-1 rounded bg-black/65 backdrop-blur-[4px] text-[0.7rem] tracking-[0.08em] text-white pointer-events-none">Pan Right</figcaption>
</figure>
<figure class="group relative m-0 aspect-[3/2] overflow-hidden rounded-xl shadow-[0_4px_20px_rgba(0,0,0,.5)] cursor-pointer">
<img src="https://picsum.photos/seed/kb4/400/260" alt="Forest path" class="block w-full h-full object-cover animate-thv23-pan-left [animation-play-state:paused] group-hover:[animation-play-state:running] motion-reduce:[animation-play-state:paused]!">
<figcaption class="absolute bottom-2.5 left-2.5 px-2 py-1 rounded bg-black/65 backdrop-blur-[4px] text-[0.7rem] tracking-[0.08em] text-white pointer-events-none">Pan Left</figcaption>
</figure>
<figure class="group relative m-0 aspect-[3/2] overflow-hidden rounded-xl shadow-[0_4px_20px_rgba(0,0,0,.5)] cursor-pointer">
<img src="https://picsum.photos/seed/kb5/400/260" alt="Desert dunes" class="block w-full h-full object-cover animate-thv23-drift-up [animation-play-state:paused] group-hover:[animation-play-state:running] motion-reduce:[animation-play-state:paused]!">
<figcaption class="absolute bottom-2.5 left-2.5 px-2 py-1 rounded bg-black/65 backdrop-blur-[4px] text-[0.7rem] tracking-[0.08em] text-white pointer-events-none">Drift Up</figcaption>
</figure>
<figure class="group relative m-0 aspect-[3/2] overflow-hidden rounded-xl shadow-[0_4px_20px_rgba(0,0,0,.5)] cursor-pointer">
<img src="https://picsum.photos/seed/kb6/400/260" alt="Harbour at night" class="block w-full h-full object-cover animate-thv23-diagonal [animation-play-state:paused] group-hover:[animation-play-state:running] motion-reduce:[animation-play-state:paused]!">
<figcaption class="absolute bottom-2.5 left-2.5 px-2 py-1 rounded bg-black/65 backdrop-blur-[4px] text-[0.7rem] tracking-[0.08em] text-white pointer-events-none">Diagonal</figcaption>
</figure>
</div>
</section>@import "tailwindcss";
@theme {
--color-thv23-bg: #0d0d0d;
--color-thv23-bg: oklch(0.14 0 0);
--color-thv23-dim: #9a9a9a;
--color-thv23-dim: oklch(0.68 0 0);
--font-thv23-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-thv23-zoom-in: thv23-zoom-in 6s cubic-bezier(.25,.46,.45,.94) both;
--animate-thv23-zoom-out: thv23-zoom-out 6s cubic-bezier(.25,.46,.45,.94) both;
--animate-thv23-pan-right: thv23-pan-right 6s cubic-bezier(.25,.46,.45,.94) both;
--animate-thv23-pan-left: thv23-pan-left 6s cubic-bezier(.25,.46,.45,.94) both;
--animate-thv23-drift-up: thv23-drift-up 6s cubic-bezier(.25,.46,.45,.94) both;
--animate-thv23-diagonal: thv23-diagonal 6s cubic-bezier(.25,.46,.45,.94) both;
}
.thv-23 {
width: 100%;
min-height: 100svh;
}
.thv-23 ::selection {
background: #ffffff;
color: #000;
}
@keyframes thv23-zoom-in {
from {
transform: scale(1) translate(0,0);
}
to {
transform: scale(1.18) translate(-2%,-2%);
}
}
@keyframes thv23-zoom-out {
from {
transform: scale(1.2) translate(-3%,-3%);
}
to {
transform: scale(1) translate(0,0);
}
}
@keyframes thv23-pan-right {
from {
transform: scale(1.14) translate(-5%,0);
}
to {
transform: scale(1.14) translate(5%,0);
}
}
@keyframes thv23-pan-left {
from {
transform: scale(1.14) translate(5%,0);
}
to {
transform: scale(1.14) translate(-5%,0);
}
}
@keyframes thv23-drift-up {
from {
transform: scale(1.14) translate(0,5%);
}
to {
transform: scale(1.14) translate(0,-5%);
}
}
@keyframes thv23-diagonal {
from {
transform: scale(1) translate(0,0);
}
to {
transform: scale(1.2) translate(-4%,-4%);
}
}@import "tailwindcss";
@theme {
--color-thv23-bg: #0d0d0d;
--color-thv23-bg: oklch(0.14 0 0);
--color-thv23-dim: #9a9a9a;
--color-thv23-dim: oklch(0.68 0 0);
--font-thv23-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-thv23-zoom-in: thv23-zoom-in 6s cubic-bezier(.25,.46,.45,.94) both;
--animate-thv23-zoom-out: thv23-zoom-out 6s cubic-bezier(.25,.46,.45,.94) both;
--animate-thv23-pan-right: thv23-pan-right 6s cubic-bezier(.25,.46,.45,.94) both;
--animate-thv23-pan-left: thv23-pan-left 6s cubic-bezier(.25,.46,.45,.94) both;
--animate-thv23-drift-up: thv23-drift-up 6s cubic-bezier(.25,.46,.45,.94) both;
--animate-thv23-diagonal: thv23-diagonal 6s cubic-bezier(.25,.46,.45,.94) both;
}
.thv-23 {
width: 100%;
min-height: 100svh;
}
.thv-23 ::selection {
background: #ffffff;
color: #000;
}
@keyframes thv23-zoom-in {
from {
transform: scale(1) translate(0,0);
}
to {
transform: scale(1.18) translate(-2%,-2%);
}
}
@keyframes thv23-zoom-out {
from {
transform: scale(1.2) translate(-3%,-3%);
}
to {
transform: scale(1) translate(0,0);
}
}
@keyframes thv23-pan-right {
from {
transform: scale(1.14) translate(-5%,0);
}
to {
transform: scale(1.14) translate(5%,0);
}
}
@keyframes thv23-pan-left {
from {
transform: scale(1.14) translate(5%,0);
}
to {
transform: scale(1.14) translate(-5%,0);
}
}
@keyframes thv23-drift-up {
from {
transform: scale(1.14) translate(0,5%);
}
to {
transform: scale(1.14) translate(0,-5%);
}
}
@keyframes thv23-diagonal {
from {
transform: scale(1) translate(0,0);
}
to {
transform: scale(1.2) translate(-4%,-4%);
}
}Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>Compiles in the browser — perfect for a CodePen or a quick test, not for production.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-thv23-bg: #0d0d0d;
--color-thv23-bg: oklch(0.14 0 0);
--color-thv23-dim: #9a9a9a;
--color-thv23-dim: oklch(0.68 0 0);
--font-thv23-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-thv23-zoom-in: thv23-zoom-in 6s cubic-bezier(.25,.46,.45,.94) both;
--animate-thv23-zoom-out: thv23-zoom-out 6s cubic-bezier(.25,.46,.45,.94) both;
--animate-thv23-pan-right: thv23-pan-right 6s cubic-bezier(.25,.46,.45,.94) both;
--animate-thv23-pan-left: thv23-pan-left 6s cubic-bezier(.25,.46,.45,.94) both;
--animate-thv23-drift-up: thv23-drift-up 6s cubic-bezier(.25,.46,.45,.94) both;
--animate-thv23-diagonal: thv23-diagonal 6s cubic-bezier(.25,.46,.45,.94) both;
}
.thv-23 {
width: 100%;
min-height: 100svh;
}
.thv-23 ::selection {
background: #ffffff;
color: #000;
}
@keyframes thv23-zoom-in {
from {
transform: scale(1) translate(0,0);
}
to {
transform: scale(1.18) translate(-2%,-2%);
}
}
@keyframes thv23-zoom-out {
from {
transform: scale(1.2) translate(-3%,-3%);
}
to {
transform: scale(1) translate(0,0);
}
}
@keyframes thv23-pan-right {
from {
transform: scale(1.14) translate(-5%,0);
}
to {
transform: scale(1.14) translate(5%,0);
}
}
@keyframes thv23-pan-left {
from {
transform: scale(1.14) translate(5%,0);
}
to {
transform: scale(1.14) translate(-5%,0);
}
}
@keyframes thv23-drift-up {
from {
transform: scale(1.14) translate(0,5%);
}
to {
transform: scale(1.14) translate(0,-5%);
}
}
@keyframes thv23-diagonal {
from {
transform: scale(1) translate(0,0);
}
to {
transform: scale(1.2) translate(-4%,-4%);
}
}@import "tailwindcss";
@theme {
--color-thv23-bg: #0d0d0d;
--color-thv23-bg: oklch(0.14 0 0);
--color-thv23-dim: #9a9a9a;
--color-thv23-dim: oklch(0.68 0 0);
--font-thv23-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-thv23-zoom-in: thv23-zoom-in 6s cubic-bezier(.25,.46,.45,.94) both;
--animate-thv23-zoom-out: thv23-zoom-out 6s cubic-bezier(.25,.46,.45,.94) both;
--animate-thv23-pan-right: thv23-pan-right 6s cubic-bezier(.25,.46,.45,.94) both;
--animate-thv23-pan-left: thv23-pan-left 6s cubic-bezier(.25,.46,.45,.94) both;
--animate-thv23-drift-up: thv23-drift-up 6s cubic-bezier(.25,.46,.45,.94) both;
--animate-thv23-diagonal: thv23-diagonal 6s cubic-bezier(.25,.46,.45,.94) both;
}
.thv-23 {
width: 100%;
min-height: 100svh;
}
.thv-23 ::selection {
background: #ffffff;
color: #000;
}
@keyframes thv23-zoom-in {
from {
transform: scale(1) translate(0,0);
}
to {
transform: scale(1.18) translate(-2%,-2%);
}
}
@keyframes thv23-zoom-out {
from {
transform: scale(1.2) translate(-3%,-3%);
}
to {
transform: scale(1) translate(0,0);
}
}
@keyframes thv23-pan-right {
from {
transform: scale(1.14) translate(-5%,0);
}
to {
transform: scale(1.14) translate(5%,0);
}
}
@keyframes thv23-pan-left {
from {
transform: scale(1.14) translate(5%,0);
}
to {
transform: scale(1.14) translate(-5%,0);
}
}
@keyframes thv23-drift-up {
from {
transform: scale(1.14) translate(0,5%);
}
to {
transform: scale(1.14) translate(0,-5%);
}
}
@keyframes thv23-diagonal {
from {
transform: scale(1) translate(0,0);
}
to {
transform: scale(1.2) translate(-4%,-4%);
}
}<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;800&display=swap">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;800&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- This is the demo where an arbitrary property is plainly correct:
animation-play-stateis a control signal, not a design token, and it should never become a utility. - UI change from the CSS original: every image carries a descriptive
alt(the source shippedalt="zoom-in", which describes the effect rather than the picture), and the labels are real<figcaption>s. - Each preset's duration and easing live in its
@themeentry, so retiming the whole set is six one-line edits in one block.
Here's a working CSS demo 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: Tailwind Ken Burns Image Hover Effect
Source: https://codefronts.com/motion/tailwind-css-hover-effects/tailwind-ken-burns-image-hover-effect/
Six documentary-style slow zoom and pan presets that run only while hovered. Each animation is paused at rest and resumed on hover — animation-play-state has no utility, so it rides as an arbitrary property, which is exactly the right call.
## HTML
```html
<section class="thv-23 w-full min-h-svh bg-thv23-bg font-thv23-sans px-8 py-10">
<p class="text-center text-[0.78rem] tracking-[0.12em] uppercase text-thv23-dim mb-8">Ken Burns Hover — 6 Presets</p>
<div class="grid grid-cols-[repeat(auto-fit,minmax(230px,1fr))] gap-5 max-w-[860px] mx-auto">
<figure class="group relative m-0 aspect-[3/2] overflow-hidden rounded-xl shadow-[0_4px_20px_rgba(0,0,0,.5)] cursor-pointer">
<img src="https://picsum.photos/seed/kb1/400/260" alt="Coastline at dawn" class="block w-full h-full object-cover animate-thv23-zoom-in [animation-play-state:paused] group-hover:[animation-play-state:running] motion-reduce:[animation-play-state:paused]!">
<figcaption class="absolute bottom-2.5 left-2.5 px-2 py-1 rounded bg-black/65 backdrop-blur-[4px] text-[0.7rem] tracking-[0.08em] text-white pointer-events-none">Zoom In</figcaption>
</figure>
<figure class="group relative m-0 aspect-[3/2] overflow-hidden rounded-xl shadow-[0_4px_20px_rgba(0,0,0,.5)] cursor-pointer">
<img src="https://picsum.photos/seed/kb2/400/260" alt="Mountain ridge" class="block w-full h-full object-cover animate-thv23-zoom-out [animation-play-state:paused] group-hover:[animation-play-state:running] motion-reduce:[animation-play-state:paused]!">
<figcaption class="absolute bottom-2.5 left-2.5 px-2 py-1 rounded bg-black/65 backdrop-blur-[4px] text-[0.7rem] tracking-[0.08em] text-white pointer-events-none">Zoom Out</figcaption>
</figure>
<figure class="group relative m-0 aspect-[3/2] overflow-hidden rounded-xl shadow-[0_4px_20px_rgba(0,0,0,.5)] cursor-pointer">
<img src="https://picsum.photos/seed/kb3/400/260" alt="City skyline" class="block w-full h-full object-cover animate-thv23-pan-right [animation-play-state:paused] group-hover:[animation-play-state:running] motion-reduce:[animation-play-state:paused]!">
<figcaption class="absolute bottom-2.5 left-2.5 px-2 py-1 rounded bg-black/65 backdrop-blur-[4px] text-[0.7rem] tracking-[0.08em] text-white pointer-events-none">Pan Right</figcaption>
</figure>
<figure class="group relative m-0 aspect-[3/2] overflow-hidden rounded-xl shadow-[0_4px_20px_rgba(0,0,0,.5)] cursor-pointer">
<img src="https://picsum.photos/seed/kb4/400/260" alt="Forest path" class="block w-full h-full object-cover animate-thv23-pan-left [animation-play-state:paused] group-hover:[animation-play-state:running] motion-reduce:[animation-play-state:paused]!">
<figcaption class="absolute bottom-2.5 left-2.5 px-2 py-1 rounded bg-black/65 backdrop-blur-[4px] text-[0.7rem] tracking-[0.08em] text-white pointer-events-none">Pan Left</figcaption>
</figure>
<figure class="group relative m-0 aspect-[3/2] overflow-hidden rounded-xl shadow-[0_4px_20px_rgba(0,0,0,.5)] cursor-pointer">
<img src="https://picsum.photos/seed/kb5/400/260" alt="Desert dunes" class="block w-full h-full object-cover animate-thv23-drift-up [animation-play-state:paused] group-hover:[animation-play-state:running] motion-reduce:[animation-play-state:paused]!">
<figcaption class="absolute bottom-2.5 left-2.5 px-2 py-1 rounded bg-black/65 backdrop-blur-[4px] text-[0.7rem] tracking-[0.08em] text-white pointer-events-none">Drift Up</figcaption>
</figure>
<figure class="group relative m-0 aspect-[3/2] overflow-hidden rounded-xl shadow-[0_4px_20px_rgba(0,0,0,.5)] cursor-pointer">
<img src="https://picsum.photos/seed/kb6/400/260" alt="Harbour at night" class="block w-full h-full object-cover animate-thv23-diagonal [animation-play-state:paused] group-hover:[animation-play-state:running] motion-reduce:[animation-play-state:paused]!">
<figcaption class="absolute bottom-2.5 left-2.5 px-2 py-1 rounded bg-black/65 backdrop-blur-[4px] text-[0.7rem] tracking-[0.08em] text-white pointer-events-none">Diagonal</figcaption>
</figure>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-thv23-bg: #0d0d0d;
--color-thv23-bg: oklch(0.14 0 0);
--color-thv23-dim: #9a9a9a;
--color-thv23-dim: oklch(0.68 0 0);
--font-thv23-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-thv23-zoom-in: thv23-zoom-in 6s cubic-bezier(.25,.46,.45,.94) both;
--animate-thv23-zoom-out: thv23-zoom-out 6s cubic-bezier(.25,.46,.45,.94) both;
--animate-thv23-pan-right: thv23-pan-right 6s cubic-bezier(.25,.46,.45,.94) both;
--animate-thv23-pan-left: thv23-pan-left 6s cubic-bezier(.25,.46,.45,.94) both;
--animate-thv23-drift-up: thv23-drift-up 6s cubic-bezier(.25,.46,.45,.94) both;
--animate-thv23-diagonal: thv23-diagonal 6s cubic-bezier(.25,.46,.45,.94) both;
}
.thv-23 {
width: 100%;
min-height: 100svh;
}
.thv-23 ::selection {
background: #ffffff;
color: #000;
}
@keyframes thv23-zoom-in {
from {
transform: scale(1) translate(0,0);
}
to {
transform: scale(1.18) translate(-2%,-2%);
}
}
@keyframes thv23-zoom-out {
from {
transform: scale(1.2) translate(-3%,-3%);
}
to {
transform: scale(1) translate(0,0);
}
}
@keyframes thv23-pan-right {
from {
transform: scale(1.14) translate(-5%,0);
}
to {
transform: scale(1.14) translate(5%,0);
}
}
@keyframes thv23-pan-left {
from {
transform: scale(1.14) translate(5%,0);
}
to {
transform: scale(1.14) translate(-5%,0);
}
}
@keyframes thv23-drift-up {
from {
transform: scale(1.14) translate(0,5%);
}
to {
transform: scale(1.14) translate(0,-5%);
}
}
@keyframes thv23-diagonal {
from {
transform: scale(1) translate(0,0);
}
to {
transform: scale(1.2) translate(-4%,-4%);
}
}
```Here's a working CSS demo 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: Tailwind Ken Burns Image Hover Effect
Source: https://codefronts.com/motion/tailwind-css-hover-effects/tailwind-ken-burns-image-hover-effect/
Six documentary-style slow zoom and pan presets that run only while hovered. Each animation is paused at rest and resumed on hover — animation-play-state has no utility, so it rides as an arbitrary property, which is exactly the right call.
## HTML
```html
<section class="thv-23 w-full min-h-svh bg-thv23-bg font-thv23-sans px-8 py-10">
<p class="text-center text-[0.78rem] tracking-[0.12em] uppercase text-thv23-dim mb-8">Ken Burns Hover — 6 Presets</p>
<div class="grid grid-cols-[repeat(auto-fit,minmax(230px,1fr))] gap-5 max-w-[860px] mx-auto">
<figure class="group relative m-0 aspect-[3/2] overflow-hidden rounded-xl shadow-[0_4px_20px_rgba(0,0,0,.5)] cursor-pointer">
<img src="https://picsum.photos/seed/kb1/400/260" alt="Coastline at dawn" class="block w-full h-full object-cover animate-thv23-zoom-in [animation-play-state:paused] group-hover:[animation-play-state:running] motion-reduce:[animation-play-state:paused]!">
<figcaption class="absolute bottom-2.5 left-2.5 px-2 py-1 rounded bg-black/65 backdrop-blur-[4px] text-[0.7rem] tracking-[0.08em] text-white pointer-events-none">Zoom In</figcaption>
</figure>
<figure class="group relative m-0 aspect-[3/2] overflow-hidden rounded-xl shadow-[0_4px_20px_rgba(0,0,0,.5)] cursor-pointer">
<img src="https://picsum.photos/seed/kb2/400/260" alt="Mountain ridge" class="block w-full h-full object-cover animate-thv23-zoom-out [animation-play-state:paused] group-hover:[animation-play-state:running] motion-reduce:[animation-play-state:paused]!">
<figcaption class="absolute bottom-2.5 left-2.5 px-2 py-1 rounded bg-black/65 backdrop-blur-[4px] text-[0.7rem] tracking-[0.08em] text-white pointer-events-none">Zoom Out</figcaption>
</figure>
<figure class="group relative m-0 aspect-[3/2] overflow-hidden rounded-xl shadow-[0_4px_20px_rgba(0,0,0,.5)] cursor-pointer">
<img src="https://picsum.photos/seed/kb3/400/260" alt="City skyline" class="block w-full h-full object-cover animate-thv23-pan-right [animation-play-state:paused] group-hover:[animation-play-state:running] motion-reduce:[animation-play-state:paused]!">
<figcaption class="absolute bottom-2.5 left-2.5 px-2 py-1 rounded bg-black/65 backdrop-blur-[4px] text-[0.7rem] tracking-[0.08em] text-white pointer-events-none">Pan Right</figcaption>
</figure>
<figure class="group relative m-0 aspect-[3/2] overflow-hidden rounded-xl shadow-[0_4px_20px_rgba(0,0,0,.5)] cursor-pointer">
<img src="https://picsum.photos/seed/kb4/400/260" alt="Forest path" class="block w-full h-full object-cover animate-thv23-pan-left [animation-play-state:paused] group-hover:[animation-play-state:running] motion-reduce:[animation-play-state:paused]!">
<figcaption class="absolute bottom-2.5 left-2.5 px-2 py-1 rounded bg-black/65 backdrop-blur-[4px] text-[0.7rem] tracking-[0.08em] text-white pointer-events-none">Pan Left</figcaption>
</figure>
<figure class="group relative m-0 aspect-[3/2] overflow-hidden rounded-xl shadow-[0_4px_20px_rgba(0,0,0,.5)] cursor-pointer">
<img src="https://picsum.photos/seed/kb5/400/260" alt="Desert dunes" class="block w-full h-full object-cover animate-thv23-drift-up [animation-play-state:paused] group-hover:[animation-play-state:running] motion-reduce:[animation-play-state:paused]!">
<figcaption class="absolute bottom-2.5 left-2.5 px-2 py-1 rounded bg-black/65 backdrop-blur-[4px] text-[0.7rem] tracking-[0.08em] text-white pointer-events-none">Drift Up</figcaption>
</figure>
<figure class="group relative m-0 aspect-[3/2] overflow-hidden rounded-xl shadow-[0_4px_20px_rgba(0,0,0,.5)] cursor-pointer">
<img src="https://picsum.photos/seed/kb6/400/260" alt="Harbour at night" class="block w-full h-full object-cover animate-thv23-diagonal [animation-play-state:paused] group-hover:[animation-play-state:running] motion-reduce:[animation-play-state:paused]!">
<figcaption class="absolute bottom-2.5 left-2.5 px-2 py-1 rounded bg-black/65 backdrop-blur-[4px] text-[0.7rem] tracking-[0.08em] text-white pointer-events-none">Diagonal</figcaption>
</figure>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-thv23-bg: #0d0d0d;
--color-thv23-bg: oklch(0.14 0 0);
--color-thv23-dim: #9a9a9a;
--color-thv23-dim: oklch(0.68 0 0);
--font-thv23-sans: 'Space Grotesk', system-ui, sans-serif;
--animate-thv23-zoom-in: thv23-zoom-in 6s cubic-bezier(.25,.46,.45,.94) both;
--animate-thv23-zoom-out: thv23-zoom-out 6s cubic-bezier(.25,.46,.45,.94) both;
--animate-thv23-pan-right: thv23-pan-right 6s cubic-bezier(.25,.46,.45,.94) both;
--animate-thv23-pan-left: thv23-pan-left 6s cubic-bezier(.25,.46,.45,.94) both;
--animate-thv23-drift-up: thv23-drift-up 6s cubic-bezier(.25,.46,.45,.94) both;
--animate-thv23-diagonal: thv23-diagonal 6s cubic-bezier(.25,.46,.45,.94) both;
}
.thv-23 {
width: 100%;
min-height: 100svh;
}
.thv-23 ::selection {
background: #ffffff;
color: #000;
}
@keyframes thv23-zoom-in {
from {
transform: scale(1) translate(0,0);
}
to {
transform: scale(1.18) translate(-2%,-2%);
}
}
@keyframes thv23-zoom-out {
from {
transform: scale(1.2) translate(-3%,-3%);
}
to {
transform: scale(1) translate(0,0);
}
}
@keyframes thv23-pan-right {
from {
transform: scale(1.14) translate(-5%,0);
}
to {
transform: scale(1.14) translate(5%,0);
}
}
@keyframes thv23-pan-left {
from {
transform: scale(1.14) translate(5%,0);
}
to {
transform: scale(1.14) translate(-5%,0);
}
}
@keyframes thv23-drift-up {
from {
transform: scale(1.14) translate(0,5%);
}
to {
transform: scale(1.14) translate(0,-5%);
}
}
@keyframes thv23-diagonal {
from {
transform: scale(1) translate(0,0);
}
to {
transform: scale(1.2) translate(-4%,-4%);
}
}
```How this works
Pausing beats toggling. Applying an animation only on hover restarts it from frame zero every time; attaching it always and flipping animation-play-state means the shot resumes where it stopped. Tailwind has no utility for the property, so the arbitrary form is not a workaround — it is the API.
Six presets, six tokens. animate-thv23-zoom-in, -zoom-out, -pan-right and so on; the keyframes stay in CSS and @theme exposes each as a utility with duration and easing baked in.
The frame still clips. overflow-hidden on the card, object-cover on the image — the same two-element arrangement as the zoom demo.
Make it yours
- Pace: change the duration inside each
@themeentry —10sreads like a documentary,3slike a trailer. - Intensity: edit the scale endpoints in the keyframes; 1.08–1.25 stays sharp.
- Direction: the pan presets differ only by the sign of their translate endpoints.
Gotchas — read before shipping
overflow-hiddenon the wrapper is mandatory or the scaled frame bleeds.- Pausing rather than removing the animation is what prevents the jump-to-start flicker on re-hover.
- Scales above 1.5 expose source resolution — these stay at or below 1.2.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
animation-play-state is universally supported; the floor is Tailwind v4's generated sheet.