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:
Frontend
Backend
Database
Tools
- React - Component library
- Next.js - React framework
- jQuery - JavaScript library
- Bootstrap - CSS framework
- Tailwind CSS - Utility-first CSS
- Node.js - JavaScript runtime
- Express - Web framework
- Flask - Python framework
- PostgreSQL - Relational database
- MongoDB - NoSQL database
- MySQL - Relational database
- Docker - Containerization
- Postman - API testing
- npm - Package manager
- Git - Version control
- Figma - Design tool
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;
}
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)
Whether to pause animation on hover
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
- 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"
}