<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css"
integrity="sha512-Kc323vGBEqzTmouAECnVceyQqyqdsSiqLQISBL29aUW4U/M7pSPA/gEUZQqv1cwx4OnYxTxve5UMg5GT6L4JJg=="
crossorigin="anonymous" referrerpolicy="no-referrer" />
<title>expanding-cards</title>
</head>
<body>
<svg style="display: none;">
<defs>
<filter id="colored-shadow" width="300%" height="300%" x="-0.75" y="-0.75"
color-interpolation-filters="sRGB">
<feOffset in="SourceGraphic" result="copy" />
<feColorMatrix in="copy" type="saturate" values="2" result="saturated" />
<feColorMatrix in="saturated" type="matrix"
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 33 33 33 101 -132" result="brightened" />
<feMorphology in="brightened" operator="dilate" radius="1" result="spread" />
<feGaussianBlur in="spread" stdDeviation="30" result="shadow" />
<feOffset in="SourceGraphic" result="source" />
<feComposite in="source" in2="shadow" operator="over" />
</filter>
</defs>
</svg>
<div class="wrapper"></div>
<script src="../cdns/dat.gui.js"></script>
</body>
</html>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--show-reflections: 0;
--colored-shadow: none;
}
body {
min-height: 100vh;
background-color: #112;
display: grid;
perspective: 1200px;
}
.wrapper {
display: flex;
align-items: center;
justify-content: center;
transform-style: preserve-3d;
font-family: Poppins, sans-serif;
width: 100%;
height: 100%;
filter: var(--colored-shadow);
}
.panel {
position: relative;
flex: .5;
margin: 10px;
background: var(--image-url) no-repeat center / cover;
height: 80%;
min-width: 40px;
max-width: 60px;
border-radius: 35px;
cursor: pointer;
color: #fff;
border: none;
overflow: hidden;
transform-origin: 50% 100%;
transform-style: preserve-3d;
perspective: 250px;
box-reflect: below 10px linear-gradient(to top, rgba(0 0 0 / .4) calc(0px * var(--show-reflections)), #0000 calc(60px * var(--show-reflections)));
transition:
flex .5s cubic-bezier(0.05, 0.61, 0.41, 0.95),
max-width .5s cubic-bezier(0.05, 0.61, 0.41, 0.95);
}
.panel:focus {
outline: none;
}
.panel::before {
content: '';
position: absolute;
inset: 0;
top: calc(100% + 10px);
left: 50%;
translate: -50% 0;
width: 10px;
height: 10px;
border-radius: 50px;
background-color: #fff;
opacity: 0;
transition: opacity .3s, width .3s;
}
.panel .photo {
position: absolute;
inset: -50px;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.panel [popover] {
all: unset;
position: absolute;
width: 0px;
height: 0px;
pointer-events: none;
}
.panel .content {
position: absolute;
left: 0;
bottom: 0;
min-width: 100%;
height: 60px;
display: flex;
align-items: center;
justify-content: stretch;
text-align: left;
padding: 0px 10px;
gap: 10px;
user-select: none;
}
.panel .content img {
display: block;
width: 40px;
aspect-ratio: 1/1;
border-radius: 50%;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
font-size: 1rem;
color: var(--icon-color);
box-shadow: 0 3px 5px -1.5px black;
}
.panel .content h3 {
display: inline-block;
color: rgba(255 255 255 / .9);
line-clamp: 1;
line-clamp: 1;
overflow: hidden;
display: box;
box-orient: vertical;
box-orient: vertical;
font-size: 1.25rem;
font-weight: 400;
font-family: Poppins, sans-serif;
text-align: left;
word-spacing: 1px;
letter-spacing: -1px;
width: 100%;
text-transform: capitalize;
transform: translateX(20px);
opacity: 0;
transition-property: transform, opacity;
transition-duration: .3s;
transition-delay: .3s;
transition-timing-function: cubic-bezier(0.445, 0.05, 0.55, 0.95);
}
.panel h3:hover {
line-clamp: initial;
line-clamp: initial;
overflow: visible;
}
.panel:has([popover]:popover-open) {
max-width: 50%;
flex: 5;
/* background-size: 120%; */
transform: scale(1.02);
margin-inline: 10px;
z-index: 10;
}
.panel:has([popover]:popover-open)::before {
opacity: 1;
width: 50%;
}
.panel [popover]:popover-open+.content h3 {
opacity: 1;
transform: translateX(0);
}
</style>
const
wrapper = document.querySelector(".wrapper"),
options = { show_reflections: false, colored_shadow: false, colored_shadow_on_hover: false }
let panels_count = 5, panels = [];
async function create_panels(panels_count) {
let panels = []
await search_unsplash("neon", panels_count, 1).then(data => {
data.results.forEach((result, i) => {
const panel = document.createElement("button");
panel.className = "panel"
panel.id = "panel-" + i;
panel.dataset.mouseOver = 0
panel.setAttribute("popovertarget", "pop-" + i);
panel.innerHTML = `
<div class="photo" style="background-image: url(${result.urls.regular});"></div>
<div popover="auto" id="pop-${i}"></div>
<div class="content">
<img src="${result.user.profile_image.medium}" alt="Profile image of ${result.user.name}"/>
<h3>${result.alt_description}</h3>
</div>
`
panels.push(panel)
wrapper.append(panel)
panel.onmouseenter = () => panel.dataset.mouseOver = 1
panel.onmouseleave = () => {
panel.dataset.mouseOver = 0
panel.querySelector(".photo").animate([
{ transformOrigin: "50% 50%", transform: `scale(1)` }
], {
duration: 500,
easing: "ease",
fill: "forwards"
})
}
panel.onmousemove = e => {
if (parseInt(panel.dataset.mouseOver)) {
let { clientX: x, clientY: y } = e;
x = ((x - panel.offsetLeft) - (panel.offsetWidth / 2)) / panel.offsetWidth
y = ((y - panel.offsetTop) - (panel.offsetHeight / 2)) / panel.offsetHeight
x1 = (e.clientX - panel.offsetLeft) / panel.offsetWidth
y1 = (e.clientY - panel.offsetTop) / panel.offsetHeight
panel.querySelector(".photo").animate([
{ transformOrigin: `${x1 * 100}% ${y1 * 100}%`, transform: `rotateX(${0}deg) rotateY(${0}deg) scale(1.5)` }
], {
duration: 500,
easing: "ease",
fill: "forwards"
})
}
}
})
})
return panels
}
async function search_unsplash(search_query, count, page) {
const endpoint = `https://api.unsplash.com/search/photos?query=${search_query}&per_page=${count}&page=${page}&client_id=hT5BqMWGeHTnAiBs1SZEzghf0dx12VEy7bomkexkmPI`
const response = await fetch(endpoint)
if (!response.ok) throw Error(response.statusText)
const json = await response.json()
return json
}
create_panels(panels_count)
const gui = new dat.GUI()
const folder1 = gui.addFolder("Expanding Cards")
folder1.open()
const show_reflections = folder1.add(options, "show_reflections")
show_reflections.onChange(e => document.documentElement.style.setProperty("--show-reflections", e ? 1 : 0))
const colored_shadow = folder1.add(options, "colored_shadow")
colored_shadow.onChange(e => document.documentElement.style.setProperty("--colored-shadow", e ? "url(#colored-shadow)" : "none"))
This Pen doesn't use any external CSS resources.