<!--
1. useReducer
2. offset
3. tilt
-->
<div id="app"></div>
<a href="https://youtu.be/5ptXXNjuUfg" target="_blank" data-keyframers-credit style="color: #FFF"></a>
<script src="https://codepen.io/shshaw/pen/QmZYMG.js"></script>
*,
*::before,
*::after {
box-sizing: border-box;
position: relative;
}
html,
body {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
font-size: 3vmin;
}
html {
background: #151515;
color: #fff;
overflow: hidden;
}
body {
display: flex;
justify-content: center;
align-items: center;
}
.slides {
display: grid;
> .slide {
grid-area: 1 / -1;
}
> button {
appearance: none;
background: transparent;
border: none;
color: white;
position: absolute;
font-size: 5rem;
width: 5rem;
height: 5rem;
top: 30%;
transition: opacity 0.3s;
opacity: 0.7;
z-index: 5;
&:hover {
opacity: 1;
}
&:focus {
outline: none;
}
&:first-child {
left: -50%;
}
&:last-child {
right: -50%;
}
}
}
.slide {
//transform-style: preserve-3d;
// border: solid 1px red;
// &[data-active] {
// .slideContent > * {
// transform: none;
// opacity: 1;
// }
// }
}
.slideContent {
width: 30vw;
height: 40vw;
background-size: cover;
background-position: center center;
background-repeat: no-repeat;
transition: transform 0.5s ease-in-out;
opacity: 0.7;
display: grid;
align-content: center;
transform-style: preserve-3d;
transform: perspective(1000px) translateX(calc(100% * var(--offset)))
rotateY(calc(-45deg * var(--dir)));
}
.slideContentInner {
transform-style: preserve-3d;
transform: translateZ(2rem);
transition: opacity 0.3s linear;
text-shadow: 0 0.1rem 1rem #000;
opacity: 0;
.slideSubtitle,
.slideTitle {
font-size: 2rem;
font-weight: normal;
letter-spacing: 0.2ch;
text-transform: uppercase;
margin: 0;
}
.slideSubtitle::before {
content: "— ";
}
.slideDescription {
margin: 0;
font-size: 0.8rem;
letter-spacing: 0.2ch;
}
}
.slideBackground {
position: fixed;
top: 0;
left: -10%;
right: -10%;
bottom: 0;
background-size: cover;
background-position: center center;
z-index: -1;
opacity: 0;
transition: opacity 0.3s linear, transform 0.3s ease-in-out;
pointer-events: none;
transform: translateX(calc(10% * var(--dir)));
}
.slide[data-active] {
z-index: 2;
pointer-events: auto;
.slideBackground {
opacity: 0.2;
transform: none;
}
.slideContentInner {
opacity: 1;
}
.slideContent {
--x: calc(var(--px) - 0.5);
--y: calc(var(--py) - 0.5);
opacity: 1;
transform: perspective(1000px);
&:hover {
transition: none;
transform: perspective(1000px) rotateY(calc(var(--x) * 45deg))
rotateX(calc(var(--y) * -45deg));
}
}
}
View Compiled
console.clear();
const slides = [
{
title: "Machu Picchu",
subtitle: "Peru",
description: "Adventure is never far away",
image:
"https://images.unsplash.com/photo-1571771019784-3ff35f4f4277?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=800&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ"
},
{
title: "Chamonix",
subtitle: "France",
description: "Let your dreams come true",
image:
"https://images.unsplash.com/photo-1581836499506-4a660b39478a?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=800&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ"
},
{
title: "Mimisa Rocks",
subtitle: "Australia",
description: "A piece of heaven",
image:
"https://images.unsplash.com/photo-1566522650166-bd8b3e3a2b4b?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=800&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ"
},
{
title: "Four",
subtitle: "Australia",
description: "A piece of heaven",
image:
"https://images.unsplash.com/flagged/photo-1564918031455-72f4e35ba7a6?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=800&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ"
},
{
title: "Five",
subtitle: "Australia",
description: "A piece of heaven",
image:
"https://images.unsplash.com/photo-1579130781921-76e18892b57b?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=800&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ"
}
];
function useTilt(active) {
const ref = React.useRef(null);
React.useEffect(() => {
if (!ref.current || !active) {
return;
}
const state = {
rect: undefined,
mouseX: undefined,
mouseY: undefined
};
let el = ref.current;
const handleMouseMove = (e) => {
if (!el) {
return;
}
if (!state.rect) {
state.rect = el.getBoundingClientRect();
}
state.mouseX = e.clientX;
state.mouseY = e.clientY;
const px = (state.mouseX - state.rect.left) / state.rect.width;
const py = (state.mouseY - state.rect.top) / state.rect.height;
el.style.setProperty("--px", px);
el.style.setProperty("--py", py);
};
el.addEventListener("mousemove", handleMouseMove);
return () => {
el.removeEventListener("mousemove", handleMouseMove);
};
}, [active]);
return ref;
}
const initialState = {
slideIndex: 0
};
const slidesReducer = (state, event) => {
if (event.type === "NEXT") {
return {
...state,
slideIndex: (state.slideIndex + 1) % slides.length
};
}
if (event.type === "PREV") {
return {
...state,
slideIndex:
state.slideIndex === 0 ? slides.length - 1 : state.slideIndex - 1
};
}
};
function Slide({ slide, offset }) {
const active = offset === 0 ? true : null;
const ref = useTilt(active);
return (
<div
ref={ref}
className="slide"
data-active={active}
style={{
"--offset": offset,
"--dir": offset === 0 ? 0 : offset > 0 ? 1 : -1
}}
>
<div
className="slideBackground"
style={{
backgroundImage: `url('${slide.image}')`
}}
/>
<div
className="slideContent"
style={{
backgroundImage: `url('${slide.image}')`
}}
>
<div className="slideContentInner">
<h2 className="slideTitle">{slide.title}</h2>
<h3 className="slideSubtitle">{slide.subtitle}</h3>
<p className="slideDescription">{slide.description}</p>
</div>
</div>
</div>
);
}
function App() {
const [state, dispatch] = React.useReducer(slidesReducer, initialState);
return (
<div className="slides">
<button onClick={() => dispatch({ type: "PREV" })}>‹</button>
{[...slides, ...slides, ...slides].map((slide, i) => {
let offset = slides.length + (state.slideIndex - i);
return <Slide slide={slide} offset={offset} key={i} />;
})}
<button onClick={() => dispatch({ type: "NEXT" })}>›</button>
</div>
);
}
const elApp = document.getElementById("app");
ReactDOM.render(<App />, elApp);
View Compiled
This Pen doesn't use any external CSS resources.