Open demo

CSS code

HTML
<div><span class="lbl">Zoom in</span>
<a href="#" class="zp zp-in">
  <span class="zp-media"><img src="https://images.unsplash.com/photo-1493246507139-91e8fad9978e?auto=format&fit=crop&w=900&h=675&q=70" alt="Snow-capped peaks reflected in a still turquoise lake at sunrise" width="900" height="675"></span>
  <span class="zp-body"><span class="zp-kicker">Canada · 8 days</span><span class="zp-title">Rockies by Rail</span><span class="zp-meta">From $2,480 · small group of 12</span></span>
</a></div>
<div><span class="lbl">Zoom out</span>
<a href="#" class="zp zp-out">
  <span class="zp-media"><img src="https://images.unsplash.com/photo-1501785888041-af3ef285b470?auto=format&fit=crop&w=900&h=675&q=70" alt="A small boat crossing an emerald alpine lake below forested cliffs" width="900" height="675"></span>
  <span class="zp-body"><span class="zp-kicker">Italy · 6 days</span><span class="zp-title">Dolomite Lakes Walk</span><span class="zp-meta">From $1,640 · guided, easy pace</span></span>
</a></div>
<div class="full"><span class="lbl">Pan across a panorama</span>
<a href="#" class="zp zp-pan">
  <span class="zp-media"><img src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?auto=format&fit=crop&w=1800&h=520&q=70" alt="Mountain ridges rising above a sea of cloud at dusk" width="1800" height="520"></span>
  <span class="zp-body"><span class="zp-kicker">Switzerland · 5 days</span><span class="zp-title">Above the Clouds Hut Trek</span><span class="zp-meta">From $1,920 · four mountain huts, one summit morning</span></span>
</a></div>
<div><span class="lbl">Zoom toward a focal point</span>
<a href="#" class="zp zp-focus" style="--zp-x:74%;--zp-y:28%">
  <span class="zp-media"><img src="https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?auto=format&fit=crop&w=900&h=675&q=70" alt="A wide valley of pine forest leading to snowy mountains under a blue sky" width="900" height="675"></span>
  <span class="zp-body"><span class="zp-kicker">New Zealand · 10 days</span><span class="zp-title">Southern Alps Circuit</span><span class="zp-meta">From $3,150 · campervan and huts</span></span>
</a></div>
<div><span class="lbl">Zoom with a slight turn</span>
<a href="#" class="zp zp-turn">
  <span class="zp-media"><img src="https://images.unsplash.com/photo-1507525428034-b723cf961d3e?auto=format&fit=crop&w=900&h=675&q=70" alt="Gentle waves washing onto a sandy beach at sunset" width="900" height="675"></span>
  <span class="zp-body"><span class="zp-kicker">Portugal · 7 days</span><span class="zp-title">Algarve Slow Week</span><span class="zp-meta">From $1,280 · coastal villas</span></span>
</a></div>
CSS
/* Zoom & pan: the photo moves inside a clipped frame, so the card never changes size.
   Every effect runs on :hover and on :focus-visible, so keyboard users see it too. */
.zp{--zp-accent:#a3461f;--zp-ink:#1d1a16;--zp-muted:#5f574e;--zp-card:#fff;--zp-radius:18px;
  --zp-ease:cubic-bezier(.2,.7,.2,1);
  display:block;border-radius:var(--zp-radius);overflow:hidden;background:var(--zp-card);color:var(--zp-ink);
  text-decoration:none;box-shadow:0 1px 2px rgba(29,26,22,.06),0 14px 34px -22px rgba(29,26,22,.45)}
.zp:focus-visible{outline:3px solid var(--zp-accent);outline-offset:4px}
.zp-media{position:relative;display:block;aspect-ratio:4/3;overflow:hidden;background:#d8cdbf}
.zp-media img{display:block;width:100%;height:100%;object-fit:cover;transition:transform .8s var(--zp-ease),filter .4s}
.zp-body{display:grid;gap:4px;padding:16px 20px 20px;line-height:1.4}
.zp-kicker{font-size:.72rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--zp-accent)}
.zp-title{font:600 1.3rem/1.2 Georgia,"Times New Roman",serif;text-decoration:underline 2px transparent;
  text-underline-offset:5px;transition:text-decoration-color .3s}
.zp-meta{font-size:.9rem;color:var(--zp-muted)}
.zp:is(:hover,:focus-visible) .zp-title{text-decoration-color:var(--zp-accent)}

