<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;
            -webkit-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;
            -webkit-line-clamp: 1;
            overflow: hidden;
            display: -webkit-box;
            box-orient: vertical;
            -webkit-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;
            -webkit-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"))

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.9/dat.gui.min.js