Skip to main content
The Skills component showcases technical proficiencies using an infinite horizontal scrolling carousel of technology icons.

Overview

Location: src/app/components/sections/Skills.tsx This component displays technology logos in a continuous infinite scroll animation, creating an engaging visual showcase of technical skills and tools.

Implementation

export default function Skills() {
  return (
    <div
      className="max-lg:col-span-1 max-lg:row-span-1 col-span-4 row-span-2 col-start-1 row-start-7 bg-spotify-light-dark rounded-xl h-fit"
      id="skills"
    >
      <h1 className="text-2xl font-semibold pl-5 pt-3">Skills</h1>
      <InfiniteMovingCards items={skillItems} speed="slow" className="" />
    </div>
  );
}

Grid Layout

Desktop Layout

.skills {
  grid-column: span 4;      /* Takes 4 columns */
  grid-row: span 2;         /* Takes 2 rows */
  grid-column-start: 1;     /* Starts at column 1 */
  grid-row-start: 7;        /* Starts at row 7 */
}

Mobile Responsive

  • Tablet/Mobile: Collapses to single column and row
    max-lg:col-span-1 max-lg:row-span-1
    

Skill Items Data

Data Structure

const skillItems: { quote: ReactNode; name: string }[] = [
  {
    quote: (
      <Image
        src="https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/react/react-original.svg"
        width={40}
        height={40}
        alt="React"
      />
    ),
    name: "React",
  },
  // Additional skills...
];
The property name “quote” is inherited from the InfiniteMovingCards component’s generic design, but contains icon elements rather than text quotes.

Complete Skill List

The component showcases 16 technologies:
  • React - Component library
  • Next.js - React framework
  • jQuery - JavaScript library
  • Bootstrap - CSS framework
  • Tailwind CSS - Utility-first CSS

Icon Source

All technology icons are loaded from the Devicon CDN:
<Image
  src="https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/{technology}/{technology}-original.svg"
  width={40}
  height={40}
  alt="{Technology Name}"
/>
Using the Devicon CDN ensures consistent, high-quality logos without increasing bundle size.

Infinite Moving Cards Component

Location: src/app/components/ui/infinite-moving-cars.tsx

Props Interface

interface InfiniteMovingCardsProps {
  items: {
    quote: ReactNode;
    name: string;
  }[];
  direction?: "left" | "right";
  speed?: "fast" | "normal" | "slow";
  pauseOnHover?: boolean;
  className?: string;
}
items
array
required
Array of objects containing icon elements and names
direction
'left' | 'right'
default:"left"
Direction of scroll animation
speed
'fast' | 'normal' | 'slow'
default:"fast"
Animation speed: fast (20s), normal (40s), slow (80s)
pauseOnHover
boolean
default:"true"
Whether to pause animation on hover
className
string
Additional CSS classes

Implementation

export const InfiniteMovingCards = ({
  items,
  direction = "left",
  speed = "fast",
  pauseOnHover = true,
  className,
}: InfiniteMovingCardsProps) => {
  const containerRef = React.useRef<HTMLDivElement>(null);
  const scrollerRef = React.useRef<HTMLUListElement>(null);
  const [start, setStart] = useState(false);

  const getDirection = useCallback(() => {
    if (containerRef.current) {
      if (direction === "left") {
        containerRef.current.style.setProperty(
          "--animation-direction",
          "forwards"
        );
      } else {
        containerRef.current.style.setProperty(
          "--animation-direction",
          "reverse"
        );
      }
    }
  }, [direction]);

  const getSpeed = useCallback(() => {
    if (containerRef.current) {
      if (speed === "fast") {
        containerRef.current.style.setProperty("--animation-duration", "20s");
      } else if (speed === "normal") {
        containerRef.current.style.setProperty("--animation-duration", "40s");
      } else {
        containerRef.current.style.setProperty("--animation-duration", "80s");
      }
    }
  }, [speed]);

  const addAnimation = useCallback(() => {
    if (containerRef.current && scrollerRef.current) {
      const scrollerContent = Array.from(scrollerRef.current.children);

      // Duplicate items for seamless loop
      scrollerContent.forEach((item) => {
        const duplicatedItem = item.cloneNode(true);
        if (scrollerRef.current) {
          scrollerRef.current.appendChild(duplicatedItem);
        }
      });

      getDirection();
      getSpeed();
      setStart(true);
    }
  }, [getDirection, getSpeed]);

  useEffect(() => {
    addAnimation();
  }, [addAnimation]);

  return (
    <div
      ref={containerRef}
      className={cn(
        "scroller relative z-0 max-w-7xl overflow-hidden [mask-image:linear-gradient(to_right,transparent,white_20%,white_80%,transparent)]",
        className
      )}
    >
      <ul
        ref={scrollerRef}
        className={cn(
          "flex min-w-full shrink-0 gap-4 py-5 w-max flex-nowrap",
          start && "animate-scroll",
          pauseOnHover && "hover:[animation-play-state:paused]"
        )}
      >
        {items.map((item, idx) => (
          <li
            className="relative rounded-2xl flex-shrink-0 md:w-[100px]"
            key={item.name}
          >
            <blockquote className="flex flex-col items-center">
              <div className="relative z-20 leading-[1.6] w-16">
                {item.quote}
              </div>
            </blockquote>
          </li>
        ))}
      </ul>
    </div>
  );
};

