Chapter 04 — Activity Guide

Build an Animated Portfolio Page

Learn CSS animations, the Intersection Observer API, and layout design — while building a real portfolio page you can customize, deploy, and share.

Why This Project?

Chapters 01 through 03 were about function — storing data, capturing ideas, generating documents. Chapter 04 is about presence. A portfolio page is the first thing anyone sees. It has to make a visual argument before a single word is read.

This chapter teaches the skills that separate "I know HTML" from "I can design for the web" — animation timing, scroll-triggered reveals, responsive layout, and form handling. You'll also add animated counters and a live contact form demo.

CSS @keyframes

Declare named animations with @keyframes, then attach them to elements with animation:. Stagger delays to sequence entrance effects without JavaScript.

Intersection Observer

A browser API that fires a callback when an element enters or leaves the viewport. The modern way to trigger scroll animations — no scroll event listeners, no performance hit.

CSS Grid + Responsive

grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)) creates a self-adjusting project grid that works on any screen — no breakpoint math required.

Counter Animation

requestAnimationFrame with an easing function drives a smooth number count-up effect. The stat goes from 0 to its target over a fixed duration when scrolled into view.

CSS Animations Intersection Observer CSS Grid requestAnimationFrame Layout Design Forms GitHub Pages

The AI Angle for This Chapter

Layout and animation are areas where AI shines — but only if you give it visual context. Describing "a hero section with a centered heading" produces generic output. Describing "a hero with a radial red-to-black gradient, a white all-caps heading at 5rem, and a subtitle in #888 with two buttons below it" produces something you can actually use.

AI reads specificity. The more visual precision you bring to your prompt — colors, sizes, spacing, animation timing, easing curves — the closer the first output will be to what you imagined. Vague prompts produce vague layouts.

“A layout prompt without visual details is a wireframe without dimensions.

Before prompting any section, define these four things:

Before You Build — Plan the Sections

A portfolio page is a sequence of sections, each with a job to do. Map yours before you prompt:

  1. List every section you want: Hero, About, Projects, Stats, Process, Contact are common — but what's yours?
  2. For each section, write one sentence about its job. What should the visitor know or feel after reading it?
  3. Which sections need animation, and which are static? Too much animation is noise.
  4. What's the one piece of information that must survive if someone only spends 5 seconds on the page?
  5. If this page replaces a résumé, what would you remove from a traditional résumé that doesn't belong here?

A portfolio page that answers "Who are you, what have you built, and how do I reach you?" in under 10 seconds is successful. Everything else is decoration.

Build It

Phase 1

Build the hero section with entrance animations

The hero is the first thing seen. It should communicate your name/brand, your value proposition, and two calls to action — all within a single viewport height. Entrance animations play on load using @keyframes fadeDown with staggered animation-delay values.

Build a full-viewport hero section with:
- A radial gradient background (dark red to black)
- A small eyebrow label in red uppercase
- A large all-caps heading with one word highlighted red
- A subtitle in muted gray (#888)
- Two buttons: one solid red (primary CTA), one outlined ghost
- A scroll hint arrow at the bottom with a bounce animation
Entrance: each element fades in from above (translateY -16px to 0)
with staggered animation-delay: 0s, 0.12s, 0.24s, 0.36s.
Pure HTML/CSS. No frameworks.
Phase 2

Add scroll-reveal with Intersection Observer

Elements outside the viewport start invisible and slide in when they enter the scroll view. Add three reveal classes: .reveal (fade up), .reveal-left, and .reveal-right. A single IntersectionObserver toggles the .visible class on all of them.

Write an IntersectionObserver that:
1. Observes all elements with class .reveal, .reveal-left, .reveal-right
2. When an element intersects (threshold: 0.12), adds class .visible
3. .reveal starts at opacity:0, translateY(32px) → transitions to
   opacity:1, translateY(0) over 0.65s ease
4. .reveal-left starts at translateX(-40px), .reveal-right at
   translateX(40px) — both transition to translateX(0)
Add these classes to every section heading and content block.
Phase 3

Build the project grid

A self-filling CSS grid of project cards. Each card has an icon, title, description, tag chips, and a hover arrow. The grid uses auto-fill with minmax — no media queries needed for the columns.

Build a project grid section using:
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr))
Each card has: an emoji icon, a bold title, a short description,
a row of small tag chips, and a right-arrow that turns red on hover.
Card hover: lift with translateY(-4px) and a red border.
Add .reveal class to each card for scroll-in animation.
Include real project entries: at least 4 cards with distinct content.
Phase 4

