Naming issues is difficult, proper? Properly, “Flippy Snaps” was the very best factor I might provide you with. 😂 I noticed an impact like this on TV one night and made a be aware to myself to make one thing comparable.
Though this isn’t one thing I’d look to drop on an internet site any time quickly, it’s a neat little problem to make. It suits in with my complete stance on “Playfulness in Code” to be taught. Anyway, a couple of days later, I sat down on the keyboard, and a few hours later I had this:
3D CSS Flippy Snaps ✨
Faucet to flip for one more picture 👇
⚒️ @reactjs && @greensock
👉 https://t.co/Na14z40tHE through @CodePen pic.twitter.com/nz6pdQGpmd
— Jhey 🐻🛠️✨ (@jh3yy) November 8, 2021
My last demo is a React app, however we don’t have to dig into utilizing React to elucidate the mechanics of constructing this work. We’ll create the React app as soon as we’ve established the best way to make issues work.
Observe: Earlier than we get began. It’s value noting that the efficiency of this demo is affected by the grid measurement and the demos are finest considered in Chromium-based browsers.
Let’s begin by making a grid. Let’s say we would like a ten by 10 grid. That’s 100 cells (For this reason React is useful for one thing like this). Every cell goes to include a component that accommodates the back and front for a flippable card.
<div class=”flippy-snap”>
<!– 100 of those –>
<div class=”flippy-snap__card flippy-card”>
<div class=”flippy-card__front></div>
<div class=”flippy-card__rear></div>
</div>
</div>
The types for our grid are fairly simple. We are able to use show: grid and use a customized property for the grid measurement. Right here we’re defaulting to 10.
.flippy-snap {
show: grid;
grid-gap: 1px;
grid-template-columns: repeat(var(–grid-size, 10), 1fr);
grid-template-rows: repeat(var(–grid-size, 10), 1fr);
}
We received’t use grid-gap within the last demo, however, it’s good for seeing the cells simpler while creating.
See the Pen 1. Making a Grid by JHEY
Subsequent, we have to model the perimeters of our playing cards and show pictures. We are able to do that by leveraging inline CSS customized properties. Let’s begin by updating the markup. We want every card to know its x and y place within the grid.
<div class=”flippy-snap”>
<div class=”flippy-snap__card flippy-card” model=”–x: 0; –y: 0;”>
<div class=”flippy-card__front”></div>
<div class=”flippy-card__rear”></div>
</div>
<div class=”flippy-snap__card flippy-card” model=”–x: 1; –y: 0;”>
<div class=”flippy-card__front”></div>
<div class=”flippy-card__rear”></div>
</div>
<!– Different playing cards –>
</div>
For the demo, I am utilizing Pug to generate this for me. You’ll be able to see the compiled HTML by clicking “View Compiled HTML” within the demo.
– const GRID_SIZE = 10
– const COUNT = Math.pow(GRID_SIZE, 2)
.flippy-snap
– for(let f = 0; f < COUNT; f++)
– const x = f % GRID_SIZE
– const y = Math.flooring(f / GRID_SIZE)
.flippy-snap__card.flippy-card(model=`–x: ${x}; –y: ${y};`)
.flippy-card__front
.flippy-card__rear
Then we’d like some types.
.flippy-card {
–current-image: url(“https://random-image.com/768”);
–next-image: url(“https://random-image.com/124”);
top: 100%;
width: 100%;
place: relative;
}
.flippy-card__front,
.flippy-card__rear {
place: absolute;
top: 100%;
width: 100%;
backface-visibility: hidden;
background-image: var(–current-image);
background-position: calc(var(–x, 0) * -100%) calc(var(–y, 0) * -100%);
background-size: calc(var(–grid-size, 10) * 100%);
}
.flippy-card__rear {
background-image: var(–next-image);
rework: rotateY(180deg) rotate(180deg);
}
The rear of the cardboard will get its place utilizing a mixture of rotations through rework. However, the fascinating half is how we present the picture half for every card. On this demo, we’re utilizing a customized property to outline the URLs for 2 pictures. After which we set these because the background-image for every card face.
However the trick is how we outline the background-size and background-position. Utilizing the customized properties –x and –y we multiply the worth by -100%. After which we set the background-size to –grid-size multiplied by 100%. This provides shows the proper a part of the picture for a given card.
See the Pen 2. Including an Picture by JHEY
You might have seen that we had –current-image and –next-image. However, at the moment, there is no such thing as a option to see the subsequent picture. For that, we’d like a option to flip our playing cards. We are able to use one other customized property for this.
Let’s introduce a –count property and set a rework for our playing cards:
.flippy-snap {
–count: 0;
perspective: 50vmin;
}
.flippy-card {
rework: rotateX(calc(var(–count) * -180deg));
transition: rework 0.25s;
transform-style: preserve-3d;
}
We are able to set the –count property on the containing aspect. Scoping means all of the playing cards can choose up that worth and use it to remodel their rotation on the x-axis. We additionally have to set transform-style: preserve-3d in order that we see the again of the playing cards. Setting a perspective provides us that 3D perspective.
This demo allows you to replace the –count property worth so you may see the impact it has.
See the Pen 3. Turning Playing cards by JHEY
At this level, you could possibly wrap it up there and set a easy click on handler that increments –count by one on every click on.
const SNAP = doc.querySelector(‘.flippy-snap’)
let rely = 0
const UPDATE = () => SNAP.model.setProperty(‘–count’, rely++)
SNAP.addEventListener(‘click on’, UPDATE)
Take away the grid-gap and also you’d get this. Click on the snap to flip it.
See the Pen 4. Boring Flips by JHEY
Now now we have the essential mechanics labored out, it’s time to show this right into a React app. There’s a bit to interrupt down right here.
const App = () => {
const [snaps, setSnaps] = useState([])
const [disabled, setDisabled] = useState(true)
const [gridSize, setGridSize] = useState(9)
const snapRef = useRef(null)
const grabPic = async () => {
const pic = await fetch(‘https://supply.unsplash.com/random/1000×1000’)
return pic.url
}
useEffect(() => {
const setup = async () => {
const url = await grabPic()
const nextUrl = await grabPic()
setSnaps([url, nextUrl])
setDisabled(false)
}
setup()
}, [])
const setNewImage = async rely => {
const newSnap = await grabPic()
setSnaps(
rely.present % 2 !== 0 ? [newSnap, snaps[1]] : [snaps[0], newSnap]
)
setDisabled(false)
}
const onFlip = async rely => {
setDisabled(true)
setNewImage(rely)
}
if (snaps.size !== 2) return <h1 className=”loader”>Loading…</h1>
return (
<FlippySnap
gridSize={gridSize}
disabled={disabled}
snaps={snaps}
onFlip={onFlip}
snapRef={snapRef}
/>
)
}
Our App element handles grabbing pictures and passing them to our FlippySnap element. That’s the majority of what’s taking place right here. For this demo, we’re grabbing pictures from Unsplash.
const grabPic = async () => {
const pic = await fetch(‘https://supply.unsplash.com/random/1000×1000’)
return pic.url
}
// Preliminary impact grabs two snaps for use by FlippySnap
useEffect(() => {
const setup = async () => {
const url = await grabPic()
const nextUrl = await grabPic()
setSnaps([url, nextUrl])
setDisabled(false)
}
setup()
}, [])
If there aren’t two snaps to point out, then we present a “Loading…” message.
if (snaps.size !== 2) return <h1 className=”loader”>Loading…</h1>
If we’re grabbing a brand new picture, we have to disable FlippySnap so we will’t spam-click it.
<FlippySnap
gridSize={gridSize}
disabled={disabled} // Toggle a “disabled” prop to cease spam clicks
snaps={snaps}
onFlip={onFlip}
snapRef={snapRef}
/>
We’re letting App dictate the snaps that get displayed by FlippySnap and during which order. On every flip, we seize a brand new picture, and relying on what number of instances we’ve flipped, we set the proper snaps. The choice can be to set the snaps and let the element determine the order.
const setNewImage = async rely => {
const newSnap = await grabPic() // Seize the snap
setSnaps(
rely.present % 2 !== 0 ? [newSnap, snaps[1]] : [snaps[0], newSnap]
) // Set the snaps primarily based on the present “rely” which we get from FlippySnap
setDisabled(false) // Allow clicks once more
}
const onFlip = async rely => {
setDisabled(true) // Disable so we will not spam click on
setNewImage(rely) // Seize a brand new snap to show
}
How would possibly FlippySnap look? There isn’t a lot to it in any respect!
const FlippySnap = ({ disabled, gridSize, onFlip, snaps }) => {
const CELL_COUNT = Math.pow(gridSize, 2)
const rely = useRef(0)
const flip = e => {
if (disabled) return
rely.present = rely.present + 1
if (onFlip) onFlip(rely)
}
return (
<button
className=”flippy-snap”
ref={containerRef}
model={{
‘–grid-size’: gridSize,
‘–count’: rely.present,
‘–current-image’: `url(‘${snaps[0]}’)`,
‘–next-image’: `url(‘${snaps[1]}’)`,
}}
onClick={flip}>
{new Array(CELL_COUNT).fill().map((cell, index) => {
const x = index % gridSize
const y = Math.flooring(index / gridSize)
return (
<span
key={index}
className=”flippy-card”
model={{
‘–x’: x,
‘–y’: y,
}}>
<span className=”flippy-card__front”></span>
<span className=”flippy-card__rear”></span>
</span>
)
})}
</button>
)
}
The element handles rendering all of the playing cards and setting the inline customized properties. The onClick handler for the container increments the rely. It additionally triggers the onFlip callback. If the state is at the moment disabled, it does nothing. That flip of the disabled state and grabbing a brand new snap triggers the flip when the element re-renders.
See the Pen 5. React Basis by JHEY
We have now a React element that may now flip by pictures for so long as we need to hold requesting new ones. However, that flip transition is a bit boring. To spice it up, we’re going to utilize GreenSock and its utilities. Specifically, the “distribute” utility. It will permit us to distribute the delay of flipping our playing cards in a grid-like burst from wherever we click on. To do that, we’re going to make use of GreenSock to animate the –count worth on every card.
It’s value noting that now we have a selection right here. We might choose to use the types with GreenSock. As an alternative of animating the –count property worth, we might animate rotateX. We might do that primarily based on the rely ref now we have. And this additionally goes for some other issues we select to animate with GreenSock on this article. It’s right down to desire and use case. Chances are you’ll really feel that updating the customized property worth is sensible. The profit being that you simply don’t have to replace any JavaScript to get a special styled conduct. We might change the CSS to make use of rotateY for instance.
Our up to date flip operate might appear like this:
if (disabled) return
const x = parseInt(e.goal.parentNode.getAttribute(‘data-snap-x’), 10)
const y = parseInt(e.goal.parentNode.getAttribute(‘data-snap-y’), 10)
rely.present = rely.present + 1
gsap.to(containerRef.present.querySelectorAll(‘.flippy-card’), {
‘–count’: rely.present,
delay: gsap.utils.distribute({
from: [x / gridSize, y / gridSize],
quantity: gridSize / 20,
base: 0,
grid: [gridSize, gridSize],
ease: ‘power1.inOut’,
}),
period: 0.2,
onComplete: () => {
// At this level replace the pictures
if (onFlip) onFlip(rely)
},
})
}
Observe how we’re getting an x and y worth by studying attributes of the clicked card. For this demo, we’ve opted for including some information attributes to every card. These attributes talk a card’s place within the grid. We’re additionally utilizing a brand new ref known as containerRef. That is so we reference solely the playing cards for a FlippySnap occasion when utilizing GreenSock.
{new Array(CELL_COUNT).fill().map((cell, index) => {
const x = index % gridSize
const y = Math.flooring(index / gridSize)
return (
<span
className=”flippy-card”
data-snap-x={x}
data-snap-y={y}
model={{
‘–x’: x,
‘–y’: y,
}}>
<span className=”flippy-card__front”></span>
<span className=”flippy-card__rear”></span>
</span>
)
})}
As soon as we get these x and y values, we will make use of them in our animation. Utilizing gsap.to we need to animate the –count customized property for each .flippy-card that’s a baby of containerRef.
To distribute the delay from the place we click on, we set the worth of delay to make use of gsap.utils.distribute. The from worth of the distribute operate takes an Array containing ratios alongside the x and y axis. To get this, we divide x and y by gridSize. The bottom worth is the preliminary worth. For this, we would like 0 delay on the cardboard we click on. The quantity is the most important worth. We have gone for gridSize / 20 however you could possibly experiment with completely different values. One thing primarily based on the gridSize is a good suggestion although. The grid worth tells GreenSock the grid measurement to make use of when calculating distribution. Final however not least, the benefit defines the benefit of the delay distribution.
gsap.to(containerRef.present.querySelectorAll(‘.flippy-card’), {
‘–count’: rely.present,
delay: gsap.utils.distribute({
from: [x / gridSize, y / gridSize],
quantity: gridSize / 20,
base: 0,
grid: [gridSize, gridSize],
ease: ‘power1.inOut’,
}),
period: 0.2,
onComplete: () => {
// At this level replace the pictures
if (onFlip) onFlip(rely)
},
})
As for the remainder of the animation, we’re utilizing a flip period of 0.2 seconds. And we make use of onComplete to invoke our callback. We move the flip rely to the callback so it may well use this to find out snap order. Issues just like the period of the flip might get configured by passing in several props if we wished.
Placing all of it collectively provides us this:
See the Pen 6. Distributed Flips with GSAP by JHEY
Those who prefer to push issues a bit might need seen that we will nonetheless “spam” click on the snap. And that’s as a result of we don’t disable FlippySnap till GreenSock has accomplished. To repair this, we will use an inner ref that we toggle firstly and finish of utilizing GreenSock.
const flip = e => {
if (disabled || flipping.present) return
const x = parseInt(e.goal.parentNode.getAttribute(‘data-snap-x’), 10)
const y = parseInt(e.goal.parentNode.getAttribute(‘data-snap-y’), 10)
rely.present = rely.present + 1
gsap.to(containerRef.present.querySelectorAll(‘.flippy-card’), {
‘–count’: rely.present,
delay: gsap.utils.distribute({
from: [x / gridSize, y / gridSize],
quantity: gridSize / 20,
base: 0,
grid: [gridSize, gridSize],
ease: ‘power1.inOut’,
}),
period: 0.2,
onStart: () => {
flipping.present = true
},
onComplete: () => {
// At this level replace the pictures
flipping.present = false
if (onFlip) onFlip(rely)
},
})
}
And now we will now not spam click on our FlippySnap!
See the Pen 7. No Spam Clicks by JHEY
Now it’s time for some additional touches. In the meanwhile, there’s no visible signal that we will click on our FlippySnap. What if once we hover, the playing cards increase in the direction of us? We might use onPointerOver and use the “distribute” utility once more.
const point out = e => {
const x = parseInt(e.currentTarget.getAttribute(‘data-snap-x’), 10)
const y = parseInt(e.currentTarget.getAttribute(‘data-snap-y’), 10)
gsap.to(containerRef.present.querySelectorAll(‘.flippy-card’), {
‘–hovered’: gsap.utils.distribute({
from: [x / gridSize, y / gridSize],
base: 0,
quantity: 1,
grid: [gridSize, gridSize],
ease: ‘power1.inOut’
}),
period: 0.1,
})
}
Right here, we’re setting a brand new customized property on every card named –hovered. That is set to a price from 0 to 1. Then inside our CSS, we’re going to replace our card types to look at for the worth.
.flippy-card {
rework: translate3d(0, 0, calc((1 – (var(–hovered, 1))) * 5vmin))
rotateX(calc(var(–count) * -180deg));
}
Right here we’re saying {that a} card will transfer on the z-axis at most 5vmin.
We then apply this to every card utilizing the onPointerOver prop.
{new Array(CELL_COUNT).fill().map((cell, index) => {
const x = index % gridSize
const y = Math.flooring(index / gridSize)
return (
<span
onPointerOver={point out}
className=”flippy-card”
data-snap-x={x}
data-snap-y={y}
model={{
‘–x’: x,
‘–y’: y,
}}>
<span className=”flippy-card__front”></span>
<span className=”flippy-card__rear”></span>
</span>
)
})}
And when our pointer leaves our FlippySnap we need to reset our card positions.
const reset = () => {
gsap.to(containerRef.present.querySelectorAll(‘.flippy-card’), {
‘–hovered’: 1,
period: 0.1,
})
}
And we will apply this with the onPointerLeave prop.
<button
className=”flippy-snap”
ref={containerRef}
onPointerLeave={reset}
model={{
‘–grid-size’: gridSize,
‘–count’: rely.present,
‘–current-image’: `url(‘${snaps[0]}’)`,
‘–next-image’: `url(‘${snaps[1]}’)`,
}}
onClick={flip}>
Put that every one collectively and we get one thing like this. Attempt shifting your pointer over it.
See the Pen 8. Visible Inidication with Raised Playing cards by JHEY
What subsequent? How a couple of loading indicator so we all know when our App is grabbing the subsequent picture? We are able to render a loading spinner when our FlippySnap is disabled.
{disabled && <span className=’flippy-snap__loader’></span>}
He types for which might make a rotating circle.
.flippy-snap__loader {
border-radius: 50%;
border: 6px stable #fff;
border-left-color: #000;
border-right-color: #000;
place: absolute;
proper: 10%;
backside: 10%;
top: 8%;
width: 8%;
rework: translate3d(0, 0, 5vmin) rotate(0deg);
animation: spin 1s infinite;
}
@keyframes spin {
to {
rework: translate3d(0, 0, 5vmin) rotate(360deg);
}
}
And this offers us a loading indicator when grabbing a brand new picture.
See the Pen 9. Add Loading Indicator by JHEY
That’s it!
That’s how we will create a FlippySnap with React and GreenSock. It’s enjoyable to make issues that we might not create on a day-to-day foundation. Demos like this may pose completely different challenges and might stage up your problem-solving sport.
I took it slightly additional and added a slight parallax impact together with some audio. It’s also possible to configure the grid measurement! (Huge grids have an effect on efficiency although.)
See the Pen 3D CSS Flippy Snaps v2 (React && GSAP) by JHEY
It’s value noting that this demo works finest in Chromium-based browsers.
So, the place would you are taking it subsequent? I’d prefer to see if I can recreate it with Three.js subsequent. That may handle the efficiency. 😅
Keep Superior! ʕ•ᴥ•ʔ
Subscribe to MarketingSolution.
Receive web development discounts & web design tutorials.
Now! Lets GROW Together!