Animation Details

Speed Configuration

The Skills component uses speed="slow" for a relaxed scroll:
switch (speed) {
  case "fast": duration = "20s"; break;
  case "normal": duration = "40s"; break;
  case "slow": duration = "80s"; break;  // Used by Skills
}

Seamless Looping

The component duplicates all items to create a seamless infinite loop:
const addAnimation = () => {
  const scrollerContent = Array.from(scrollerRef.current.children);
  
  // Clone each item and append to create continuous scroll
  scrollerContent.forEach((item) => {
    const duplicatedItem = item.cloneNode(true);
    scrollerRef.current.appendChild(duplicatedItem);
  });
};
Duplicating items ensures the scroll animation loops without any visible gaps or jumps.

Gradient Mask

The component uses a CSS mask for fade-in/fade-out edges:
mask-image: linear-gradient(to right, transparent, white 20%, white 80%, transparent)
This creates a subtle fade effect at both ends of the scroll area.

Pause on Hover

hover:[animation-play-state:paused]
Users can pause the animation by hovering over the component to examine specific technologies.

Styling

Container Styles

/* Main container */
.skills {
  background: var(--spotify-light-dark);  /* #121212 */
  border-radius: 0.75rem;                 /* rounded-xl */
  height: fit-content;                     /* h-fit */
}

/* Header */
.skills h1 {
  font-size: 1.5rem;        /* text-2xl */
  font-weight: 600;         /* font-semibold */
  padding-left: 1.25rem;    /* pl-5 */
  padding-top: 0.75rem;     /* pt-3 */
}

Icon Container

/* Individual skill item */
li {
  border-radius: 1rem;           /* rounded-2xl */
  flex-shrink: 0;
  width: 100px;                  /* md:w-[100px] */
}

/* Icon wrapper */
.icon-wrapper {
  position: relative;
  z-index: 20;
  line-height: 1.6;
  width: 4rem;                   /* w-16 (64px) */
}
Each icon is displayed at 40x40 pixels within a 64px-wide container, providing comfortable spacing.

Usage Example

To add new skills to the showcase:
const skillItems = [
  // Existing skills...
  {
    quote: (
      <Image
        src="https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/kubernetes/kubernetes-plain.svg"
        width={40}
        height={40}
        alt="Kubernetes"
      />
    ),
    name: "Kubernetes",
  },
];
Browse available icons at devicon.dev and follow the CDN pattern: https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/{name}/{name}-{variant}.svg

Performance Considerations

  • Icons load from CDN (not bundled)
  • CSS animations use GPU acceleration
  • useCallback hooks prevent unnecessary re-renders
  • Images use Next.js Image component for optimization

Dependencies

{
  "next/image": "Image optimization",
  "react": "useState, useEffect, useCallback, useRef",
  "../ui/infinite-moving-cars": "Custom infinite scroll component"
}

Build docs developers (and LLMs) love