Skip to content

Instantly share code, notes, and snippets.

@Pp3ng
Created May 30, 2025 18:41
Show Gist options
  • Select an option

  • Save Pp3ng/45dcedd4b33e81bb14624df850b75604 to your computer and use it in GitHub Desktop.

Select an option

Save Pp3ng/45dcedd4b33e81bb14624df850b75604 to your computer and use it in GitHub Desktop.
A React component that shows animated, scrolling tech stack icons
import React from "react";
import {
SiC,
SiCplusplus,
SiNvidia,
SiGnubash,
SiTypescript,
SiDebian,
SiFreebsd,
SiDocker,
SiGit,
SiVim,
SiTmux,
SiGnu,
SiMysql,
SiRedis,
SiMongodb,
SiNodedotjs,
SiReact,
SiTailwindcss,
} from "@icons-pack/react-simple-icons";
import { ZapIcon } from "lucide-react";
const cn = (...classes: (string | undefined)[]) => {
return classes.filter(Boolean).join(" ");
};
const range = (n: number) => Array.from({ length: n }, (_, i) => i);
type TechStackIcon = {
Icon: React.ComponentType<{ className?: string }>;
name: string;
};
type MarqueeProps = {
children: React.ReactNode;
gap?: string;
direction?: "left" | "up";
pauseOnHover?: boolean;
reverse?: boolean;
fade?: boolean;
className?: string;
};
type StacksCardProps = {
title?: string;
firstRowIcons?: TechStackIcon[];
secondRowIcons?: TechStackIcon[];
className?: string;
};
const Marquee = (props: MarqueeProps) => {
const {
children,
gap = "1rem",
direction = "left",
pauseOnHover = false,
reverse = false,
fade = false,
className,
} = props;
const maskDirection = direction === "left" ? "to right" : "to bottom";
const mask = fade
? `linear-gradient(${maskDirection}, transparent 0%, rgba(0, 0, 0, 1.0) 10%, rgba(0, 0, 0, 1.0) 90%, transparent 100%)`
: undefined;
return (
<div
className={cn(
"group flex overflow-hidden",
direction === "left" ? "flex-row" : "flex-col",
className
)}
style={{
maskImage: mask,
WebkitMaskImage: mask,
gap,
}}
>
{range(2).map((n) => (
<div
key={n}
style={
{
"--gap": gap,
animation:
direction === "left"
? reverse
? "marquee-left 30s linear infinite reverse"
: "marquee-left 30s linear infinite"
: reverse
? "marquee-up 30s linear infinite reverse"
: "marquee-up 30s linear infinite",
} as React.CSSProperties
}
className={cn(
"flex shrink-0 justify-around gap-[var(--gap)]",
direction === "left" ? "flex-row" : "flex-col",
pauseOnHover ? "group-hover:[animation-play-state:paused]" : ""
)}
>
{children}
</div>
))}
</div>
);
};
const StacksCard = ({
title = "Tech Stack",
firstRowIcons = [
{ Icon: SiC, name: "C" },
{ Icon: SiCplusplus, name: "C++" },
{ Icon: SiNvidia, name: "NVIDIA" },
{ Icon: SiGnubash, name: "Bash" },
{ Icon: SiRedis, name: "Redis" },
{ Icon: SiDebian, name: "Debian" },
{ Icon: SiFreebsd, name: "FreeBSD" },
{ Icon: SiVim, name: "Vim" },
{ Icon: SiGnu, name: "GNU" },
],
secondRowIcons = [
{ Icon: SiTmux, name: "Tmux" },
{ Icon: SiDocker, name: "Docker" },
{ Icon: SiGit, name: "Git" },
{ Icon: SiMysql, name: "MySQL" },
{ Icon: SiTypescript, name: "TypeScript" },
{ Icon: SiMongodb, name: "MongoDB" },
{ Icon: SiNodedotjs, name: "Node.js" },
{ Icon: SiReact, name: "React" },
{ Icon: SiTailwindcss, name: "Tailwind CSS" },
],
className = "",
}: StacksCardProps) => {
// Add keyframes for animations
const keyframesId = "stacks-card-keyframes";
React.useEffect(() => {
// Check if keyframes already exist
if (!document.getElementById(keyframesId)) {
const style = document.createElement("style");
style.id = keyframesId;
style.textContent = `
@keyframes marquee-left {
from { transform: translateX(0%); }
to { transform: translateX(-100%); }
}
@keyframes marquee-up {
from { transform: translateY(0%); }
to { transform: translateY(-100%); }
}
`;
document.head.appendChild(style);
}
return () => {
const existingStyle = document.getElementById(keyframesId);
if (existingStyle) {
existingStyle.remove();
}
};
}, []);
return (
<div
className={`flex h-60 flex-col gap-2 overflow-hidden rounded-xl p-4 lg:p-6 transition-all duration-300 ease-out hover:shadow-[0_7.2px_28.8px_rgba(31,38,135,0.45)] ${className}`}
style={{
background: "rgba(255, 255, 255, 0.45)",
backdropFilter: "blur(8px) saturate(180%)",
WebkitBackdropFilter: "blur(8px) saturate(180%)",
boxShadow: "0 7.2px 28.8px rgba(31, 38, 135, 0.25)",
}}
>
<div className="flex items-center gap-2">
<ZapIcon className="size-[18px] text-gray-800" />
<h2 className="text-sm font-medium text-gray-800">{title}</h2>
</div>
{/* First row: scrolling to right */}
<Marquee gap="30px" className="py-4" fade pauseOnHover>
{firstRowIcons.map((tech, index) => (
<tech.Icon
key={`first-${tech.name}-${index}`}
className="size-12 opacity-80 transition-all duration-300 ease-out hover:opacity-100 hover:scale-110 text-gray-800"
/>
))}
</Marquee>
{/* Second row: scrolling to left */}
<Marquee gap="30px" className="py-4" fade pauseOnHover reverse>
{secondRowIcons.map((tech, index) => (
<tech.Icon
key={`second-${tech.name}-${index}`}
className="size-12 opacity-80 transition-all duration-300 ease-out hover:opacity-100 hover:scale-110 text-gray-800"
/>
))}
</Marquee>
</div>
);
};
export default StacksCard;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment