I recently decided to build a mobile-first roommate finder app as a way to learn the new Next.js 16 App Router.
The hardest part? Building a "Card Stack" that feels like a native app (Tinder-style) without using heavy libraries like framer-motion.
This is how I solved it using just React 19, Tailwind CSS, and good old useState.
Here is the gig!
"Moving" a stack of DOM elements was never a thing. We have been rendering one active card and changing the data behind it all this time!
To do this, we need to track two things in our state:
- The Index: Which item in the array are we looking at?
- The Direction: Is the user swiping Left (Reject) or Right (Like)?
The Code
Here is the simplified logic from my ExplorePage component:
'use client';
import { useState } from 'react';
export default function SwipeStack({ items }) {
// 1. Track which card is active
const [currentIndex, setCurrentIndex] = useState(0);
// 2. Track animation direction ('left' | 'right' | null)
const [swipeDirection, setSwipeDirection] = useState(null);
const handleSwipe = (direction) => {
// Step A: Trigger the animation
setSwipeDirection(direction);
// Step B: Wait for animation to finish, then show next card
setTimeout(() => {
setCurrentIndex((prev) => prev + 1);
setSwipeDirection(null); // Reset animation
}, 300); // Matches CSS transition duration
};
const currentItem = items[currentIndex];
if (!currentItem) return <div>No more profiles!</div>;
return (
<div className="relative w-full max-w-sm h-[500px]">
<div
className={`
transition-all duration-300 ease-out
${swipeDirection === 'left' ? '-translate-x-full -rotate-12 opacity-0' : ''}
${swipeDirection === 'right' ? 'translate-x-full rotate-12 opacity-0' : ''}
`}
>
{/* Your Card Component */}
<Card item={currentItem} />
</div>
{/* Control Buttons */}
<div className="flex gap-4 justify-center mt-8">
<button onClick={() => handleSwipe('left')}>β</button>
<button onClick={() => handleSwipe('right')}>π</button>
</div>
</div>
);
}
Check this out, with the code, you get:
Instant Feedback: When you click "Like", setSwipeDirection('right') adds the Tailwind classes translate-x-full and rotate-12. The card visually flies off the screen.
State Update: The setTimeout waits exactly 300ms (the duration of our CSS transition). Once the card is off-screen, we increment currentIndex.
Reset: React re-renders with the new data at the same position (center), and we remove the animation classes. To the user, it looks like a brand new card appeared behind the old one.
The Result
This creates a buttery smooth 60fps animation on mobile browsers because we are only transforming translate and opacity.
WAIT
I open-sourced the entire UI kit, including the Swipe Logic, Bottom Navigation, and Chat Interface.
You can grab the repo here to see how I handled the array filtering and mobile safe-areas:
π GitHub Repo: Next.js Mobile Marketplace Starter
π Live Demo
Let me know if you have questions about the framework!
Top comments (0)