/* 1. Zoom in */
.zp-in:is(:hover,:focus-visible) .zp-media img{transform:scale(1.1)}
/* 2. Zoom out: starts cropped in, settles to the full frame */
.zp-out .zp-media img{transform:scale(1.18)}
.zp-out:is(:hover,:focus-visible) .zp-media img{transform:scale(1)}
/* 3. Pan: a wide photo slides across a letterbox frame */
.zp-pan .zp-media{aspect-ratio:21/9}
.zp-pan .zp-media img{width:150%;max-width:none;transition-duration:3.5s,.4s;transition-timing-function:ease-in-out}
.zp-pan:is(:hover,:focus-visible) .zp-media img{transform:translateX(-33.333%)}
/* 4. Zoom toward a focal point: set --zp-x / --zp-y to the subject */
.zp-focus .zp-media img{transform-origin:var(--zp-x,50%) var(--zp-y,50%)}
.zp-focus:is(:hover,:focus-visible) .zp-media img{transform:scale(1.4)}
/* 5. Zoom with a slight turn */
.zp-turn:is(:hover,:focus-visible) .zp-media img{transform:scale(1.16) rotate(-2.5deg)}

/* Reduced motion: nothing moves; the photo brightens and the title underlines instead */
@media (prefers-reduced-motion:reduce){
  .zp .zp-media img{transform:none!important;transition:filter .3s}
  .zp:is(:hover,:focus-visible) .zp-media img{filter:brightness(1.08) saturate(1.2)}
}

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div class="row gx-4 gy-5">
<div class="col-md-6"><span class="lbl">Zoom in</span>
<a href="#" class="card zp zp-in text-decoration-none">
  <div class="ratio ratio-4x3"><img class="object-fit-cover" src="https://images.unsplash.com/photo-1493246507139-91e8fad9978e?auto=format&fit=crop&w=900&h=675&q=70" alt="Snow-capped peaks reflected in a still turquoise lake at sunrise" width="900" height="675"></div>
  <div class="card-body"><span class="zp-kicker">Canada · 8 days</span><span class="zp-title">Rockies by Rail</span><span class="zp-meta">From $2,480 · small group of 12</span></div>
</a></div>
<div class="col-md-6"><span class="lbl">Zoom out</span>
<a href="#" class="card zp zp-out text-decoration-none">
  <div class="ratio ratio-4x3"><img class="object-fit-cover" src="https://images.unsplash.com/photo-1501785888041-af3ef285b470?auto=format&fit=crop&w=900&h=675&q=70" alt="A small boat crossing an emerald alpine lake below forested cliffs" width="900" height="675"></div>
  <div class="card-body"><span class="zp-kicker">Italy · 6 days</span><span class="zp-title">Dolomite Lakes Walk</span><span class="zp-meta">From $1,640 · guided, easy pace</span></div>
</a></div>
<div class="col-12"><span class="lbl">Pan across a panorama</span>
<a href="#" class="card zp zp-pan text-decoration-none">
  <div class="ratio ratio-21x9"><img class="object-fit-cover" src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?auto=format&fit=crop&w=1800&h=520&q=70" alt="Mountain ridges rising above a sea of cloud at dusk" width="1800" height="520"></div>
  <div class="card-body"><span class="zp-kicker">Switzerland · 5 days</span><span class="zp-title">Above the Clouds Hut Trek</span><span class="zp-meta">From $1,920 · four mountain huts, one summit morning</span></div>
</a></div>
<div class="col-md-6"><span class="lbl">Zoom toward a focal point</span>
<a href="#" class="card zp zp-focus text-decoration-none" style="--zp-x:74%;--zp-y:28%">
  <div class="ratio ratio-4x3"><img class="object-fit-cover" src="https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?auto=format&fit=crop&w=900&h=675&q=70" alt="A wide valley of pine forest leading to snowy mountains under a blue sky" width="900" height="675"></div>
  <div class="card-body"><span class="zp-kicker">New Zealand · 10 days</span><span class="zp-title">Southern Alps Circuit</span><span class="zp-meta">From $3,150 · campervan and huts</span></div>
</a></div>
<div class="col-md-6"><span class="lbl">Zoom with a slight turn</span>
<a href="#" class="card zp zp-turn text-decoration-none">
  <div class="ratio ratio-4x3"><img class="object-fit-cover" src="https://images.unsplash.com/photo-1507525428034-b723cf961d3e?auto=format&fit=crop&w=900&h=675&q=70" alt="Gentle waves washing onto a sandy beach at sunset" width="900" height="675"></div>
  <div class="card-body"><span class="zp-kicker">Portugal · 7 days</span><span class="zp-title">Algarve Slow Week</span><span class="zp-meta">From $1,280 · coastal villas</span></div>
