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.
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.
Before prompting any section, define these four things:
- What is the background color/treatment? (solid, gradient, image, pattern)
- What is the primary typographic hierarchy? (eyebrow → headline → subtext)
- What animation should fire, and when? (on load vs. on scroll, duration, easing)
- What's the responsive behavior? (stack on mobile, collapse columns, hide/show)
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:
- List every section you want: Hero, About, Projects, Stats, Process, Contact are common — but what's yours?
- For each section, write one sentence about its job. What should the visitor know or feel after reading it?
- Which sections need animation, and which are static? Too much animation is noise.
- What's the one piece of information that must survive if someone only spends 5 seconds on the page?
- 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
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.
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.
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.
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.
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
@keyframes+animation:— define reusable named animations and attach them to elements with timing, delay, and fill-modeanimation-delay— stagger entrance animations without JavaScript by offsetting each element's start timeIntersectionObserver— fire callbacks when elements enter the viewport; toggle a class to trigger CSS transitionsrequestAnimationFrame(step)— schedule frame-by-frame animation updates; far more efficient thansetInterval- Easing with math —
1 - Math.pow(1 - t, 3)is ease-out cubic: fast start, gentle landing grid-template-columns: repeat(auto-fill, minmax(...))— fluid responsive grid with no media queriesposition: sticky+backdrop-filter: blur()— frosted-glass sticky navscroll-behavior: smoothonhtml— native smooth scrolling to anchor links
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
- What's the difference between a CSS animation and a CSS transition? When would you use each?
- Why is Intersection Observer better than a scroll event listener for reveal animations? What's the performance difference?
- The stat counter uses
requestAnimationFrame. Why not just usesetInterval? - The contact form doesn't actually send anything. What would you need to add to make it functional? (Hint: look up Formspree or EmailJS.)
- 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.
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 →