Add animated stat counters

A full-width red stats bar with 4 numbers that count up from 0 to their target when scrolled into view. The counter animation uses requestAnimationFrame with a cubic ease-out function for a natural deceleration feel.

Add a stats bar section with a solid red background.
Inside: 4 stat blocks (number + label), laid out in a grid.
Each number has data-target="X" attribute.
When the IntersectionObserver fires on the parent .reveal element,
run animateCounter(el):
- duration: 1200ms
- easing: ease-out cubic (1 - Math.pow(1 - progress, 3))
- update el.textContent = Math.round(ease * target)
using requestAnimationFrame until progress >= 1.
Phase 5

Build the contact section and form

A two-column layout: contact info on the left (links, location, bio), a working form on the right. The form captures name, email, subject, and message — and on submit shows a toast confirmation (no server needed for the demo).

Build a two-column contact section:
Left: short intro paragraph, location line, and two icon+link rows
(e.g. GitHub link, live show link).
Right: a form with name, email, subject (text), and message (textarea)
inputs. On submit (preventDefault), show a toast notification:
"✅ Message received, [name]! (Demo — no server)"
and reset the form. Match the dark form field styling from Ch 03.
Add reveal-left to left column, reveal-right to right column.

Concepts You Just Used

Intersection Observer is the key upgrade from Chapter 01. In Ch 01 you used timers. Here you used the browser's built-in viewport detection — which is how every modern animation library (GSAP ScrollTrigger, Framer Motion) works under the hood. You just built the same thing from scratch.

Reflect

  1. What's the difference between a CSS animation and a CSS transition? When would you use each?
  2. Why is Intersection Observer better than a scroll event listener for reveal animations? What's the performance difference?
  3. The stat counter uses requestAnimationFrame. Why not just use setInterval?
  4. The contact form doesn't actually send anything. What would you need to add to make it functional? (Hint: look up Formspree or EmailJS.)
  5. If you had to cut three sections to make this page load twice as fast, which would you cut and why?

A portfolio page is never finished — but it can be shipped. The difference between developers who have portfolios and those who don't usually isn't skill. It's willingness to publish something imperfect.

Go Further

🌟 Dark/Light Toggle

Add a theme toggle button. Switch CSS custom properties on :root between dark and light palettes. Persist preference in localStorage.

🎬 Project Modal

Clicking a project card opens a full-screen modal with a detailed case study — screenshots, tech stack, lessons learned. Close with Escape key.

📨 Real Contact Form

Wire the contact form to Formspree or EmailJS so messages actually arrive in your inbox — no backend code required.

🎓 Skills Progress Bars

Add animated horizontal progress bars to the About section. Each bar fills from 0% to its target width when scrolled into view using the same Intersection Observer.

📷 Custom Avatar

Replace the emoji avatar with a real image upload or a CSS-drawn geometric portrait. The spinning ring animation stays.

📄 Downloadable CV

Add a "Download CV" button that either links to a PDF or generates one dynamically using the same window.print() technique from Chapter 03.

Up Next — Chapter 05

Meeting Note Taker

Build a structured note-taking app with auto-timestamping, action item tagging, and one-click export to plain text or markdown. You'll learn keyboard shortcuts, rich text handling, and file download APIs.

Start Chapter 05 →