<svg style="display: none" width="0" height="0" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<g id="face-step-1">
<ellipse class="left-eye" cx="59.5" cy="59.001" rx="26.5" ry="37" fill="#fff"/>
<ellipse class="right-eye" cx="131.5" cy="59.001" rx="26.5" ry="37" fill="#fff"/>
<circle class="left-pupil" cx="59.5" cy="43.501" r="13.5" fill="#884578"/>
<circle class="right-pupil" cx="131.5" cy="43.501" r="13.5" fill="#884578"/>
<ellipse class="left-lower-lid" cx="62.367" cy="95.453" rx="46.487" ry="19.5" transform="rotate(-20.356 62.367 95.453)" fill="#C794BB"/>
<ellipse class="left-upper-lid" rx="42.306" ry="14.491" transform="matrix(.96216 -.27248 .43717 .89938 47.04 24.56)" fill="#C794BB"/>
<ellipse class="right-lower-lid" cx="130.194" cy="94.537" rx="46.487" ry="19.5" transform="rotate(17.442 130.194 94.537)" fill="#C794BB"/>
<ellipse class="right-upper-lid" rx="42.508" ry="14.387" transform="matrix(.9694 .24552 -.39868 .9171 145.057 26.028)" fill="#C794BB"/>
<path class="mouth" d="M98 114c-14.5 0-37 3-49 14.5 6.5 5 16.5 13.5 28 6.5s26-6 41.5 0c8.406 3.254 18.5-3 23.5-10-18-11-29.5-11-44-11Z" fill="#884578"/>
<path class="teeth" d="M57 117a6 6 0 0 1 6-6h66a6 6 0 0 1 6 6v1.463c0 4.65-5.322 7.992-9.781 6.671-6.5-1.926-16.24-3.848-29.219-3.848-12.98 0-22.719 1.922-29.219 3.848-4.46 1.321-9.781-2.021-9.781-6.671V117Z" fill="#fff"/>
</g>
<g id="face-step-2">
<ellipse class="left-eye" cx="59.5" cy="70" rx="26.5" ry="37" fill="#fff"/>
<ellipse class="right-eye" cx="131.5" cy="70" rx="26.5" ry="37" fill="#fff"/>
<circle class="left-pupil" cx="60" cy="72" r="15" fill="#884578"/>
<circle class="right-pupil" cx="132" cy="72" r="15" fill="#884578"/>
<ellipse class="left-lower-lid" cx="54.367" cy="122.452" rx="46.487" ry="19.5" transform="rotate(22.398 54.367 122.452)" fill="#C794BB"/>
<ellipse class="left-upper-lid" rx="44.124" ry="10.325" transform="matrix(.98775 -.15603 .65697 .75392 50.367 15.669)" fill="#C794BB"/>
<ellipse class="right-lower-lid" cx="140.194" cy="121.537" rx="46.487" ry="19.5" transform="rotate(-15 140.194 121.537)" fill="#C794BB"/>
<ellipse class="right-upper-lid" rx="44.559" ry="9.991" transform="matrix(.99017 .13989 -.6147 .78876 143.314 16.545)" fill="#C794BB"/>
<path class="mouth" d="M94 130c-36 0-49.5-9-61-3-9.5 4.957 9.5 36.5 31.5 46 17.48 7.548 41.258 7.727 58 0 19.5-9 42-41.444 34-45-13.5-6-26.5 2-62.5 2Z" fill="#884578"/>
<path class="teeth" d="M39 117a6 6 0 0 1 6-6h101a6 6 0 0 1 6 6v10.614c0 2.612-1.689 4.9-4.233 5.489C140.278 134.835 122.856 138 95.5 138s-44.778-3.165-52.267-4.897c-2.544-.589-4.233-2.877-4.233-5.489V117Z" fill="#fff"/>
</g>
</defs>
</svg>
<div class="browser">
<div class="browser-elements">
<div class="top-bar">
<div class="points">
<span class="point"></span>
<span class="point"></span>
<span class="point"></span>
</div>
</div>
<div class="scroll-bar">
<div class="bar"></div>
</div>
</div>
<div class="page content-spacing">
<div data-flip class="page-background"></div>
<header></header>
<ul class="items content-spacing">
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
<div class="loading"><span class="spinner"></span></div>
<footer data-flip>
<svg class="face" viewBox="0 0 194 194" width="194" height="194" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<clipPath id="teethMask" >
<path class="mouth" d="M98.5 154c-10 0-18.5 3.5-35 13.071C69 173 70.5 166 83 160.5s24-4 31.5 0S128 171 132 165c-14.5-8-23.5-11-33.5-11Z" fill="#884578"/>
</clipPath>
</defs>
<ellipse class="left-eye" cx="59.5" cy="109" rx="26.5" ry="37" fill="#fff"/>
<ellipse class="right-eye" cx="131.5" cy="109" rx="26.5" ry="37" fill="#fff"/>
<circle class="left-pupil" cx="59.5" cy="123.5" r="13.5" fill="#884578"/>
<circle class="right-pupil" cx="131.5" cy="123.5" r="13.5" fill="#884578"/>
<ellipse class="left-lower-lid" cx="54.367" cy="152.453" rx="46.487" ry="19.5" transform="rotate(-20.356 54.367 152.453)" fill="#C794BB"/>
<ellipse class="left-upper-lid" cx="50.367" cy="81.453" rx="46.487" ry="19.5" transform="rotate(-20.356 50.367 81.453)" fill="#C794BB"/>
<ellipse class="right-lower-lid" cx="140.194" cy="151.537" rx="46.487" ry="19.5" transform="rotate(17.442 140.194 151.537)" fill="#C794BB"/>
<ellipse class="right-upper-lid" cx="143.314" cy="83.511" rx="46.487" ry="19.5" transform="rotate(18.357 143.314 83.51)" fill="#C794BB"/>
<path class="mouth" d="M98.5 154c-10 0-18.5 3.5-35 13.071C69 173 70.5 166 83 160.5s24-4 31.5 0S128 171 132 165c-14.5-8-23.5-11-33.5-11Z" fill="#884578"/>
<path class="teeth" clip-path="url(#teethMask)" d="M62.5 136a6 6 0 0 1 6-6h57a6 6 0 0 1 6 6v9a6 6 0 0 1-6 6h-57a6 6 0 0 1-6-6v-9Z" fill="#fff"/>
</svg>
<div class="hand" data-type>
<svg class="hand-type hand-wave" viewBox="0 0 126 115" width="126" height="115" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10.882 56.862c-1.525 6.843-.838 20.375 3.335 30.087 4.172 9.712 14.813 19.839 22.393 24.083s19.868 3.753 28.056 0c8.187-3.753 19.207-13.67 25.999-24.083l29.5-35.644c3-4.26 8.5-11.26 3-15.76s-12 .5-15.5 4.5L86.482 62.37s-2.317 1.675-3.6 0c-1.281-1.675 0-3.298 0-3.298l29.783-42.027c2.5-4 2.999-9.757-1.001-12.5-3.999-2.744-9.5-1.5-12.753 3.802L72.3 49.94s-2.135 3.606-4.633 1.923c-2.5-1.683 0-4.517 0-4.517l17.64-31.864c3.859-7.134 1.359-11.936-2.641-13.936s-10 .5-13 5.5c0 0-18.5 33.5-22 40s-10.42 10.79-13.49 4.26c-3.072-6.53-1.927-14.867-8.53-20.81-9.737-8.761-19.786-6.727-24.98 2.55 5.764 5.152 12.122 15.264 10.216 23.817Z" fill="#9D608F"/></svg>
<svg class="hand-type hand-cookie" viewBox="0 0 118 115" width="118" height="122" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="64.869" cy="52.5" r="52.5" fill="#E6B66C"/><ellipse cx="42.869" cy="28" rx="7.5" ry="8" fill="#87624A"/><ellipse cx="82.869" cy="33" rx="7.5" ry="8" fill="#87624A"/><ellipse cx="51.869" cy="80" rx="7.5" ry="8" fill="#87624A"/><ellipse cx="72.869" cy="56" rx="7.5" ry="8" fill="#87624A"/><ellipse cx="79.869" cy="86" rx="7.5" ry="8" fill="#87624A"/><path d="M2.869 100.5c9.2 25.2 31.5 22.5 44.5 18.5s21.5-8 25.5-14.5c-24.5 2.5-30-5.333-39-9-6-25.5 5.3-14 16.5-18s12-10 6.5-19.5c0 0-10 .5-29.5-6s-33.7 23.3-24.5 48.5Z" fill="#9D608F"/></svg>
</div>
</footer>
</div>
</div>
:root {
--color-background: #F5CAC3;
--color-background-fade: #F5CAC300;
--color-browser: #84A59D;
--color-browser-alt: #C4D4D0;
--color-page: #F7EDE2;
--color-element: #C6CDD2;
--color-footer: #C794BB;
--page-margin: 20px;
--page-padding: 10px;
--top-bar-height: 30px;
--scroll-bar-width: 0px;
--content-gap: 20px;
--list-padding: 2rem;
--browser-border-radius: 30px;
--browser-top-bar-width: min(50%, 200px);
--browser-top-bar-radius: 20px;
--points-display: none;
--footer-face-width: clamp(60px, 20vh, 194px);
--footer-face-position: flex-start;
--footer-hand-position: 60%;
--shadow-color: 9deg 32% 60%;
--shadow-elevation-high:
0.3px 0.5px 0.6px hsl(var(--shadow-color) / 0.36),
0.9px 1.9px 2.1px -0.5px hsl(var(--shadow-color) / 0.33),
1.8px 3.5px 4px -0.9px hsl(var(--shadow-color) / 0.31),
3.1px 6.1px 6.9px -1.4px hsl(var(--shadow-color) / 0.29),
5.2px 10.5px 11.8px -1.9px hsl(var(--shadow-color) / 0.27),
8.6px 17.2px 19.3px -2.4px hsl(var(--shadow-color) / 0.25),
13.5px 26.9px 30.2px -2.8px hsl(var(--shadow-color) / 0.23),
20.3px 40.5px 45.5px -3.3px hsl(var(--shadow-color) / 0.2);
}
@media (min-width: 550px) {
:root {
--list-padding: 5rem;
--browser-border-radius: 10px;
--browser-top-bar-width: 100%;
--browser-top-bar-radius: 0px;
--scroll-bar-width: 0px;
--footer-face-position: center;
--points-display: flex;
--footer-hand-position: 70%;
}
}
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background-color: var(--color-background);
}
.browser {
--right: calc(var(--scroll-bar-width) + var(--page-margin));
--left: var(--page-margin);
--width: min(90vmin, 1000px);
--margin-top: 5vmin;
width: calc(var(--width) - (var(--left) + var(--right)));
padding: calc(var(--top-bar-height) + var(--page-margin)) var(--right) 40vh var(--left);
position: relative;
display: block;
margin: var(--margin-top) auto 0 auto;
border: solid 5px transparent;
&::before,
&::after,
.browser-elements {
content: '';
display: flex;
position: fixed;
width: var(--width);
left: 50%;
height: calc(50vh - 5vmin);
transform: translatex(-50%);
top: var(--margin-top);
border-radius: var(--browser-border-radius);
pointer-events: none;
border: solid 5px transparent;
}
&::before {
background-color: var(--color-browser-alt);
z-index: 1;
}
&::after {
box-shadow: var(--shadow-elevation-high);
border-color: var(--color-browser);
z-index: 4;
}
.browser-elements {
z-index: 3;
&::before {
content: "";
position: absolute;
inset: 0;
background-color: var(--color-background);
transform: translatey(-100%);
}
&::after {
content: "";
position: absolute;
inset: 0;
background-color: var(--color-background-fade);
transform: translatey(100%);
}
}
}
.top-bar {
width: var(--browser-top-bar-width);
height: var(--top-bar-height);
background-color: var(--color-browser);
position: absolute;
top: 0;
left: 50%;
transform: translatex(-50%);
border-bottom-right-radius: var(--browser-top-bar-radius);
border-bottom-left-radius: var(--browser-top-bar-radius);
display: flex;
align-items: center;
.points {
display: var(--points-display);
gap: 5px;
margin: 0 5px 3px 5px;
align-items: center;
.point {
display: block;
width: 10px;
height: 10px;
border-radius: 10px;
background-color: var(--color-browser-alt);
}
}
}
.content-spacing {
display: flex;
flex-direction: column;
gap: var(--content-gap);
}
.page {
z-index: 2;
border-radius: 8px;
position: relative;
overflow: hidden;
padding: var(--page-padding);
> * {
z-index: 2;
border-radius: 8px;
}
}
.page-background {
position: absolute;
inset: 0;
background-color: var(--color-page);
z-index: 1;
}
header {
background-color: var(--color-element);
min-height: 100px;
width: 100%;
}
.items {
list-style: none;
padding: 0 var(--list-padding);
margin: 0;
li {
background-color: var(--color-element);
padding: 3vh;
border-radius: 6px;
&.flipping {
display: none;
}
}
}
footer {
width: 100%;
height: var(--footer-face-width);
background-color: currentcolor;
color: white;
display: flex;
align-items: center;
justify-content: var(--footer-face-position);
color: var(--color-footer);
padding-bottom: 20px;
position: relative;
}
.face {
width: var(--footer-face-width);
height: var(--footer-face-width);
}
.loading {
width: 100%;
height: 40px;
padding: 20px 0;
display: flex;
align-items: center;
justify-content: center;
.spinner {
display: block;
width: 40px;
height: 40px;
border: 6px solid var(--color-element);
border-left-color: transparent;
border-radius: 100vmax;
animation: spin 0.7s linear infinite;
}
}
.hand {
position: absolute;
left: var(--footer-hand-position);
top: 0%;
.hand-type {
display: none;
}
&[data-type="cookie"] .hand-cookie { display: block; }
&[data-type="wave"] .hand-wave { display: block; }
}
.hand-wave {
transform-origin: 20% 90%;
animation-timing-function: ease-in-out;
animation: wave 0.4s infinite;
}
@keyframes wave {
0%, 100% { transform: rotate(-20deg) translatex(-20%) }
50% { transform: rotate(15deg) translatex(0%) }
}
@keyframes spin {
100% {transform: rotate(360deg)}
}
View Compiled
console.clear();
gsap.registerPlugin(Flip);
const items = document.querySelector('.items')
let flipping = false;
let count = 0;
// Face bits!
const faceBits = [
'.left-eye',
'.left-pupil',
'.left-upper-lid',
'.left-lower-lid',
'.right-eye',
'.right-pupil',
'.right-upper-lid',
'.right-lower-lid',
'.mouth',
'.teeth'
]
/*
We need to convert all the svg shapes
into paths for the SVGMorph plugin to
work later on.
*/
faceBits.forEach(bit => MorphSVGPlugin.convertToPath(bit))
/*
Setting up a GSAP timeline that we'll
use to animate the morphing footer face.
The animation will be linked to the scroll
position using ScrollTrigger
*/
const scroll = gsap.timeline({
scrollTrigger: {
scrub: 0.5,
trigger: "footer",
start: "bottom bottom",
end: "top 55%",
// markers: true
},
onComplete: () => addMore()
});
/*
We add all the face bits SVGMorph animations
to the scroll animation. Each face bit morphs
through 3 steps. Sad -> looking up -> Happy.
'Sad' is the initial stat of the svg, 'looking up'
states are defined in #face-step-1 and 'happy'
is #face-step-2
*/
for(i=1; i <=2; i++) {
faceBits.forEach(bit => {
scroll.to(`.face ${bit}`, {morphSVG: `#face-step-${i} ${bit}`, duration:1, ease:'none'}, i - 1)
})
}
/*
Adding a little empty animtion at the end
just so we see the happy face for a little
longer
*/
scroll.to('footer', {duration: 0.3}, 2)
/*
This is a little scrollTriggered animation
for the page loading spinner, we want to
appear just at the last second.
*/
gsap.from(".loading", {
scrollTrigger: {
trigger: "footer",
start: "top 55%",
toggleActions: "play none none reset",
// markers: true,
},
scale: 0,
duration: 0.3,
ease: "back.out"
});
/*
The hand waving/cookie animation is
a seperate scrollTrigger animation.
This just animates the hand appearing,
the wave is a CSS animation
*/
gsap.from(".hand", {
scrollTrigger: {
trigger: "footer",
start: "top 57%",
toggleActions: "play none none reverse",
// markers: true,
onEnter: () => {
count++;
document.querySelector('.hand').dataset.type = (count <= 1 || Math.random() < 0.8) ? 'wave' : 'cookie';
}
},
scale: 0.3,
opacity: 0,
duration: 0.4,
y: 200,
ease: "back.out"
});
/*
This function is called at the end of
the face morph scroll animation. It sets
up a FLIP animation for the new page content
and moving the footer down the page.
*/
function addMore() {
if(!flipping) {
flipping = true;
Flip.killFlipsOf('[data-flip]', true)
createItems();
const state = Flip.getState('[data-flip]');
prepForFlip()
ScrollTrigger.refresh();
Flip.from(state, {
duration: 0.7,
ease: "bounce.out",
onComplete: () => {
flipping = false;
if ((window.innerHeight + window.scrollY) >= document.body.scrollHeight) addMore();
},
onEnter: elements => gsap.fromTo(elements, {xPercent: -120, opacity: 1}, {xPercent: 0, opacity: 1, delay: 0.1, duration: .5, stagger: 0.1, ease: "back.out"})
});
}
}
/*
This adds the new elements to the page
*/
function createItems() {
for(let i = 0; i < 4; i++) {
const p = document.createElement("p");
const li = document.createElement("li");
li.dataset.flip = '';
li.className = 'flipping';
li.appendChild(p)
items.appendChild(li)
}
}
/*
This un-hides the new elements after the
FLIP state has been recorded.
*/
function prepForFlip() {
items.querySelectorAll('[data-flip]')
.forEach(item => {
item.classList.remove('flipping')
delete item.dataset.flip
})
}
This Pen doesn't use any external CSS resources.