Open demo

CSS code

HTML
<div><span class="lbl">Corners to frame</span>
<a href="#" class="bd bd-corners">
  <img src="https://images.unsplash.com/photo-1616594039964-ae9021a400a0?auto=format&fit=crop&w=600&h=800&q=70" alt="A bedroom with charcoal walls, a pale upholstered bed and a brass ceiling light" width="600" height="800">
  <span class="bd-cap"><span class="bd-kicker">Residential</span><span class="bd-title">Slate Bedroom, Oslo</span></span>
  <span class="bd-frame" aria-hidden="true"></span>
</a></div>
<div><span class="lbl">Clockwise draw</span>
<a href="#" class="bd bd-draw">
  <img src="https://images.unsplash.com/photo-1618773928121-c32242e63f39?auto=format&fit=crop&w=600&h=800&q=70" alt="A hotel bedroom with a wide white bed, bedside lamps and timber panelling" width="600" height="800">
  <span class="bd-cap"><span class="bd-kicker">Hospitality</span><span class="bd-title">Harbour Hotel Suites</span></span>
  <span class="bd-frame" aria-hidden="true"></span>
</a></div>
<div><span class="lbl">Inset frame</span>
<a href="#" class="bd bd-inset">
  <img src="https://images.unsplash.com/photo-1586023492125-27b2c045efd7?auto=format&fit=crop&w=600&h=800&q=70" alt="A mustard armchair beside a floor lamp and a framed print in a white living room" width="600" height="800">
  <span class="bd-cap"><span class="bd-kicker">Styling</span><span class="bd-title">Mustard Reading Corner</span></span>
  <span class="bd-frame" aria-hidden="true"></span>
</a></div>
<div><span class="lbl">SVG stroke trace</span>
<a href="#" class="bd bd-svg">
  <img src="https://images.unsplash.com/photo-1555041469-a586c61ea9bc?auto=format&fit=crop&w=600&h=800&q=70" alt="A green velvet sofa against a plain white wall on oak floorboards" width="600" height="800">
  <span class="bd-cap"><span class="bd-kicker">Furniture</span><span class="bd-title">Velvet Studio Lounge</span></span>
  <span class="bd-frame" aria-hidden="true"><svg focusable="false"><rect x="0" y="0" width="100%" height="100%" pathLength="100"/></svg></span>
</a></div>
CSS
/* Border and outline draw-on-hover effects. The caption is always visible; the lines are decoration,
   so on touch screens nothing is lost. Each frame draws on :hover and on :focus-visible. */
