Skip to example

Shared layout animation

An example of a shared layout animation, where an element animates between positions using Motion's animateLayout with data-layout-id.

JavaScript

Source code

<div class="container">
    <nav>
        <ul class="tabs">
            <li class="tab selected" data-tab="tomato">
                <span class="tab-label">Tomato</span>
                <div class="underline" data-layout-id="underline"></div>
            </li>
            <li class="tab" data-tab="lettuce">
                <span class="tab-label">Lettuce</span>
            </li>
            <li class="tab" data-tab="cheese">
                <span class="tab-label">Cheese</span>
            </li>
        </ul>
    </nav>
    <main class="icon-container">
        <div class="icon" data-content="tomato"></div>
    </main>
</div>

<script type="module">
    import { animate, animateLayout } from "motion"

    const tabs = {
        tomato: "&#x1F345;",
        lettuce: "&#x1F96C;",
        cheese: "&#x1F9C0;",
    }

    let selectedTab = "tomato"
    const iconContainer = document.querySelector(".icon-container")
    const currentIcon = document.querySelector(".icon")
    currentIcon.innerHTML = tabs[selectedTab]

    const tabElements = document.querySelectorAll(".tab")

    tabElements.forEach((tab) => {
        tab.addEventListener("click", async () => {
            const newTab = tab.dataset.tab
            if (newTab === selectedTab) return

            const oldTab = selectedTab
            selectedTab = newTab

            // Animate the underline moving to the new tab
            await animateLayout(
                () => {
                    // Remove underline from old tab
                    document
                        .querySelector(`.tab[data-tab="${oldTab}"]`)
                        .classList.remove("selected")
                    document
                        .querySelector(`.tab[data-tab="${oldTab}"] .underline`)
                        ?.remove()

                    // Add underline to new tab
                    document
                        .querySelector(`.tab[data-tab="${newTab}"]`)
                        .classList.add("selected")
                    const newTabElement = document.querySelector(
                        `.tab[data-tab="${newTab}"]`
                    )
                    const underline = document.createElement("div")
                    underline.className = "underline"
                    underline.setAttribute("data-layout-id", "underline")
                    newTabElement.appendChild(underline)
                },
                { duration: 0.3, ease: [0.4, 0, 0.2, 1] }
            )

            // Animate content change
            const currentIcon = document.querySelector(".icon")

            // Exit animation for old content
            await animate(
                currentIcon,
                { y: -10, opacity: 0 },
                { duration: 0.2 }
            )

            // Update content
            currentIcon.innerHTML = tabs[newTab]
            await animate(
                currentIcon,
                { y: [10, 0], opacity: [0, 1] },
                { duration: 0.2 }
            )
        })
    })
</script>

<style>
    .container {
        width: 480px;
        height: 360px;
        max-width: calc(100% - 40px);
        max-height: calc(100% - 40px);
        border-radius: 10px;
        background: white;
        overflow: hidden;
        box-shadow: 0 1px 1px hsl(0deg 0% 0% / 0.075),
            0 2px 2px hsl(0deg 0% 0% / 0.075), 0 4px 4px hsl(0deg 0% 0% / 0.075),
            0 8px 8px hsl(0deg 0% 0% / 0.075),
            0 16px 16px hsl(0deg 0% 0% / 0.075);
        display: flex;
        flex-direction: column;
    }

    .container nav {
        background: #fdfdfd;
        padding: 5px 5px 0;
        border-radius: 10px;
        border-bottom-left-radius: 0;
        border-bottom-right-radius: 0;
        border-bottom: 1px solid #eeeeee;
        height: 44px;
    }

    .tabs {
        list-style: none;
        padding: 0;
        margin: 0;
        font-weight: 500;
        font-size: 14px;
        display: flex;
        width: 100%;
    }

    .tab {
        border-radius: 5px;
        border-bottom-left-radius: 0;
        border-bottom-right-radius: 0;
        width: 100%;
        padding: 10px 15px;
        position: relative;
        background: white;
        cursor: pointer;
        height: 24px;
        display: flex;
        justify-content: space-between;
        align-items: center;
        flex: 1;
        min-width: 0;
        user-select: none;
        color: var(--black);
    }

    .tab.selected {
        background-color: #eee;
    }

    .tab-label {
        position: relative;
        z-index: 1;
    }

    .underline {
        position: absolute;
        bottom: -2px;
        left: 0;
        right: 0;
        height: 2px;
        background: var(--hue-1);
    }

    .icon-container {
        display: flex;
        justify-content: center;
        align-items: center;
        flex: 1;
    }

    .icon {
        font-size: 128px;
    }
</style>