</a></div>
</div>
CSS
/* Bootstrap .card as a link, .ratio frames for the photo. The photo moves inside the frame;
   every effect runs on :hover and :focus-visible. */
.zp{--zp-accent:#a3461f;--zp-ink:#1d1a16;--zp-muted:#5f574e;--zp-ease:cubic-bezier(.2,.7,.2,1);
  --bs-card-border-width:0;--bs-card-border-radius:18px;--bs-card-inner-border-radius:18px;--bs-card-spacer-x:20px;--bs-card-spacer-y:16px;
  --bs-card-bg:#fff;--bs-card-color:var(--zp-ink);
  overflow:hidden;color:var(--zp-ink);box-shadow:0 1px 2px rgba(29,26,22,.06),0 14px 34px -22px rgba(29,26,22,.45)}
.zp:focus-visible{outline:3px solid var(--zp-accent);outline-offset:4px}
.zp .ratio{overflow:hidden;background:#d8cdbf}
.zp .ratio>img{transition:transform .8s var(--zp-ease),filter .4s}
.zp .card-body{display:grid;gap:4px;padding-bottom:20px;line-height:1.4}
.zp-kicker{font-size:.72rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--zp-accent)}
.zp-title{font:600 1.3rem/1.2 Georgia,"Times New Roman",serif;text-decoration:underline 2px transparent;
  text-underline-offset:5px;transition:text-decoration-color .3s}
.zp-meta{font-size:.9rem;color:var(--zp-muted)}
.zp:is(:hover,:focus-visible) .zp-title{text-decoration-color:var(--zp-accent)}

.zp-in:is(:hover,:focus-visible) .ratio>img{transform:scale(1.1)}
.zp-out .ratio>img{transform:scale(1.18)}
.zp-out:is(:hover,:focus-visible) .ratio>img{transform:scale(1)}
.zp-pan .ratio>img{width:150%;max-width:none;transition-duration:3.5s,.4s;transition-timing-function:ease-in-out}
.zp-pan:is(:hover,:focus-visible) .ratio>img{transform:translateX(-33.333%)}
.zp-focus .ratio>img{transform-origin:var(--zp-x,50%) var(--zp-y,50%)}
.zp-focus:is(:hover,:focus-visible) .ratio>img{transform:scale(1.4)}
.zp-turn:is(:hover,:focus-visible) .ratio>img{transform:scale(1.16) rotate(-2.5deg)}

@media (prefers-reduced-motion:reduce){
  .zp .ratio>img{transform:none!important;transition:filter .3s}
  .zp:is(:hover,:focus-visible) .ratio>img{filter:brightness(1.08) saturate(1.2)}
}

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div class="grid grid-cols-1 gap-x-7 gap-y-[46px] md:grid-cols-2">
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#665d53] uppercase">Zoom in</span>
<a href="#" class="group block overflow-hidden rounded-[18px] bg-white text-ink no-underline shadow-[0_1px_2px_rgba(29,26,22,.06),0_14px_34px_-22px_rgba(29,26,22,.45)] focus-visible:outline-3 focus-visible:outline-offset-4 focus-visible:outline-clay">
  <span class="block aspect-[4/3] overflow-hidden bg-[#d8cdbf]"><img class="block h-full object-cover transition-[scale,rotate,translate,filter] ease-[cubic-bezier(.2,.7,.2,1)] motion-reduce:group-hover:brightness-108 motion-reduce:group-hover:saturate-120 motion-reduce:group-focus-visible:brightness-108 motion-reduce:group-focus-visible:saturate-120 w-full duration-[800ms] motion-safe:group-hover:scale-110 motion-safe:group-focus-visible:scale-110" src="https://images.unsplash.com/photo-1493246507139-91e8fad9978e?auto=format&fit=crop&w=900&h=675&q=70" alt="Snow-capped peaks reflected in a still turquoise lake at sunrise" width="900" height="675"></span>
  <span class="grid gap-1 px-5 pt-4 pb-5 leading-[1.4]"><span class="text-[.72rem] font-bold tracking-[.12em] text-clay uppercase">Canada · 8 days</span><span class="font-serif text-[1.3rem] leading-[1.2] font-semibold underline decoration-transparent decoration-2 underline-offset-[5px] transition-[text-decoration-color] duration-300 group-hover:decoration-clay group-focus-visible:decoration-clay">Rockies by Rail</span><span class="text-[.9rem] text-muted">From $2,480 · small group of 12</span></span>
