—>ACCESS THE PROJECT HERE<—

So, you know how sometimes an idea just pops into your head and you have to make it? That’s pretty much the story behind this “Love Notes” code I’ve been tinkering with. I was playing around with some animation libraries, specifically GSAP (which is awesome, by the way – more on that later!), and thought, “What if I could make some kind of digital, interactive stack of sweet messages?” Kinda like those little affirmation cards, but, you know, for romance! Or just a bit of daily sweetness.

I’m a total sucker for a good aesthetic, so before I even got too deep into the JavaScript wizardry, I started thinking about the vibe.

Setting the Mood: The “Moonlit” Theme & Design

I wanted something that felt cozy, a bit dreamy, and modern. That’s where the whole “Moonlit” theme came from. You’ll see it in the CSS variables: –gradient-start-moonlit, –primary-accent-moonlit, etc. I imagined something you’d look at on a quiet evening, so I went with a dark, gradient background – not just plain black, but a subtle shift from a darker to a slightly lighter charcoal. The primary accent, that var(–primary-accent-moonlit) blue (#7aa2f7), was chosen to pop against the dark background without being too harsh. It’s for focus states, little highlights, and that cute arrow in the swipe guide.

For fonts, I’m a big believer that they set so much of the tone. I picked ‘Poppins’ for the general UI text – it’s clean, modern, and super readable. But for the quotes themselves, the actual “love notes,” I wanted something a bit more classic and elegant. That’s why I brought in ‘Lora’. It’s a serif font that just feels a bit more… well, romantic and story-like, perfect for “whispers of affection,” as the subtitle says.

The cards themselves needed to feel tangible, like you could almost pick them up. Rounded corners, a soft white background, and subtle box shadows give them that feel. And when you press down on a card? I made the shadow a bit more pronounced and added a slight glow (–card-glow-moonlit) to make it feel more responsive. It’s these little details that I think make a big difference.

Oh, and responsiveness! It absolutely had to look good on phones. Most people would probably use something like this on their mobile, right? So, I spent a good chunk of time with media queries, adjusting font sizes, card sizes, and even hiding the footer or swipe guide on really small landscape screens where space is super tight. You’ll notice in the CSS how the card-container height and quote-text font sizes use clamp() and vh units to be super flexible.

The Heart of It: Swipeable Cards & GSAP Magic

Alright, let’s talk about the fun part: making those cards dance! The core idea was a stack of cards, where you swipe one away to reveal the next. This immediately screamed “GSAP!” to me. If you haven’t used GreenSock Animation Platform, you’re missing out. It makes complex animations so much more manageable and performant than trying to wrestle with CSS transitions for everything, especially when physics-like interactions are involved.

You’ll see a CARD_STATES object in the JavaScript. This was my way of defining how each card in the stack should look – the active card right at the front, then the ones receding behind it (deck1, deck2, deck3), each slightly smaller, lower, and further back in Z-space. gsap.to() and gsap.set() are used everywhere to transition cards between these states and their initial offscreen position.

The swipe interaction itself was a journey!

Listeners: I attached mousedown and touchstart event listeners to the active card.

Tracking: When you start dragging, I record the start coordinates (startX, startY). As you move (mousemove or touchmove), I calculate the difference (diffX, diffY).

Visual Feedback: The card moves with your finger/mouse. I even made it rotate slightly based on how far you drag it horizontally (rotationFactor * MAX_DRAG_ROTATION). It just feels more natural. And the quickSetX, quickSetY, quickSetRotZ functions from GSAP are super efficient for updating these properties on every move event.

The Decisive Moment: When you let go (mouseup or touchend), I check if you’ve dragged it far enough (that’s SWIPE_THRESHOLD_X_FACTOR). If yes, whoosh! The card animates off-screen using EASE_CARD_EXIT, a new card is brought in from the “deck,” and the whole stack re-organizes. If you didn’t swipe far enough, it snaps back to its original position with a nice EASE_SNAP_BACK.

I also added keyboard navigation (left and right arrow keys) because accessibility matters, and sometimes it’s just nicer to use keys. You’ll see handleKeyboardInteraction and programmaticKeyboardSwipe cover that.

The Brains: Quotes, States, and Keeping it Fresh

Behind the scenes, there’s an ORIGINAL_QUOTES array. When the page loads, these get shuffled into an availableQuotesPool. Each time a new card is needed, we pop a quote from this pool. If the pool runs dry, we reshuffle the original list and start again. This way, you get variety and don’t see the same few quotes over and over too quickly.

The quotesDataForCards array keeps track of which quote is currently assigned to which of the four card slots in the DOM. Only the visible cards actually have quote data loaded into them to keep things efficient. When a card is swiped away, its quote data is nulled out, and the next card in line gets a new quote.

Little Touches That I Think Are Neat

The Swipe Guide: You know, that little “Swipe for another whisper ✧” thing? Initially, I had it fixed at the bottom, but it felt a bit disconnected. So, I moved it directly below the card stack ( in the HTML). It now feels more integrated. It also only shows up initially if you haven’t dismissed it before (thanks, localStorage! You’ll see the SWIPEGUIDE_STORAGE_KEY got an update to _v2.1 – little version markers help me keep track if I change how a feature works). If you’re idle for a bit, it gently fades in to remind you there’s more to see, with a subtle pulsing arrow animation. That arrow animation (swipeGuideArrowTimeline) is a separate little GSAP timeline.

Haptic Feedback: On mobile, when you successfully swipe a card, navigator.vibrate(10) gives a tiny buzz. It’s a small thing, but it adds to the tactile feel.

Accessibility: Things like aria-live=“polite” on the quote text mean that screen readers will announce the new quote when it appears. The cards get tabindex=“0” when active so they can be focused and interacted with via keyboard.

CSS Variables for Durations: Instead of hardcoding animation speeds in JavaScript, I defined them in CSS (–card-enter-duration-css, etc.) and then read them into JS. This makes it easier to tweak timing from one central place in the CSS if I want to change the overall feel.

Was It All Sunshine and Rainbows?

Haha, not entirely! Getting the swipe physics to feel just right took a lot of trial and error. There were moments when cards would fly off in weird directions or not snap back properly. Debugging the state management for which card was active and which quote it should display also had its “aha!” (or “argh!”) moments. And making sure all the different animations (card enter, card exit, text fade, swipe guide) played nicely together without tripping over each other needed careful sequencing and use of GSAP’s callbacks.

For instance, ensuring that interaction listeners were only on the active card and were properly removed and re-added during the card cycle was crucial to prevent weirdness. And the isInteracting flag is super important to stop multiple actions from firing if, say, a mouseup happens right after a touch end.

Wrapping It Up

Overall, I’m pretty chuffed with how this “Love Notes” project turned out. It started as a small idea to play with animations and ended up being a rather polished little web app. It was a fantastic learning experience, especially with fine-tuning animations with GSAP, managing state, and thinking about all those little user experience details. It’s the kind of project I really enjoy – a blend of creative design and interesting technical challenges.

Hope you liked this little peek behind the curtain! Let me know if you have any questions or if there’s a feature you think would be cool to add!