Skip to main content

Overview

The Intro component displays a two-part introduction section:
  1. A full-height black section with the main value proposition
  2. A light gray section showcasing four key technical expertise areas
The component uses IntersectionObserver for staggered reveal animations as elements enter the viewport.

Component structure

src/components/Intro.tsx
import { useEffect, useRef } from 'react';
import { useLanguage } from '../context/LanguageContext';

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

  const features = [
    { num: '01', title: t('intro.feat1.title'), desc: t('intro.feat1.desc') },
    { num: '02', title: t('intro.feat2.title'), desc: t('intro.feat2.desc') },
    { num: '03', title: t('intro.feat3.title'), desc: t('intro.feat3.desc') },
    { num: '04', title: t('intro.feat4.title'), desc: t('intro.feat4.desc') },
  ];

  useEffect(() => {
    const io = new IntersectionObserver((entries) => {
      entries.forEach((e) => {
        if (e.isIntersecting) {
          e.target.classList.add('in-view');
          io.unobserve(e.target);
        }
      });
    }, { threshold: 0.1 });
    
    sectionRef.current?.querySelectorAll('[data-reveal]').forEach((el) => io.observe(el));
    return () => io.disconnect();
  }, []);

  return (
    <section ref={sectionRef} className="relative z-10">
      {/* Black section with value proposition */}
      <div className="bg-black text-white py-24 md:py-32 min-h-[100svh]">
        {/* ... */}
      </div>
      
      {/* Light section with expertise areas */}
      <div className="bg-[#f2f2f2] text-black pt-24 pb-24 md:pt-32 md:pb-32">
        {/* ... */}
      </div>
    </section>
  );
}

Features

Two-tone layout

The component uses contrasting color schemes:
  • Black section: Dark background with white text for main messaging
  • Light gray section: #f2f2f2 background with black text for expertise cards

Internationalization

All text content is translated using the useLanguage hook:
intro.title1
string
First line of main heading (e.g., “SOFTWARE”)
intro.title2
string
Second line of main heading (e.g., “ENGINEERING”)
intro.desc1
string
Main value proposition paragraph
intro.subtitle1
string
First line of subtitle (e.g., “CUSTOM”)
intro.subtitle2
string
Second line of subtitle (e.g., “SOLUTIONS”)
intro.desc2
string
Secondary description paragraph
intro.expertise1
string
First line of expertise heading (e.g., “TECHNICAL”)
intro.expertise2
string
Second line of expertise heading (grayed out)
intro.feat1.title
string
Title for first expertise area (e.g., “JAVA BACKEND”)
intro.feat1.desc
string
Description for first expertise area

IntersectionObserver animations

Elements are animated as they enter the viewport:
src/components/Intro.tsx:15-23
useEffect(() => {
  const io = new IntersectionObserver((entries) => {
    entries.forEach((e) => {
      if (e.isIntersecting) {
        e.target.classList.add('in-view');
        io.unobserve(e.target);
      }
    });
  }, { threshold: 0.1 });
  
  sectionRef.current?.querySelectorAll('[data-reveal]').forEach((el) => io.observe(el));
  return () => io.disconnect();
}, []);
Elements with the data-reveal attribute are observed and animated when 10% of them becomes visible (threshold: 0.1).

Expertise cards

Four expertise areas are displayed with:
  • Large numbers: 20vw (mobile) to 12vw (desktop) for visual hierarchy
  • Staggered animations: Each card has a delay based on its index (i * 0.06s)
  • Hover effects: Scale transform on the large number (scale-110)
  • Responsive layout: Vertical stacking on mobile, horizontal on desktop
src/components/Intro.tsx:52-62
{features.map((feature, i) => (
  <div key={i} className="border-b-2 border-black py-12 md:py-24 flex flex-col md:flex-row gap-6 md:gap-24 relative overflow-hidden group">
    <div 
      data-reveal="left" 
      style={{transitionDelay:`${i*0.06}s`}} 
      className="font-display text-[20vw] md:text-[12vw] leading-none text-black/10 md:text-black/20 select-none transition-transform duration-500 group-hover:scale-110 origin-left"
    >
      {feature.num}
    </div>
    <div data-reveal="right" style={{transitionDelay:`${i*0.06+0.12}s`}} className="flex-1 flex flex-col justify-end">
      <h3 className="font-wide text-xl md:text-4xl font-bold uppercase mb-4 tracking-wider">{feature.title}</h3>
      <p className="font-sans text-base md:text-xl text-black/70 max-w-xl leading-relaxed">{feature.desc}</p>
    </div>
  </div>
))}

Styling details

Full viewport height

The black section uses min-h-[100svh] (small viewport height) to ensure it fills the screen on mobile devices with dynamic address bars.

Border-based dividers

Expertise cards are separated with 2px black borders:
  • Top border on the container
  • Bottom border on each card
This creates a consistent, brutalist aesthetic.

Opacity hierarchy

  • Large numbers: text-black/10 on mobile, text-black/20 on desktop
  • Body text in black section: text-white/80
  • Body text in light section: text-black/70
  • Second line of expertise heading: text-black/30

Dependencies

useLanguage
hook
Custom hook from LanguageContext for accessing translation function
useEffect
React hook
Sets up IntersectionObserver for reveal animations
useRef
React hook
References the section element for querying child elements

Experience

Also uses IntersectionObserver for reveal animations

Stack

Similar grid layout with staggered reveals

Internationalization

Learn about the translation system

Animation system

Understand the animation patterns used

Build docs developers (and LLMs) love