</a></div>
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#665d53] uppercase">Zoom out</span>
<a href="#" class="group block overflow-hidden rounded-[18px] bg-white text-ink no-underline shadow-[0_1px_2px_rgba(29,26,22,.06),0_14px_34px_-22px_rgba(29,26,22,.45)] focus-visible:outline-3 focus-visible:outline-offset-4 focus-visible:outline-clay">
  <span class="block aspect-[4/3] overflow-hidden bg-[#d8cdbf]"><img class="block h-full object-cover transition-[scale,rotate,translate,filter] ease-[cubic-bezier(.2,.7,.2,1)] motion-reduce:group-hover:brightness-108 motion-reduce:group-hover:saturate-120 motion-reduce:group-focus-visible:brightness-108 motion-reduce:group-focus-visible:saturate-120 w-full duration-[800ms] motion-safe:scale-118 motion-safe:group-hover:scale-100 motion-safe:group-focus-visible:scale-100" src="https://images.unsplash.com/photo-1501785888041-af3ef285b470?auto=format&fit=crop&w=900&h=675&q=70" alt="A small boat crossing an emerald alpine lake below forested cliffs" width="900" height="675"></span>
  <span class="grid gap-1 px-5 pt-4 pb-5 leading-[1.4]"><span class="text-[.72rem] font-bold tracking-[.12em] text-clay uppercase">Italy · 6 days</span><span class="font-serif text-[1.3rem] leading-[1.2] font-semibold underline decoration-transparent decoration-2 underline-offset-[5px] transition-[text-decoration-color] duration-300 group-hover:decoration-clay group-focus-visible:decoration-clay">Dolomite Lakes Walk</span><span class="text-[.9rem] text-muted">From $1,640 · guided, easy pace</span></span>
</a></div>
<div class="md:col-span-2"><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#665d53] uppercase">Pan across a panorama</span>
<a href="#" class="group block overflow-hidden rounded-[18px] bg-white text-ink no-underline shadow-[0_1px_2px_rgba(29,26,22,.06),0_14px_34px_-22px_rgba(29,26,22,.45)] focus-visible:outline-3 focus-visible:outline-offset-4 focus-visible:outline-clay">
  <span class="block aspect-[21/9] overflow-hidden bg-[#d8cdbf]"><img class="block h-full object-cover transition-[scale,rotate,translate,filter] ease-[cubic-bezier(.2,.7,.2,1)] motion-reduce:group-hover:brightness-108 motion-reduce:group-hover:saturate-120 motion-reduce:group-focus-visible:brightness-108 motion-reduce:group-focus-visible:saturate-120 w-[150%] max-w-none duration-[3500ms] ease-in-out motion-safe:group-hover:-translate-x-1/3 motion-safe:group-focus-visible:-translate-x-1/3" src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?auto=format&fit=crop&w=1800&h=520&q=70" alt="Mountain ridges rising above a sea of cloud at dusk" width="1800" height="520"></span>
  <span class="grid gap-1 px-5 pt-4 pb-5 leading-[1.4]"><span class="text-[.72rem] font-bold tracking-[.12em] text-clay uppercase">Switzerland · 5 days</span><span class="font-serif text-[1.3rem] leading-[1.2] font-semibold underline decoration-transparent decoration-2 underline-offset-[5px] transition-[text-decoration-color] duration-300 group-hover:decoration-clay group-focus-visible:decoration-clay">Above the Clouds Hut Trek</span><span class="text-[.9rem] text-muted">From $1,920 · four mountain huts, one summit morning</span></span>
</a></div>
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#665d53] uppercase">Zoom toward a focal point</span>
<a href="#" class="group block overflow-hidden rounded-[18px] bg-white text-ink no-underline shadow-[0_1px_2px_rgba(29,26,22,.06),0_14px_34px_-22px_rgba(29,26,22,.45)] focus-visible:outline-3 focus-visible:outline-offset-4 focus-visible:outline-clay">
  <span class="block aspect-[4/3] overflow-hidden bg-[#d8cdbf]"><img class="block h-full object-cover transition-[scale,rotate,translate,filter] ease-[cubic-bezier(.2,.7,.2,1)] motion-reduce:group-hover:brightness-108 motion-reduce:group-hover:saturate-120 motion-reduce:group-focus-visible:brightness-108 motion-reduce:group-focus-visible:saturate-120 w-full duration-[800ms] origin-[74%_28%] motion-safe:group-hover:scale-140 motion-safe:group-focus-visible:scale-140" src="https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?auto=format&fit=crop&w=900&h=675&q=70" alt="A wide valley of pine forest leading to snowy mountains under a blue sky" width="900" height="675"></span>
  <span class="grid gap-1 px-5 pt-4 pb-5 leading-[1.4]"><span class="text-[.72rem] font-bold tracking-[.12em] text-clay uppercase">New Zealand · 10 days</span><span class="font-serif text-[1.3rem] leading-[1.2] font-semibold underline decoration-transparent decoration-2 underline-offset-[5px] transition-[text-decoration-color] duration-300 group-hover:decoration-clay group-focus-visible:decoration-clay">Southern Alps Circuit</span><span class="text-[.9rem] text-muted">From $3,150 · campervan and huts</span></span>
