Skip to main content
The Stack component showcases the developer’s technology stack in a responsive grid layout with icons loaded from the Devicon CDN, featuring smooth hover effects and IntersectionObserver reveal animations.

Features

  • Icon CDN loading from jsdelivr/devicons
  • Responsive grid (2 cols mobile, 3 tablet, 5 desktop)
  • Grayscale hover effects (desktop only)
  • IntersectionObserver for staggered reveal animations
  • Grid-based layout with 1px borders
  • Invert filter for specific icons (e.g., LaTeX)

Visual Appearance

A grid of technology cards, each containing an icon and label. On desktop, icons start grayscale and low opacity, becoming full-color on hover with a scale animation. The entire grid has a unified border system creating a seamless appearance.

Component Code

import { useEffect, useRef } from 'react';
import { useLanguage } from '../context/LanguageContext';

const techStack = [
  { name: 'Java', icon: 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/java/java-original.svg' },
  { name: 'PHP', icon: 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/php/php-original.svg' },
  { name: 'MySQL', icon: 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/mysql/mysql-original.svg' },
  { name: 'WordPress', icon: 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/wordpress/wordpress-plain.svg' },
  { name: 'JavaScript', icon: 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/javascript/javascript-original.svg' },
  { name: 'HTML5', icon: 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/html5/html5-original.svg' },
  { name: 'CSS3', icon: 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/css3/css3-original.svg' },
  { name: 'Linux', icon: 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/linux/linux-original.svg' },
  { name: 'Bash', icon: 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/bash/bash-plain.svg' },
  { name: 'Docker', icon: 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/docker/docker-original.svg' },
  { name: 'Git', icon: 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/git/git-original.svg' },
  { name: 'Figma', icon: 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/figma/figma-original.svg' },
  { name: 'LaTeX', icon: 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/latex/latex-original.svg', invert: true },
];

export default function Stack() {
  const { t } = useLanguage();
  const gridRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const io = new IntersectionObserver((entries) => {
      entries.forEach((e) => {
        if (e.isIntersecting) {
          e.target.querySelectorAll('[data-reveal]').forEach((el, i) => {
            (el as HTMLElement).style.transitionDelay = `${i * 0.04}s`;
            el.classList.add('in-view');
          });
          io.unobserve(e.target);
        }
      });
    }, { threshold: 0.1 });
    if (gridRef.current) io.observe(gridRef.current);
    return () => io.disconnect();
  }, []);

  return (
    <section className="py-24 md:py-32 px-6 md:px-12 bg-black border-t border-white/10 relative z-10">
      <div className="max-w-7xl mx-auto w-full">
        <h2 className="font-wide text-3xl md:text-5xl font-bold uppercase mb-16 md:mb-24 text-center md:text-left">
          {t('stack.title')}
        </h2>
        <div ref={gridRef} className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-px bg-white/10 border border-white/10">
          {techStack.map((tech, i) => (
            <div key={i} data-reveal="up" className="bg-black p-8 md:p-10 flex flex-col items-center justify-center gap-6 group md:hover:bg-white/[0.02] transition-colors duration-500">
              <img
                src={tech.icon}
                alt={`Logotipo de ${tech.name} - Stack Tecnológico de Yeray Garrido`}
                loading="lazy"
                decoding="async"
                width="56"
                height="56"
                className={`w-10 h-10 md:w-14 md:h-14 object-contain transition-all duration-500 transform md:opacity-50 md:grayscale md:group-hover:opacity-100 md:group-hover:grayscale-0 md:group-hover:scale-110 ${tech.invert ? 'invert' : ''}`}
              />
              <span className="font-sans text-xs tracking-widest uppercase font-bold transition-colors duration-500 text-white md:text-white/50 md:group-hover:text-white">
                {tech.name}
              </span>
            </div>
          ))}
        </div>
      </div>
    </section>
  );
}

Technology Stack Array

The component uses a static array of technology objects:
const techStack = [
  { name: 'Java', icon: 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/java/java-original.svg' },
  { name: 'PHP', icon: 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/php/php-original.svg' },
  { name: 'MySQL', icon: 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/mysql/mysql-original.svg' },
  { name: 'WordPress', icon: 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/wordpress/wordpress-plain.svg' },
  { name: 'JavaScript', icon: 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/javascript/javascript-original.svg' },
  { name: 'HTML5', icon: 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/html5/html5-original.svg' },
  { name: 'CSS3', icon: 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/css3/css3-original.svg' },
  { name: 'Linux', icon: 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/linux/linux-original.svg' },
  { name: 'Bash', icon: 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/bash/bash-plain.svg' },
  { name: 'Docker', icon: 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/docker/docker-original.svg' },
  { name: 'Git', icon: 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/git/git-original.svg' },
  { name: 'Figma', icon: 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/figma/figma-original.svg' },
  { name: 'LaTeX', icon: 'https://cdn.jsdelivr.net/gh/devicons/devicon/icons/latex/latex-original.svg', invert: true },
];

Icon Loading

Icons are loaded from the Devicon CDN:
loading
lazy
Native lazy loading for performance optimization
decoding
async
Asynchronous image decoding to prevent blocking

Icon Variants

Devicon provides multiple variants per technology:
  • original - Full-color original logo
  • plain - Simplified version
  • original-wordmark - Logo with text
  • plain-wordmark - Simplified with text

Grid Layout

<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-px bg-white/10 border border-white/10">
  • gap-px: 1px gap between cells
  • bg-white/10: Gap color (background shows through)
  • border border-white/10: Outer border

Responsive Grid

  • Mobile (default): 2 columns
  • Tablet (md): 3 columns
  • Desktop (lg): 5 columns

Hover Effects

className={`
  w-10 h-10 md:w-14 md:h-14 
  object-contain 
  transition-all duration-500 transform 
  md:opacity-50              // 50% opacity on desktop
  md:grayscale               // Grayscale filter on desktop
  md:group-hover:opacity-100 // Full opacity on hover
  md:group-hover:grayscale-0 // Remove grayscale on hover
  md:group-hover:scale-110   // Scale to 110% on hover
  ${tech.invert ? 'invert' : ''}
`}
Note: Hover effects are desktop-only (prefixed with md:)

IntersectionObserver Animation

const io = new IntersectionObserver((entries) => {
  entries.forEach((e) => {
    if (e.isIntersecting) {
      e.target.querySelectorAll('[data-reveal]').forEach((el, i) => {
        (el as HTMLElement).style.transitionDelay = `${i * 0.04}s`;  // 40ms per item
        el.classList.add('in-view');
      });
      io.unobserve(e.target);
    }
  });
}, { threshold: 0.1 });

Staggered Delays

Each grid item receives a progressive delay:
  • Item 0: 0ms
  • Item 1: 40ms
  • Item 2: 80ms
  • Item 12: 480ms
This creates a cascading reveal effect across the grid.

Invert Filter

Some icons (like LaTeX) need color inversion for visibility on dark backgrounds:
{ name: 'LaTeX', icon: '...', invert: true }

<img
  className={`... ${tech.invert ? 'invert' : ''}`}
/>

Accessibility

Alt Text

alt={`Logotipo de ${tech.name} - Stack Tecnológico de Yeray Garrido`}
Descriptive alt text in Spanish (could be internationalized).

Image Attributes

width="56"
height="56"
loading="lazy"
decoding="async"
Explicit dimensions prevent layout shifts, lazy loading improves performance.

Performance Considerations

  1. Lazy Loading: Icons load only when approaching viewport
  2. Async Decoding: Non-blocking image decode
  3. IntersectionObserver: Efficient scroll-based animation trigger
  4. Auto-unobserve: Disconnects after animation completes
  5. CDN Delivery: Fast global icon delivery via jsDelivr

Responsive Behavior

Mobile
<md
  • 2-column grid
  • 8px padding per cell
  • 40px icon size
  • Icons always full-color
  • Labels always full opacity
Desktop
md+
  • 5-column grid (3 on tablet)
  • 10px padding per cell
  • 56px icon size
  • Icons start grayscale
  • Hover effects enabled

Dependencies

useLanguage
context
Translation context for section title
IntersectionObserver
Web API
Native browser API for scroll-based animations

Source Location

~/workspace/source/src/components/Stack.tsx

Build docs developers (and LLMs) love