A react component for Tiktok style swiper

PengPeng
3 min read

See the end result first:

I am building a note taking app, that is Instagram style, free, encrypted. I have a “Today in history“ page, where you can review what happened on the same day of last week / last month / last 3 months / last year etc.

TikTok is popular and the swipe up there is so addictive, so I decided to make my review page TikTok style.

It’s simple, I will just show the code here.

import './TikTokCards.css';

import React, { useRef, useState } from 'react';
import fastMemo from 'react-fast-memo';

export const TikTokCards = fastMemo(({ cards, height = '100vh' }) => {
  const [currentIndex, setCurrentIndex] = useState(0);
  const [startY, setStartY] = useState(0);
  const [isDragging, setIsDragging] = useState(false);
  const [dragOffset, setDragOffset] = useState(0);
  const containerRef = useRef(null);

  const handleStart = clientY => {
    setStartY(clientY);
    setIsDragging(true);
    setDragOffset(0);
  };

  const handleMove = clientY => {
    if (!isDragging) return;
    const diff = startY - clientY;
    setDragOffset(diff);
  };

  const handleEnd = () => {
    setIsDragging(false);
    if (Math.abs(dragOffset) > 100) {
      if (dragOffset > 0 && currentIndex < cards.length - 1) {
        setCurrentIndex(currentIndex + 1);
      } else if (dragOffset < 0 && currentIndex > 0) {
        setCurrentIndex(currentIndex - 1);
      }
    }
    setDragOffset(0);
  };

  // Touch event handlers
  const handleTouchStart = e => handleStart(e.touches[0].clientY);
  const handleTouchMove = e => handleMove(e.touches[0].clientY);
  const handleTouchEnd = () => handleEnd();

  // Mouse event handlers
  const handleMouseDown = e => handleStart(e.clientY);
  const handleMouseMove = e => handleMove(e.clientY);
  const handleMouseUp = () => handleEnd();

  // Wheel event handler for scrolling
  const handleWheel = e => {
    // e.preventDefault();
    if (e.deltaY > 0 && currentIndex < cards.length - 1) {
      setCurrentIndex(currentIndex + 1);
    } else if (e.deltaY < 0 && currentIndex > 0) {
      setCurrentIndex(currentIndex - 1);
    }
  };

  const getCardStyle = index => {
    const offset = (index - currentIndex) * 100 + (isDragging ? -dragOffset / 5 : 0);
    let opacity = 1;

    if (isDragging) {
      if (dragOffset > 0) {
        // Dragging up
        if (index === cards.length - 1) {
          opacity = 1; // Keep first card fully opaque when scrolling up
        } else if (index === currentIndex) {
          opacity = 0.7;
        } else if (index === currentIndex + 1) {
          opacity = 1;
        } else {
          opacity = 0.5;
        }
      } else if (dragOffset < 0) {
        // Dragging down
        if (index === 0) {
          opacity = 1; // Keep last card fully opaque when scrolling down
        } else if (index === currentIndex) {
          opacity = 0.7;
        } else if (index === currentIndex - 1) {
          opacity = 1;
        } else {
          opacity = 0.5;
        }
      }
    } else {
      // Not dragging
      opacity = index === currentIndex ? 1 : 0.5;
    }

    return {
      opacity,
      transform: `translateY(${offset}%)`,
      transition: isDragging ? 'none' : 'transform 200ms ease-in, opacity 500ms ease-in',
    };
  };

  return (
    <div
      className="tiktok-cards-container"
      ref={containerRef}
      onWheel={handleWheel}
      onTouchStart={handleTouchStart}
      onTouchMove={handleTouchMove}
      onTouchEnd={handleTouchEnd}
      onMouseDown={handleMouseDown}
      onMouseMove={handleMouseMove}
      onMouseUp={handleMouseUp}
      onMouseLeave={handleMouseUp}
      style={{ height }}
    >
      {cards.map((card, index) => (
        <div key={index} className="tiktok-card" style={getCardStyle(index)}>
          {card}
        </div>
      ))}
    </div>
  );
});

And the style:

.tiktok-cards-container {
  width: 100%;
  height: 80vh;
  overflow: hidden;
  position: relative;
  cursor: grab;
}

.tiktok-cards-container:active {
  cursor: grabbing;
}

.tiktok-card {
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 24px;
  background-color: var(--color-background);
  user-select: none;
}

It works well on laptop with mouse drag and mouse wheel, and of course on your phone.

Let me know if you have any question, and give my app a try, it’s free and open source: notenote.cc

0
Subscribe to my newsletter

Read articles from Peng directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Peng
Peng