Weekend in Lisbon
Three days of tiles, tarts and trams.
A video card that starts a preview on hover: the play button fades, clouds drift across the frame and a progress bar fills along the bottom. The playback is faked with transforms, no video file.
Also known as: video hover preview css, youtube thumbnail hover, video card hover effect.
<a class="hfx-video-thumbnail" href="#">
<span class="hfx-video-thumbnail-frame">
<span class="hfx-video-thumbnail-scene"></span>
<span class="hfx-video-thumbnail-play" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M9 6.5v11l9-5.5z"/></svg></span>
<span class="hfx-video-thumbnail-time">12:48</span>
<span class="hfx-video-thumbnail-progress"></span>
</span>
<span class="hfx-video-thumbnail-title">Sunset timelapse from the ridge</span>
<span class="hfx-video-thumbnail-meta">Trail Notes, 18K views</span>
</a>.hfx-video-thumbnail {
--c: var(--hfx-color, #3b82f6);
display: flex;
flex-direction: column;
gap: 0.3em;
width: 14em;
font: 400 var(--hfx-size, 16px) system-ui, sans-serif;
color: inherit;
text-decoration: none;
}
.hfx-video-thumbnail-frame {
position: relative;
isolation: isolate;
aspect-ratio: 16 / 9;
margin-bottom: 0.3em;
overflow: hidden;
border-radius: 0.7em;
}
.hfx-video-thumbnail-scene {
position: absolute;
inset: 0;
background: radial-gradient(75% 42% at 18% 112%, color-mix(in srgb, var(--c) 20%, #0b0a1f) 99%, transparent),
radial-gradient(66% 36% at 92% 108%, color-mix(in srgb, var(--c) 26%, #0b0a1f) 99%, transparent),
conic-gradient(from 128deg at 70% 42%, transparent 0, color-mix(in srgb, var(--c) 32%, #1e1b4b) 0.4deg 103.6deg, transparent 104deg),
conic-gradient(from 120deg at 92% 55%, transparent 0, color-mix(in srgb, var(--c) 32%, #1e1b4b) 0.4deg 119.6deg, transparent 120deg),
conic-gradient(from 124deg at 28% 38%, transparent 0, color-mix(in srgb, var(--c) 42%, #6b2159) 0.4deg 111.6deg, transparent 112deg),
conic-gradient(from 126deg at 50% 52%, transparent 0, color-mix(in srgb, var(--c) 42%, #6b2159) 0.4deg 107.6deg, transparent 108deg),
radial-gradient(circle at 36% 56%, #fef3c7 0 0.9em, rgb(254 215 170 / 0.55) 1em, transparent 3.8em),
linear-gradient(color-mix(in srgb, var(--c) 55%, #1e1b4b), color-mix(in srgb, var(--c) 35%, #f472b6) 55%, color-mix(in srgb, var(--c) 12%, #fdba74) 78%);
transition: transform 0.6s cubic-bezier(0.25, 0.8, 0.25, 1);
}
/* a strip of clouds twice the frame's width that repeats halfway, so its drift loops without a seam */
.hfx-video-thumbnail-scene::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 200%;
height: 55%;
background:
radial-gradient(20% 28% at 22% 45%, rgb(255 255 255 / 0.4), transparent 70%),
radial-gradient(14% 20% at 36% 28%, rgb(255 255 255 / 0.3), transparent 70%),
radial-gradient(18% 24% at 78% 60%, rgb(255 255 255 / 0.3), transparent 70%);
background-size: 50% 100%;
}
.hfx-video-thumbnail-play {
position: absolute;
inset: 0;
display: grid;
place-items: center;
width: 2.6em;
height: 2.6em;
margin: auto;
border-radius: 50%;
background: rgb(0 0 0 / 0.45);
color: #fff;
transition: opacity 0.25s ease, transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.hfx-video-thumbnail-play svg {
width: 1.2em;
height: 1.2em;
fill: currentColor;
}
.hfx-video-thumbnail-time {
position: absolute;
right: 0.55em;
bottom: 0.55em;
padding: 0.15em 0.4em;
border-radius: 0.3em;
background: rgb(0 0 0 / 0.7);
color: #fff;
font-size: 0.7em;
font-weight: 600;
transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.hfx-video-thumbnail-progress {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 0.22em;
background: rgb(255 255 255 / 0.35);
opacity: 0;
transition: opacity 0.2s ease;
}
.hfx-video-thumbnail-progress::after {
content: "";
position: absolute;
inset: 0;
background: var(--c);
transform: scaleX(0);
transform-origin: 0 50%;
}
.hfx-video-thumbnail-title {
font-size: 0.95em;
font-weight: 600;
line-height: 1.3;
transition: color 0.2s ease;
}
.hfx-video-thumbnail-meta {
font-size: 0.78em;
opacity: 0.65;
}
.hfx-video-thumbnail:is(:hover, :focus-visible) .hfx-video-thumbnail-scene {
transform: scale(1.04);
}
.hfx-video-thumbnail:is(:hover, :focus-visible) .hfx-video-thumbnail-scene::after {
animation: hfx-video-thumbnail-drift 10s linear infinite;
}
.hfx-video-thumbnail:is(:hover, :focus-visible) .hfx-video-thumbnail-play {
opacity: 0;
transform: scale(0.6);
}
.hfx-video-thumbnail:is(:hover, :focus-visible) .hfx-video-thumbnail-time {
transform: translateY(-0.35em);
}
.hfx-video-thumbnail:is(:hover, :focus-visible) .hfx-video-thumbnail-progress {
opacity: 1;
}
.hfx-video-thumbnail:is(:hover, :focus-visible) .hfx-video-thumbnail-progress::after {
animation: hfx-video-thumbnail-play 12s linear forwards;
}
.hfx-video-thumbnail:is(:hover, :focus-visible) .hfx-video-thumbnail-title {
color: var(--c);
}
@keyframes hfx-video-thumbnail-drift {
to { transform: translateX(-50%); }
}
@keyframes hfx-video-thumbnail-play {
to { transform: scaleX(1); }
}
@media (prefers-reduced-motion: reduce) {
.hfx-video-thumbnail, .hfx-video-thumbnail *, .hfx-video-thumbnail::before, .hfx-video-thumbnail::after, .hfx-video-thumbnail *::before, .hfx-video-thumbnail *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}--hfx-color on the element or a parent.
Currently previewing #3b82f6.--hfx-size. Every other dimension is in
em, so the whole thing scales together. Currently previewing 16px.transition or animation
shorthand. Staggered delays are relative, so they keep their rhythm.