DocsImage Swiper

Image Swiper

Image swiper card for a real estate website

1/4

Batumi, Georgia

5000 Kilometers away

$200 night

Installation

Install Dependencies

npm i clsx tailwind-merge framer-motion @tabler/icons-react

Create @/utils/cn.ts file

import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
    return twMerge(clsx(inputs));
}

create image-swiper.tsx file

'use client'

import { useState } from 'react'
import { motion, useMotionValue } from 'framer-motion'
import { IconChevronLeft, IconChevronRight } from '@tabler/icons-react'
import { cn } from '@/utils/cn'

export const ImageSwiper: React.FC<{ images: string[]; className?: string }> = ({
  images,
  className
}) => {
  const [imgIndex, setImgIndex] = useState(0)

  const dragX = useMotionValue(0)

  const onDragEnd = () => {
    const x = dragX.get()

    if (x <= -10 && imgIndex < images.length - 1) {
      setImgIndex((e) => e + 1)
    } else if (x >= 10 && imgIndex > 0) {
      setImgIndex((e) => e - 1)
    }
  }

  return (
    <div
      className={cn(
        'group/hover relative aspect-square h-full w-full overflow-hidden rounded-lg',
        className
      )}>
      <div className="pointer-events-none absolute top-1/2 z-10 flex w-full -translate-y-1/2 justify-between px-5 ">
        <button
          style={imgIndex === 0 ? { opacity: 0 } : {}}
          className="pointer-events-auto h-fit w-fit rounded-full bg-white/80 p-2 opacity-0 transition-all group-hover/hover:opacity-100"
          onClick={() => {
            if (imgIndex > 0) {
              setImgIndex((pv) => pv - 1)
            }
          }}>
          <IconChevronLeft className="stroke-neutral-600" size={20} />
        </button>
        <button
          style={imgIndex === images.length - 1 ? { opacity: 0 } : {}}
          className="pointer-events-auto h-fit w-fit rounded-full bg-white/80 p-2  opacity-0 transition-all group-hover/hover:opacity-100"
          onClick={() => {
            if (imgIndex < images.length - 1) {
              setImgIndex((pv) => pv + 1)
            }
          }}>
          <IconChevronRight className="stroke-neutral-600" size={20} />
        </button>
      </div>
      <div className="pointer-events-none absolute bottom-2 z-10 flex w-full items-center justify-center">
        <div className="flex w-9 items-center justify-center rounded-md bg-black/80 p-0.5 text-xs text-white opacity-0 transition-all group-hover/hover:opacity-100">
          <div>
            {imgIndex + 1}/{images.length}
          </div>
        </div>
      </div>
      <motion.div
        drag="x"
        dragConstraints={{
          left: 0,
          right: 0
        }}
        dragMomentum={false}
        style={{
          x: dragX
        }}
        animate={{
          translateX: `-${imgIndex * 100}%`
        }}
        onDragEnd={onDragEnd}
        transition={{ damping: 18, stiffness: 90, type: 'spring', duration: 0.2 }}
        className=" flex h-full cursor-grab items-center rounded-[inherit] active:cursor-grabbing">
        {images.map((src, i) => {
          return (
            <motion.div
              key={i}
              className="h-full w-full shrink-0 overflow-hidden bg-neutral-800 object-cover first:rounded-l-[inherit] last:rounded-r-[inherit]">
              <img src={src} className="pointer-events-none h-full w-full object-cover" />
            </motion.div>
          )
        })}
      </motion.div>
    </div>
  )
}