</a></div>
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#665d53] uppercase">Zoom with a slight turn</span>
<a href="#" class="group block overflow-hidden rounded-[18px] bg-white text-ink no-underline shadow-[0_1px_2px_rgba(29,26,22,.06),0_14px_34px_-22px_rgba(29,26,22,.45)] focus-visible:outline-3 focus-visible:outline-offset-4 focus-visible:outline-clay">
  <span class="block aspect-[4/3] overflow-hidden bg-[#d8cdbf]"><img class="block h-full object-cover transition-[scale,rotate,translate,filter] ease-[cubic-bezier(.2,.7,.2,1)] motion-reduce:group-hover:brightness-108 motion-reduce:group-hover:saturate-120 motion-reduce:group-focus-visible:brightness-108 motion-reduce:group-focus-visible:saturate-120 w-full duration-[800ms] motion-safe:group-hover:scale-116 motion-safe:group-focus-visible:scale-116 motion-safe:group-hover:rotate-[-2.5deg] motion-safe:group-focus-visible:rotate-[-2.5deg]" src="https://images.unsplash.com/photo-1507525428034-b723cf961d3e?auto=format&fit=crop&w=900&h=675&q=70" alt="Gentle waves washing onto a sandy beach at sunset" width="900" height="675"></span>
  <span class="grid gap-1 px-5 pt-4 pb-5 leading-[1.4]"><span class="text-[.72rem] font-bold tracking-[.12em] text-clay uppercase">Portugal · 7 days</span><span class="font-serif text-[1.3rem] leading-[1.2] font-semibold underline decoration-transparent decoration-2 underline-offset-[5px] transition-[text-decoration-color] duration-300 group-hover:decoration-clay group-focus-visible:decoration-clay">Algarve Slow Week</span><span class="text-[.9rem] text-muted">From $1,280 · coastal villas</span></span>
</a></div>
</div>
CSS
@theme {
  --color-clay: #a3461f;
  --color-ink: #1d1a16;
  --color-muted: #5f574e;
  --font-serif: Georgia, "Times New Roman", serif;
}

About this set

Five ways to move a photo inside its frame on hover, shown on travel cards with a destination, a tour name and a price. Zoom in scales the photo up by ten percent. Zoom out starts cropped in and settles back to the full frame, which suits photos whose edges matter. Pan across a panorama puts a wide photo in a 21 by 9 letterbox and slides it from left to right over three and a half seconds. Zoom toward a focal point scales the photo around a point you set with two custom properties, so the zoom lands on the subject rather than the centre. Zoom with a slight turn adds a small rotation for a more playful card. In every variant the photo sits inside a clipped frame, so the card never changes size and nothing around it moves. The effect runs on hover and on keyboard focus, and the card title draws an underline at the same time. Under reduced motion the photo stays still and brightens instead. The Bootstrap version uses a card with a .ratio frame and the Tailwind version uses group-hover and group-focus-visible.

What’s included

  • Five movements: zoom in, zoom out, panorama pan, focal-point zoom, zoom with a turn
  • Photo moves inside an overflow-hidden frame, so the layout never shifts
  • Focal point set with --zp-x and --zp-y
  • Same effect on :hover and :focus-visible
  • Reduced motion swaps movement for a brightness change

Accessibility

Each card is a single link whose name comes from the photo's alt text and the visible title, kicker and price. Keyboard focus shows a 3px outline and triggers the same zoom as hover. Under prefers-reduced-motion the photo does not move; it brightens slightly and the title underlines instead.

Customise it

Change --zp-accent, --zp-ink and --zp-radius on .zp, and the scale values in each variant rule. Set --zp-x and --zp-y on a .zp-focus card to aim the zoom. In Tailwind edit the clay, ink and muted theme colours and the scale-, origin- and translate- utilities.