.bd{--bd-line:#fff;--bd-weight:2px;--bd-gap:14px;--bd-radius:6px;--bd-focus:#1c1917;--bd-ease:cubic-bezier(.4,0,.2,1);
  position:relative;display:block;aspect-ratio:3/4;overflow:hidden;border-radius:var(--bd-radius);background:#2b2622;
  color:#fff;text-decoration:none}
.bd:focus-visible{outline:3px solid var(--bd-focus);outline-offset:4px}
.bd img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.bd::after{content:"";position:absolute;inset:0;background:linear-gradient(to top,rgba(20,16,12,.9),rgba(20,16,12,.35) 45%,rgba(20,16,12,.05) 70%);
  transition:background-color .4s}
.bd:is(:hover,:focus-visible)::after{background-color:rgba(20,16,12,.25)}
.bd-cap{position:absolute;z-index:2;left:0;right:0;bottom:0;display:grid;gap:4px;padding:0 calc(var(--bd-gap) + 16px) calc(var(--bd-gap) + 16px);line-height:1.3}
.bd-kicker{font-size:.7rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:#fde68a}
.bd-title{font:600 1.2rem/1.2 Georgia,"Times New Roman",serif}
.bd-frame{position:absolute;z-index:3;inset:var(--bd-gap);pointer-events:none}

/* 1. Corner brackets that grow into a full frame */
.bd-corners .bd-frame::before,.bd-corners .bd-frame::after{content:"";position:absolute;width:28px;height:28px;
  border:var(--bd-weight) solid var(--bd-line);transition:width .5s var(--bd-ease),height .5s var(--bd-ease)}
.bd-corners .bd-frame::before{top:0;left:0;border-right:0;border-bottom:0}
.bd-corners .bd-frame::after{right:0;bottom:0;border-left:0;border-top:0}
.bd-corners:is(:hover,:focus-visible) .bd-frame::before,.bd-corners:is(:hover,:focus-visible) .bd-frame::after{width:100%;height:100%}

/* 2. Clockwise draw: top, right, bottom, left in turn */
.bd-draw .bd-frame::before,.bd-draw .bd-frame::after{content:"";position:absolute;width:0;height:0;border:var(--bd-weight) solid transparent}
.bd-draw .bd-frame::before{top:0;left:0}
.bd-draw .bd-frame::after{right:0;bottom:0}
.bd-draw:is(:hover,:focus-visible) .bd-frame::before{width:100%;height:100%;border-top-color:var(--bd-line);border-right-color:var(--bd-line);
  transition:width .22s linear,height .22s linear .22s}
.bd-draw:is(:hover,:focus-visible) .bd-frame::after{width:100%;height:100%;border-bottom-color:var(--bd-line);border-left-color:var(--bd-line);
  transition:border-color 0s .44s,width .22s linear .44s,height .22s linear .66s}

/* 3. Inset frame: an edge-to-edge outline steps inside the photo */
.bd-inset .bd-frame{inset:0;border:1px solid rgba(255,255,255,0);transition:inset .45s var(--bd-ease),border-color .45s}
.bd-inset:is(:hover,:focus-visible) .bd-frame{inset:var(--bd-gap);border-color:var(--bd-line);
  box-shadow:0 0 0 5px rgba(255,255,255,.18)}

/* 4. SVG stroke trace: pathLength="100" lets one dash cover the whole outline */
.bd-svg .bd-frame svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible}
.bd-svg rect{fill:none;stroke:var(--bd-line);stroke-width:var(--bd-weight);stroke-dasharray:100;stroke-dashoffset:100;
  transition:stroke-dashoffset 1.1s var(--bd-ease)}
.bd-svg:is(:hover,:focus-visible) rect{stroke-dashoffset:0}

/* Reduced motion: frames appear at once instead of drawing */
@media (prefers-reduced-motion:reduce){
  .bd .bd-frame,.bd .bd-frame::before,.bd .bd-frame::after,.bd-svg rect{transition:none!important}
}

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div class="row row-cols-1 row-cols-sm-2 row-cols-lg-4 gx-4 gy-5">
<div class="col"><span class="lbl">Corners to frame</span>
<a href="#" class="card text-decoration-none bd bd-corners">
  <img class="card-img object-fit-cover" src="https://images.unsplash.com/photo-1616594039964-ae9021a400a0?auto=format&fit=crop&w=600&h=800&q=70" alt="A bedroom with charcoal walls, a pale upholstered bed and a brass ceiling light" width="600" height="800">
  <span class="card-img-overlay bd-cap"><span class="bd-kicker">Residential</span><span class="bd-title">Slate Bedroom, Oslo</span></span>
  <span class="bd-frame" aria-hidden="true"></span>
</a></div>
<div class="col"><span class="lbl">Clockwise draw</span>
<a href="#" class="card text-decoration-none bd bd-draw">
  <img class="card-img object-fit-cover" src="https://images.unsplash.com/photo-1618773928121-c32242e63f39?auto=format&fit=crop&w=600&h=800&q=70" alt="A hotel bedroom with a wide white bed, bedside lamps and timber panelling" width="600" height="800">
  <span class="card-img-overlay bd-cap"><span class="bd-kicker">Hospitality</span><span class="bd-title">Harbour Hotel Suites</span></span>
  <span class="bd-frame" aria-hidden="true"></span>
</a></div>
<div class="col"><span class="lbl">Inset frame</span>
<a href="#" class="card text-decoration-none bd bd-inset">
  <img class="card-img object-fit-cover" src="https://images.unsplash.com/photo-1586023492125-27b2c045efd7?auto=format&fit=crop&w=600&h=800&q=70" alt="A mustard armchair beside a floor lamp and a framed print in a white living room" width="600" height="800">
  <span class="card-img-overlay bd-cap"><span class="bd-kicker">Styling</span><span class="bd-title">Mustard Reading Corner</span></span>
  <span class="bd-frame" aria-hidden="true"></span>
</a></div>
<div class="col"><span class="lbl">SVG stroke trace</span>
<a href="#" class="card text-decoration-none bd bd-svg">
  <img class="card-img object-fit-cover" src="https://images.unsplash.com/photo-1555041469-a586c61ea9bc?auto=format&fit=crop&w=600&h=800&q=70" alt="A green velvet sofa against a plain white wall on oak floorboards" width="600" height="800">
  <span class="card-img-overlay bd-cap"><span class="bd-kicker">Furniture</span><span class="bd-title">Velvet Studio Lounge</span></span>
  <span class="bd-frame" aria-hidden="true"><svg focusable="false"><rect x="0" y="0" width="100%" height="100%" pathLength="100"/></svg></span>
</a></div>
</div>
CSS
/* Bootstrap .card with .card-img and .card-img-overlay for the caption. Border and outline draw-on-hover effects. The caption is always visible; the lines are decoration,
   so on touch screens nothing is lost. Each frame draws on :hover and on :focus-visible. */
.bd{--bd-line:#fff;--bd-weight:2px;--bd-gap:14px;--bd-radius:6px;--bd-focus:#1c1917;--bd-ease:cubic-bezier(.4,0,.2,1);
  --bs-card-border-width:0;--bs-card-border-radius:var(--bd-radius);--bs-card-inner-border-radius:var(--bd-radius);--bs-card-bg:#2b2622;
  aspect-ratio:3/4;overflow:hidden;color:#fff}
.bd:focus-visible{outline:3px solid var(--bd-focus);outline-offset:4px}
.bd .card-img{position:absolute;inset:0;width:100%;height:100%}
.bd::after{content:"";position:absolute;inset:0;background:linear-gradient(to top,rgba(20,16,12,.9),rgba(20,16,12,.35) 45%,rgba(20,16,12,.05) 70%);
  transition:background-color .4s}
.bd:is(:hover,:focus-visible)::after{background-color:rgba(20,16,12,.25)}
.bd-cap{z-index:2;top:auto;display:grid;border-radius:0;gap:4px;padding:0 calc(var(--bd-gap) + 16px) calc(var(--bd-gap) + 16px);line-height:1.3}
.bd-kicker{font-size:.7rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:#fde68a}
.bd-title{font:600 1.2rem/1.2 Georgia,"Times New Roman",serif}
.bd-frame{position:absolute;z-index:3;inset:var(--bd-gap);pointer-events:none}

/* 1. Corner brackets that grow into a full frame */
.bd-corners .bd-frame::before,.bd-corners .bd-frame::after{content:"";position:absolute;width:28px;height:28px;
  border:var(--bd-weight) solid var(--bd-line);transition:width .5s var(--bd-ease),height .5s var(--bd-ease)}
.bd-corners .bd-frame::before{top:0;left:0;border-right:0;border-bottom:0}
.bd-corners .bd-frame::after{right:0;bottom:0;border-left:0;border-top:0}
.bd-corners:is(:hover,:focus-visible) .bd-frame::before,.bd-corners:is(:hover,:focus-visible) .bd-frame::after{width:100%;height:100%}

/* 2. Clockwise draw: top, right, bottom, left in turn */
.bd-draw .bd-frame::before,.bd-draw .bd-frame::after{content:"";position:absolute;width:0;height:0;border:var(--bd-weight) solid transparent}
.bd-draw .bd-frame::before{top:0;left:0}
.bd-draw .bd-frame::after{right:0;bottom:0}
.bd-draw:is(:hover,:focus-visible) .bd-frame::before{width:100%;height:100%;border-top-color:var(--bd-line);border-right-color:var(--bd-line);
  transition:width .22s linear,height .22s linear .22s}
.bd-draw:is(:hover,:focus-visible) .bd-frame::after{width:100%;height:100%;border-bottom-color:var(--bd-line);border-left-color:var(--bd-line);
  transition:border-color 0s .44s,width .22s linear .44s,height .22s linear .66s}

/* 3. Inset frame: an edge-to-edge outline steps inside the photo */
.bd-inset .bd-frame{inset:0;border:1px solid rgba(255,255,255,0);transition:inset .45s var(--bd-ease),border-color .45s}
.bd-inset:is(:hover,:focus-visible) .bd-frame{inset:var(--bd-gap);border-color:var(--bd-line);
  box-shadow:0 0 0 5px rgba(255,255,255,.18)}

/* 4. SVG stroke trace: pathLength="100" lets one dash cover the whole outline */
.bd-svg .bd-frame svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible}
.bd-svg rect{fill:none;stroke:var(--bd-line);stroke-width:var(--bd-weight);stroke-dasharray:100;stroke-dashoffset:100;
  transition:stroke-dashoffset 1.1s var(--bd-ease)}
.bd-svg:is(:hover,:focus-visible) rect{stroke-dashoffset:0}

/* Reduced motion: frames appear at once instead of drawing */
@media (prefers-reduced-motion:reduce){
  .bd .bd-frame,.bd .bd-frame::before,.bd .bd-frame::after,.bd-svg rect{transition:none!important}
}

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div class="grid grid-cols-1 gap-x-6 gap-y-[46px] min-[576px]:grid-cols-2 min-[992px]:grid-cols-4">
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#6b6157] uppercase">Corners to frame</span>
<a href="#" class="group relative block aspect-[3/4] overflow-hidden rounded-md bg-[#2b2622] text-white no-underline focus-visible:outline-3 focus-visible:outline-offset-4 focus-visible:outline-[#1c1917] after:absolute after:inset-0 after:bg-[linear-gradient(to_top,rgba(20,16,12,.9),rgba(20,16,12,.35)_45%,rgba(20,16,12,.05)_70%)] after:transition-colors after:duration-400 hover:after:bg-[rgba(20,16,12,.25)] focus-visible:after:bg-[rgba(20,16,12,.25)]">
  <img class="absolute inset-0 size-full object-cover" src="https://images.unsplash.com/photo-1616594039964-ae9021a400a0?auto=format&fit=crop&w=600&h=800&q=70" alt="A bedroom with charcoal walls, a pale upholstered bed and a brass ceiling light" width="600" height="800">
  <span class="absolute inset-x-0 bottom-0 z-2 grid gap-1 px-[30px] pb-[30px] leading-[1.3]"><span class="text-[.7rem] font-bold tracking-[.14em] text-[#fde68a] uppercase">Residential</span><span class="font-serif text-[1.2rem] leading-[1.2] font-semibold">Slate Bedroom, Oslo</span></span>
  <span class="pointer-events-none absolute z-3 inset-3.5 before:absolute before:top-0 before:left-0 before:size-7 before:border-t-2 before:border-l-2 before:border-white after:absolute after:right-0 after:bottom-0 after:size-7 after:border-r-2 after:border-b-2 after:border-white before:transition-[width,height] after:transition-[width,height] before:duration-500 after:duration-500 before:ease-[cubic-bezier(.4,0,.2,1)] after:ease-[cubic-bezier(.4,0,.2,1)] group-hover:before:size-full group-focus-visible:before:size-full group-hover:after:size-full group-focus-visible:after:size-full motion-reduce:before:transition-none motion-reduce:after:transition-none" aria-hidden="true"></span>
</a></div>
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#6b6157] uppercase">Clockwise draw</span>
<a href="#" class="group relative block aspect-[3/4] overflow-hidden rounded-md bg-[#2b2622] text-white no-underline focus-visible:outline-3 focus-visible:outline-offset-4 focus-visible:outline-[#1c1917] after:absolute after:inset-0 after:bg-[linear-gradient(to_top,rgba(20,16,12,.9),rgba(20,16,12,.35)_45%,rgba(20,16,12,.05)_70%)] after:transition-colors after:duration-400 hover:after:bg-[rgba(20,16,12,.25)] focus-visible:after:bg-[rgba(20,16,12,.25)]">
  <img class="absolute inset-0 size-full object-cover" src="https://images.unsplash.com/photo-1618773928121-c32242e63f39?auto=format&fit=crop&w=600&h=800&q=70" alt="A hotel bedroom with a wide white bed, bedside lamps and timber panelling" width="600" height="800">
  <span class="absolute inset-x-0 bottom-0 z-2 grid gap-1 px-[30px] pb-[30px] leading-[1.3]"><span class="text-[.7rem] font-bold tracking-[.14em] text-[#fde68a] uppercase">Hospitality</span><span class="font-serif text-[1.2rem] leading-[1.2] font-semibold">Harbour Hotel Suites</span></span>
  <span class="pointer-events-none absolute z-3 inset-3.5 before:absolute before:top-0 before:left-0 before:size-0 before:border-2 before:border-transparent after:absolute after:right-0 after:bottom-0 after:size-0 after:border-2 after:border-transparent group-hover:before:size-full group-focus-visible:before:size-full group-hover:before:border-t-white group-focus-visible:before:border-t-white group-hover:before:border-r-white group-focus-visible:before:border-r-white group-hover:before:[transition:width_.22s_linear,height_.22s_linear_.22s] group-focus-visible:before:[transition:width_.22s_linear,height_.22s_linear_.22s] group-hover:after:size-full group-focus-visible:after:size-full group-hover:after:border-b-white group-focus-visible:after:border-b-white group-hover:after:border-l-white group-focus-visible:after:border-l-white group-hover:after:[transition:border-color_0s_.44s,width_.22s_linear_.44s,height_.22s_linear_.66s] group-focus-visible:after:[transition:border-color_0s_.44s,width_.22s_linear_.44s,height_.22s_linear_.66s] motion-reduce:before:transition-none! motion-reduce:after:transition-none!" aria-hidden="true"></span>
</a></div>
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#6b6157] uppercase">Inset frame</span>
<a href="#" class="group relative block aspect-[3/4] overflow-hidden rounded-md bg-[#2b2622] text-white no-underline focus-visible:outline-3 focus-visible:outline-offset-4 focus-visible:outline-[#1c1917] after:absolute after:inset-0 after:bg-[linear-gradient(to_top,rgba(20,16,12,.9),rgba(20,16,12,.35)_45%,rgba(20,16,12,.05)_70%)] after:transition-colors after:duration-400 hover:after:bg-[rgba(20,16,12,.25)] focus-visible:after:bg-[rgba(20,16,12,.25)]">
  <img class="absolute inset-0 size-full object-cover" src="https://images.unsplash.com/photo-1586023492125-27b2c045efd7?auto=format&fit=crop&w=600&h=800&q=70" alt="A mustard armchair beside a floor lamp and a framed print in a white living room" width="600" height="800">
  <span class="absolute inset-x-0 bottom-0 z-2 grid gap-1 px-[30px] pb-[30px] leading-[1.3]"><span class="text-[.7rem] font-bold tracking-[.14em] text-[#fde68a] uppercase">Styling</span><span class="font-serif text-[1.2rem] leading-[1.2] font-semibold">Mustard Reading Corner</span></span>
  <span class="pointer-events-none absolute z-3 inset-0 border border-white/0 transition-[inset,border-color,box-shadow] duration-450 ease-[cubic-bezier(.4,0,.2,1)] group-hover:inset-3.5 group-focus-visible:inset-3.5 group-hover:border-white group-focus-visible:border-white group-hover:shadow-[0_0_0_5px_rgba(255,255,255,.18)] group-focus-visible:shadow-[0_0_0_5px_rgba(255,255,255,.18)] motion-reduce:transition-none" aria-hidden="true"></span>
</a></div>
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#6b6157] uppercase">SVG stroke trace</span>
<a href="#" class="group relative block aspect-[3/4] overflow-hidden rounded-md bg-[#2b2622] text-white no-underline focus-visible:outline-3 focus-visible:outline-offset-4 focus-visible:outline-[#1c1917] after:absolute after:inset-0 after:bg-[linear-gradient(to_top,rgba(20,16,12,.9),rgba(20,16,12,.35)_45%,rgba(20,16,12,.05)_70%)] after:transition-colors after:duration-400 hover:after:bg-[rgba(20,16,12,.25)] focus-visible:after:bg-[rgba(20,16,12,.25)]">
  <img class="absolute inset-0 size-full object-cover" src="https://images.unsplash.com/photo-1555041469-a586c61ea9bc?auto=format&fit=crop&w=600&h=800&q=70" alt="A green velvet sofa against a plain white wall on oak floorboards" width="600" height="800">
  <span class="absolute inset-x-0 bottom-0 z-2 grid gap-1 px-[30px] pb-[30px] leading-[1.3]"><span class="text-[.7rem] font-bold tracking-[.14em] text-[#fde68a] uppercase">Furniture</span><span class="font-serif text-[1.2rem] leading-[1.2] font-semibold">Velvet Studio Lounge</span></span>
  <span class="pointer-events-none absolute z-3 inset-3.5" aria-hidden="true"><svg class="absolute inset-0 size-full overflow-visible" focusable="false"><rect class="fill-none stroke-white stroke-2 [stroke-dasharray:100] [stroke-dashoffset:100] transition-[stroke-dashoffset] duration-1100 ease-[cubic-bezier(.4,0,.2,1)] group-hover:[stroke-dashoffset:0] group-focus-visible:[stroke-dashoffset:0] motion-reduce:transition-none" x="0" y="0" width="100%" height="100%" pathLength="100"/></svg></span>
</a></div>
</div>
CSS
@theme {
  --font-serif: Georgia, "Times New Roman", serif;
}

About this set

Four line effects that draw a frame over a photo on hover, shown on an interior design portfolio with the project type and name always visible at the bottom. Corners to frame starts with two white brackets in opposite corners that grow until they meet as a full frame. Clockwise draw traces the top, right, bottom and left edges in turn, using two pseudo-elements whose width and height transition with staggered delays. Inset frame starts as an invisible outline on the photo’s edge and steps fourteen pixels inside it with a soft halo. SVG stroke trace draws a rectangle with stroke-dashoffset, using pathLength=”100″ so one dash covers the whole outline at any card size. Use the corners or the inset frame for a quiet gallery, and the clockwise draw or SVG trace where the hover should feel like an event. The lines are decoration, so nothing is lost on touch screens: the caption is always there. Under reduced motion the frames appear at once instead of drawing.

What’s included

  • Four frames: corner brackets, clockwise draw, inset outline, SVG stroke trace
  • Captions always visible, so nothing depends on hover
  • SVG trace uses pathLength so it fits any card size
  • Same effect on :hover and :focus-visible
  • Frames appear without animation under reduced motion

Accessibility

Each tile is a link named by the photo's alt text and the visible caption; the frame element is aria-hidden. Keyboard focus shows a 3px outline outside the photo and draws the same frame as hover. Under prefers-reduced-motion the frames appear instantly and the photo still darkens slightly.

Customise it

Set --bd-line, --bd-weight, --bd-gap and --bd-radius on .bd for line colour, thickness, inset and corners. The draw speed lives in each variant's transition. In Tailwind change border-white, stroke-white, inset-3.5 and the duration- utilities.