1990s HTML & CSS — The simplest possible web page: a single HTML document with text, images, and styling. Every web developer's first project. Inspired by freeCodeCamp's Responsive Web Design certification.

01

Basic HTML Structure

Complete

Why learn this?

Every web page is just HTML — the foundation of the entire web. This step teaches you the semantic building blocks that every site uses: headings, paragraphs, lists, and the document outline. Once you understand this, you can read any web page's source code. More importantly, you learn to write for humans and machines — search engines, screen readers, and automated tests all depend on correct structure.

What you built

A single-page tribute to Alan Turing with a bio, timeline, and legacy section. Plain HTML only — no CSS, no JavaScript. The focus is entirely on structure. The <h1> announces the subject, <h2> sections organise content, and <ul>/<li> present the timeline as a meaningful list rather than a generic block.

Key concepts

  • Semantic HTMLmain, section, h1-h2 tell browsers and screen readers what each part of the page means, not just how it looks. A <div>-only page works visually but offers zero structural context to assistive tech.
  • Document outline — Headings create a hierarchy (h1 → h2 → h3) that search engines and accessibility tools use to understand your page. Never skip levels (h1 → h3 is invalid) — it breaks the outline and confuses screen-reader navigation.
  • Listsul and li for timelines, features, any grouped content. Screen readers announce list count (List with 3 items), giving users critical context a <div> cannot.
  • Validation — Browsers forgive bad HTML, but valid HTML is more portable, accessible, and easier to maintain. Always validate with the W3C validator before publishing.
  • Accessibility foundations — The lang="en" attribute on <html> tells screen readers which pronunciation rules to use. Missing it causes text to be spoken with the wrong accent or phoneme set.

Design decisions & tradeoffs

  • Single <h1> — Best practice for SEO and accessibility. A second <h1> dilutes the document's main topic. Tradeoff: multi-brand sections (e.g. a sidebar with a different topic) might want separate <h1>s, but those are rare on simple pages.
  • No <header> wrapper — Putting <h1> inside a <header> is more semantic for site-wide branding, but adds unnecessary nesting for a single-page tribute. Tradeoff: less explicit grouping, harder to style separately later.
  • Plain HTML, zero CSS — Purity of focus: the student sees exactly what semantic HTML provides. Tradeoff: the page looks unstyled and may discourage beginners who expect visual feedback. Step 3 fixes this deliberately.

Alternative approaches

  • <div>-soup layout — Wrapping everything in generic <div> elements works visually but is the worst approach for accessibility, SEO, and maintainability. Still used on millions of legacy sites.
  • HTML5 outline algorithm — Using <section> with implicit heading-level reset (h1 inside section = h2-equivalent). Modern browsers mostly ignore this; the traditional <h1><h6> hierarchy is what actually works.
  • ARIA landmark roles — Adding role="banner", role="contentinfo" to supplement native HTML5 elements. Useful on older IE where <main> isn't recognised.

Browser compatibility

  • All browsers since 2010 support <!DOCTYPE html>. It triggers standards mode, preventing quirks-mode rendering that breaks CSS layout.
  • HTML5 semantic elements (<main>, <section>) are supported in all browsers since IE9. IE8 needs the HTML5 Shiv (a tiny JS polyfill) plus display: block in CSS.
  • lang="en" is understood by every user agent, including browsers, screen readers, and crawlers.

Performance notes

  • This page has zero external requests — it renders from a single HTTP response. That's the fastest possible page.
  • The minimal DOM (fewer than 50 nodes) means the browser's layout and paint phases finish in microseconds. Always measure before adding complexity.
  • No CSS means no style recalculation, no compositing layers, no repaints. Future steps add styling deliberately, but you can always return to this baseline if perf becomes an issue.

⚠️ Common pitfalls

  • Forgetting <!DOCTYPE html> — Triggers quirks mode, where browsers emulate IE5-era box modelling. CSS padding breaks, layouts collapse, and you waste hours debugging.
  • Skipping heading levels<h1> then <h3> (missing <h2>) breaks the document outline. Screen readers offer heading-level navigation; a skipped level says the author doesn't care about structure.
  • Missing lang on <html> — Affects text-to-speech pronunciation, hyphenation, and search-engine language detection. Hard to spot visually, easy to fix.
  • Unclosed tags — Most browsers auto-close them (e.g. <li> without </li>), but this breaks on XML parsers, RSS feeds, and automated tools. Always close every tag.

Next up

Step 2 adds images with figure and figcaption — your first visual element. Step 3 introduces CSS to make it look like a real page.

index.html ▶ Run
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Tribute to Alan Turing</title>
</head>
<body>
  <h1>Dr. Alan Turing</h1>
  <p>The father of theoretical
  computer science</p>

  <h2>Timeline</h2>
  <ul>
    <li><strong>1912</strong> — Born in London</li>
    <li><strong>1936</strong> — Turing Machine paper</li>
    <li><strong>1950</strong> — Turing Test proposed</li>
  </ul>

  <h2>Legacy</h2>
  <p>Turing's work laid the
  foundation for modern computing.</p>
</body>
</html>
✅ Done. Step 1 complete. Move to Step 2 below.
02

Image & Caption

Complete

Why learn this?

Images make pages engaging. The figure + figcaption pattern is the standard way to add self-contained media with a caption — used by news sites, documentation, and portfolios everywhere. Proper image embedding also addresses accessibility (screen readers), performance (lazy loading), and layout stability (Cumulative Layout Shift), three pillars of modern web development.

What you'll build

A hero image of Alan Turing at age 16, centred and captioned. The image scales with the viewport via max-width: 100%, has proper alt text for screen readers, and is wrapped in a figure with figcaption so the caption stays tied to the image regardless of layout changes.

Key concepts

  • figure — Groups image + caption into one semantic unit. Screen readers announce it as a single figure landmark, letting users skip or inspect it as a whole.
  • figcaption — Visible description tied to the image. Unlike alt, it's always on screen and supplements rather than replaces the alt text.
  • alt attribute — Describes the image for screen readers and when images fail to load. Every <img> must have one — even decorative images use alt="" to signal skip me to assistive tech.
  • max-width: 100% — Makes images responsive automatically by preventing overflow. Combined with height: auto, it preserves aspect ratio at any viewport width.
  • loading="lazy" — Defers offscreen image loading. Supported in Chrome 76+, Firefox 75+, Safari 15.4+. Reduces initial page weight and saves bandwidth.

Design decisions & tradeoffs

  • figure/figcaption vs plain div + p — Semantic grouping means the caption moves with the image in any reader mode (Pocket, Reader View) and is understood by screen readers. Tradeoff: slightly more markup than a bare <img> + <p>.
  • max-width: 100% vs fixed width — Fluid sizing prevents horizontal scroll on mobile without media queries. Tradeoff: images can become very large on wide screens; pair with max-width: 800px on the figure container to cap maximum size.
  • border-radius: 8px with box-shadow — Gives the image a polished, card-like appearance without needing background images. Tradeoff: box-shadow triggers a paint cycle on every frame; use sparingly on pages with many images.
  • loading="lazy" vs eager — Lazy loading saves bandwidth for images below the fold. Tradeoff: very slow scroll-to-see on first load if the image is near the top of the page; consider eager (loading="eager" or omitting the attribute) for above-the-fold hero images.

Alternative approaches

  • <picture> element — Provides multiple source images for different resolutions, formats (WebP/AVIF fallback), and art direction (crop on mobile). Syntax: <picture><source srcset="..." type="image/webp"><img ...></picture>. Better performance but more verbose.
  • object-fit: cover — Crops the image to fill a container's dimensions. Useful for hero banners where aspect ratio must stay consistent. Tradeoff: image content may be clipped.
  • width + height attributes for aspect ratio — Setting width="400" height="300" on the <img> tells the browser the aspect ratio before the image loads, preventing Cumulative Layout Shift (CLS). CSS max-width: 100% then scales it down responsively. This is the recommended production pattern.
  • Inline SVG — For icons or diagrams, inline SVG is resolution-independent and smaller than raster images. Not suitable for photographs.

Browser compatibility

  • figure/figcaption — Supported in all browsers since IE9 (with HTML5 Shiv for IE8).
  • border-radius — Supported since IE9, Safari 5, Chrome 4, Firefox 4. No prefixes needed.
  • box-shadow — Supported since IE9. Older browsers degrade gracefully (no shadow, image still visible).
  • loading="lazy" — Chrome 76+ (2019), Firefox 75+ (2020), Safari 15.4+ (2022). Older Safari needs a JS polyfill like loading="lazy" polyfill from Google. Always test — an unsupported browser simply loads the image eagerly (safe degradation).
  • max-width: 100% — Supported since IE7. Effectively universal.

Accessibility details

  • alt is mandatory — Every <img> must have one. For the tribute hero image, alt="Alan Turing at age 16" conveys the subject and context. If the image were purely decorative (e.g. a background flourish), use alt="" to hide it from screen readers.
  • figcaption vs alt — They serve different purposes: alt replaces the image when it cannot be loaded; figcaption is always visible. Don't duplicate verbatim — the caption adds context the alt text doesn't need to repeat.
  • aria-labelledby — For complex figures, <figure aria-labelledby="caption-id"> links the caption to the figure explicitly. The figcaption itself is usually sufficient.
  • prefers-reduced-motion — If images have hover zoom or parallax effects, wrap them in a @media (prefers-reduced-motion: reduce) query to disable the animation. This single image doesn't animate, but it's the right pattern to establish now.

Performance notes

  • Image size matters most — A 2 MB hero photo can outweigh all CSS + HTML on the page by 100x. Always compress images: JPEG at 80-85 quality, or switch to WebP/AVIF for 30-50% smaller files.
  • will-change: transform — If you add a hover scale effect on the image, will-change: transform promotes the element to its own compositor layer, avoiding repaints on animation. Overuse wastes GPU memory — apply it on :hover or via JS, not permanently.
  • Avoid transition: all — On hover, transition: all 0.3s triggers style recalc for every animatable property. Explicitly list properties: transition: transform 0.3s, box-shadow 0.3s.
  • CLS prevention — Without width/height attributes, the browser reserves zero space for the image until it loads, then reflows the page. Set explicit dimensions on <img> and let CSS max-width: 100% scale down.

⚠️ Common pitfalls

  • Missing alt attribute — Screen readers read the image filename aloud (e.g. IMG_0042.JPG). This is the most common and most impactful accessibility failure.
  • Fixed image dimensionswidth="800" without max-width: 100% causes horizontal scroll on mobile. Always combine fixed dimensions with fluid CSS.
  • Forgetting height: auto — Without it, an image with max-width: 100% alone distorts when the container is narrower than the image width.
  • Huge unoptimised images — A 4000×3000 JPEG at 10 MB will load slowly on 3G, consuming the user's data plan. Resize to display dimensions (e.g. 1200px wide) and compress.
  • loading="lazy" on hero images — If the image is above the fold, lazy loading delays the Largest Contentful Paint (LCP) metric. Use eager loading for the hero image and lazy for images farther down the page.
  • Broken image paths — A typo in src shows the browser's broken-image icon and the alt text. Always verify image URLs resolve to 200 before deploying.
HTML snippet ▶ Run
<figure>
  <img
    src="turing.jpg"
    alt="Alan Turing at age 16"
    loading="lazy"
  >
  <figcaption>
    Alan Turing at age 16 —
    already showing mathematical
    brilliance
  </figcaption>
</figure>
CSS
figure img {
  max-width: 100%;
  height: auto;
  border-radius: 8px;
  box-shadow: 0 4px 12px
    rgba(0,0,0,0.15);
}

figcaption {
  font-style: italic;
  color: #666;
  margin-top: 0.5em;
}
📸 Tip: Use Unsplash or Wikimedia Commons for free, attribution-friendly images.
03

Timeline & Styling

Complete

Why learn this?

CSS is what turns raw HTML into something you'd actually publish. You'll learn the core selectors, properties, and the box model that underpin every styled page on the internet. More importantly, you'll see how layout decisions — flexbox vs grid, border vs gap, hardcoded colours vs CSS variables — directly affect maintainability, performance, and accessibility.

What you'll build

The page gets a warm paper background, a styled timeline with dark year badges, a blockquote with Turing's famous quote, and hover effects on links. It now looks like a real tribute page. The timeline uses flexbox for alignment, the blockquote uses a left border accent, and typography is tuned for readability.

Key concepts

  • Type selectorsbody { }, h1 { } style all elements of that type. Simple but powerful — they set baseline defaults before class-based overrides.
  • Box modelpadding, margin, border, width. Every element is a box; understanding how these properties interact (and how box-sizing: border-box changes the math) is essential CSS knowledge.
  • Flexboxdisplay: flex with align-items and gap for side-by-side layout. The timeline rows use flexbox to keep the year badge aligned with the event text.
  • Blockquote — Styled pull quotes with border accent and citation. The <blockquote> element is semantic HTML specifically for quoted content — screen readers announce it differently, and search engines treat it as attributed text.
  • CSS classes.timeline, .year-badge, .event scope styles to specific elements, avoiding conflicts with other lists or spans on the page.

Design decisions & tradeoffs

  • Flexbox for timeline rows vs CSS Grid — Flexbox with gap: 0.75em is the simplest approach: items flow naturally, alignment is one line (align-items: baseline), and wrapping is automatic. CSS Grid (grid-template-columns: auto 1fr) would give more control but adds complexity. Flexbox wins here for simplicity.
  • border-bottom separators vs gap spacing — Lines between timeline entries provide clear visual separation and guide the eye vertically. Tradeoff: they add visual noise on short timelines. An alternative is gap-only spacing with a subtle background tint.
  • Dark badges (#16213e) on warm background (#faf8f5) — High contrast for readability. The dark badge colour matches a typical navy palette that reads as professional and serious. Tradeoff: hardcoded colours prevent theming — a CSS custom property (--badge-bg: #16213e) would make global colour changes one-line edits.
  • border-left: 4px on blockquote — The classic pull-quote treatment originated in print design. The thick left border visually anchors the quote to the left margin, while the rounded right corners soften the overall shape.
  • min-width: 5em on year-badge — Ensures all badges are the same width regardless of year digit count (1912 vs 1950). Tradeoff: if a year is very long (e.g. 10000 BC), the badge stretches — but for historical timelines this edge case doesn't arise.

Alternative approaches

  • CSS Grid timelinedisplay: grid; grid-template-columns: 6em 1fr; for strict column alignment. Better for multi-row entries where the event text wraps. Tradeoff: more verbose, less forgiving on small screens without @media overrides.
  • ::before pseudo-elements for decorative markers — Instead of border-bottom, use li::before { content: '●'; } to create visual timeline dots. More flexible styling (colour, size, animation) but adds a pseudo-element per list item.
  • CSS custom properties for theming:root { --accent: #16213e; --bg: #faf8f5; } makes the entire palette adjustable in one place. Step 4's polish could introduce this without breaking existing styles.
  • <dl> (description list) for timeline<dl><dt>1912</dt><dd>Born in London</dd></dl> is semantically appropriate for key-value pairs like year-description. Tradeoff: styling <dl> as a timeline is less intuitive than styling a <ul>.
  • details/summary for collapsible entries — Expands each timeline entry on click. Adds interactivity with zero JavaScript. Tradeoff: not all timeline content needs to be hidden — only appropriate for very long timelines.

Browser compatibility

  • Flexbox — Supported in all modern browsers (Chrome 29+, Firefox 28+, Safari 9+, Edge 12+, IE10+ with -ms- prefix). IE10-11 have known bugs with min-height and nested flex containers — test if using complex flex layouts.
  • gap in flexbox — Supported since Chrome 84 (2020), Firefox 63 (2018), Safari 14.1 (2021). Not supported in older Safari (14 and below) — items will render without spacing. Fallback: margin-right on children + :last-child { margin-right: 0; }.
  • border-radius — Supported in all browsers since IE9. 100% safe.
  • blockquote/cite — 100% support across all browsers, including IE6. These are HTML4-era elements.
  • backdrop-filter support — If you later add glassmorphism effects, note: Safari 9+ (with -webkit- prefix), Chrome 76+, Firefox 103+. Unsupported browsers simply show the underlying background without the blur effect (safe degradation).

Accessibility details

  • <blockquote> is semantic — Screen readers announce quoted text differently, and users can navigate between blockquotes on a page. A <div> with quotation marks does not provide this semantic cue.
  • <cite> element vs cite attribute — The <cite> element wraps the attribution text (— Alan Turing) and is visible to all users. The cite attribute on <blockquote> holds a URL to the source — use it when pulling quotes from an online article.
  • Colour contrast — The dark-blue badge (#16213e) on the warm background (#faf8f5) has a contrast ratio of approximately 12.5:1, well above the WCAG AA minimum of 4.5:1. The #666 caption text on the same background is about 5:1 — also passing.
  • list-style: none and screen readers — Removing list bullets does not remove list semantics. Screen readers still announce List with 3 items. However, some older screen readers may drop the role if list-style: none is combined with display: flex on <li> — adding role="list" to the <ul> explicitly preserves it.
  • Focus indicators — Links in the footer should have visible focus styles. Default browser focus outlines (blue ring) are sufficient at this stage, but :focus-visible is the modern pattern for keyboard-only focus rings.

Performance notes

  • Avoid transition: all — A global transition: all 0.3s on a or li causes the browser to recalculate styles for every animatable property on every state change. Explicitly list: transition: color 0.2s, border-color 0.2s.
  • will-change — If you add hover scale or background shift to timeline rows, promote the animated property: .timeline li:hover { will-change: background-color; }. Use sparingly — each will-change creates a new compositor layer.
  • box-shadow repaints — The blockquote's box-shadow is applied once, not animated, so it paints only on initial render. Animating box-shadow is expensive — avoid transition: box-shadow unless the element is already on its own compositor layer.
  • Font loading — This step uses system fonts, which are instant. If you add web fonts later, use font-display: swap to prevent invisible text during load.

⚠️ Common pitfalls

  • list-style: none removing semantics — In some older browsers (particularly Safari with VoiceOver), display: flex on li combined with list-style: none on ul causes the list role to be dropped. Fix: add role="list" to the <ul>.
  • Forgetting box-sizing: border-box — The default content-box model adds padding outside the element's width. Without a global reset (*, *::before, *::after { box-sizing: border-box; }), padding values change computed widths unexpectedly.
  • Hardcoded colours — Every colour value repeated in CSS is a maintenance burden. If the client asks for a slightly lighter background, you have to find and update every background declaration. Use CSS custom properties from the start.
  • gap unsupported in older Safari — The flexbox gap shorthand was late to arrive in Safari (14.1+, March 2021). If analytics show Safari 14 users, add margin-right: 0.75em on children as a defensive fallback.
  • Blockquote without cite — A quote with no attribution is nearly meaningless in academic or tribute contexts. Always include <cite> or the cite attribute to identify the source.
  • Missing min-width on badges — Without min-width: 5em, the year badge shrinks to fit its content, creating uneven column alignment — 1912 sits narrower than 1950. Always constrain badge widths in timeline layouts.

Approach

  1. Replace the plain ul with a class-based ul class="timeline"
  2. Wrap each year in a span class="year-badge" for the dark badge style
  3. Add a blockquote with a meaningful quote and cite attribution
  4. Add a footer link section with an external Wikipedia reference
  5. Polish typography: line-height, letter-spacing, font sizes
HTML ▶ Run
<ul class="timeline">
  <li>
    <span class="year-badge">1912</span>
    <span class="event">Born in London</span>
  </li>
</ul>

<blockquote>
  <p>"We can only see a short
  distance ahead..."</p>
  <cite>— Alan Turing</cite>
</blockquote>
CSS
.timeline {
  list-style: none;
  padding: 0;
}
.timeline li {
  display: flex;
  align-items: baseline;
  gap: 0.75em;
  padding: 0.5em 0;
  border-bottom: 1px solid #e8e4dd;
}
.year-badge {
  background: #16213e;
  color: #faf8f5;
  padding: 0.15em 0.6em;
  border-radius: 4px;
  font-size: 0.75em;
  font-weight: 700;
  min-width: 5em;
  text-align: center;
}
blockquote {
  margin: 2em 0;
  padding: 1em 1.5em;
  background: #edeae4;
  border-left: 4px solid #16213e;
  border-radius: 0 8px 8px 0;
}
04

Responsive & Polish

Complete

Why learn this?

Over 60% of web traffic is mobile. A page that looks good on desktop but breaks on a phone isn't publishable. Media queries are what make layouts adapt to any screen size. Beyond layout, this step introduces the four pillars of production-ready CSS: responsive design, accessibility, motion safety, and progressive enhancement.

What you built

The page becomes fully responsive: mobile-friendly font sizes, stacked timeline on small screens, smooth scrolling, a skip-to-content link for keyboard users, hover effects on timeline rows and images, prefers-reduced-motion to respect user accessibility settings, and a back-to-top link for long pages on mobile.

Key concepts

  • @media (max-width: 600px) — Mobile breakpoints that restructure layout. The 600px threshold targets phones in portrait orientation — iPads in landscape (768px+) still get the desktop layout.
  • Skip-to-content link — The first focusable element on the page, hidden off-screen until focused. Lets keyboard and screen-reader users bypass repeated navigation and jump straight to the main content.
  • scroll-behavior: smooth — Native smooth scrolling via CSS, no JavaScript required. Anchors links (#step2) scroll instead of jumping.
  • prefers-reduced-motion — A CSS media query that respects the user's OS-level motion preferences. When enabled, disables scrolling and transitions to prevent vestibular-disorder triggers.
  • Hover states:hover on timeline rows and images for interactive feedback. Must be paired with visible focus styles for keyboard users who cannot hover.
  • backdrop-filter — Applies a blur or colour shift behind an element (glassmorphism). Included here as a @supports progressive enhancement — unsupported browsers see a solid fallback.

Design decisions & tradeoffs

  • max-width: 600px breakpoint vs min-width mobile-first — Desktop-first (max-width) is simpler for converting an existing desktop layout: you add overrides for smaller screens. Mobile-first (min-width) is generally preferred for new projects because the base styles target the smallest viewport and get enhanced upward — less CSS, fewer overrides. For this tribute page (converted from a desktop demo), desktop-first is pragmatic.
  • scroll-behavior: smooth vs JS smooth scrolling — CSS smooth scrolling is declarative, zero-JS, and hardware-accelerated on supporting browsers. Tradeoff: not supported in Safari (as of Safari 18). Safari users get instant jumps — the page still works, but the experience is less polished. A JS polyfill can restore smooth scrolling in Safari if needed.
  • Skip-link pattern: absolute off-screen vs display:noneposition: absolute; top: -100% hides the link visually but keeps it in the DOM and focusable. display: none removes it from the accessibility tree entirely — screen readers never find it, defeating the purpose. The off-screen pattern is the only correct approach.
  • transition-duration: 0.01ms !important kills vs removing transitions — Setting duration to 0.01ms is safer than removing transition properties entirely because it works regardless of how transitions were defined (inline, in a library, or in a third-party widget). The !important ensures it overrides all other transition rules.
  • Back-to-top link vs sticky header — A back-to-top link at the bottom is simple, non-intrusive, and works without JavaScript. A sticky header with a nav bar is more convenient but takes up screen space on mobile — a significant tradeoff on small viewports where every pixel matters.

Alternative approaches

  • Mobile-first media queries@media (min-width: 600px) { /* desktop styles */ } instead of max-width. Results in less override CSS and better performance on mobile (less to un-do). Recommended for greenfield projects.
  • JS Intersection Observer for scroll animations — Triggers transitions when elements enter the viewport. More powerful than scroll-behavior but requires JS and adds complexity. Use for fade-in-on-scroll effects, not for anchor links.
  • prefers-contrast: more — An additional media query for users who need higher contrast. Complements prefers-reduced-motion. Add body { --text-color: #000; } inside it for forced high contrast.
  • @media (hover: hover) — Targets devices that support hover (desktops with mice) vs touch-only devices (phones, tablets). Use to add hover-only enhancements that shouldn't trigger on touch (where hover becomes a stuck tap state).
  • CSS scroll-margin — Adds offset to anchor targets so the heading isn't flush against the top of the viewport. h2 { scroll-margin-top: 2em; } pairs well with scroll-behavior: smooth.
  • :focus-visible vs :focus:focus-visible shows the focus ring only when the user navigates via keyboard (not mouse click). Reduces visual noise for mouse users while preserving accessibility for keyboard users.

Browser compatibility

  • scroll-behavior: smooth — Chrome 61+, Firefox 36+, Edge 79+. NOT supported in any version of Safari (as of Safari 18). Safari requires a JS polyfill (e.g. smoothscroll-polyfill). Falls back to instant jump — usable but less polished.
  • prefers-reduced-motion — Safari 10.1+, Chrome 74+, Firefox 63+, Edge 79+. Supported on all modern mobile browsers. The media query itself is well-established — the gotcha is remembering to apply it consistently across all animated properties.
  • @media (max-width: 600px) — Supported in every browser since CSS2.1 (IE9+). 100% universal.
  • 100vh iOS Safari bug — On iOS Safari, 100vh includes the address bar's retracted area, causing elements to extend below the visible viewport. The fix: use 100dvh (dynamic viewport height, supported in iOS 15.4+) or 100lvh, or set a fallback with JS. Not used in this tribute page, but essential knowledge for any full-height hero section you build later.
  • backdrop-filter — Safari 9+ (-webkit-backdrop-filter), Chrome 76+, Firefox 103+. iOS Safari supports it since 9.0. Unsupported browsers ignore the property entirely. Use @supports (backdrop-filter: blur(1px)) for progressive enhancement.
  • Gradient textbackground-clip: text + -webkit-text-fill-color: transparent for gradient text colours. Chrome 57+, Firefox 49+, Safari 15.4+. Not used here but relevant for future hero sections. Only works with -webkit- prefix in all current browsers.

Accessibility details

  • Skip-link must be the first focusable element — Place it as the very first child of <body>, before the <header> or navigation. If any interactive element precedes it, keyboard users must Tab through that element before reaching the skip link — defeating its purpose.
  • prefers-reduced-motion is not optional — Approximately 1 in 3 people experience motion sensitivity from scrolling animations and parallax effects. scroll-behavior: smooth is mild, but combined with animated transitions it can trigger vestibular disorders. Always include the prefers-reduced-motion override when you add any animation.
  • aria-label on nav links — The back-to-top link should have aria-label="Back to top" so screen readers announce its purpose even if the visible text is ambiguous (e.g. just an arrow symbol ↑).
  • Hover-only interactivity breaks on touch — A :hover effect that reveals information (e.g. a tooltip) doesn't work on touch devices where there is no hover state. The tribute page uses hover for visual polish only (highlight colour), not for content reveal — safe behaviour.
  • Focus indicators are required by WCAG 2.1 — Success Criterion 2.4.7 requires a visible focus indicator for all interactive elements. The default browser outline (blue ring) meets this, but custom designs often outline: none without providing a replacement — an accessibility violation.

Performance notes

  • transition: all is expensive on mobile CPUs — On low-end Android phones, a transition: all 0.3s on every .timeline li can cause visible jank during scroll. Always specify properties: transition: background-color 0.2s, transform 0.2s.
  • will-change creates compositor layers — Each will-change declaration promotes the element to its own GPU layer. On mobile, too many compositor layers exhaust GPU memory and cause checkerboarding (blank tiles during scroll). Limit to 3–5 elements per page.
  • Smooth scrolling and main-thread animationsscroll-behavior: smooth runs on the compositor thread in supporting browsers (Chrome, Firefox), not the main thread. It does not block JS execution or cause layout thrashing. This is a key advantage over JS-based smooth-scroll libraries.
  • backdrop-filter is compositor-threaded — Runs on the GPU, not the main thread. However, it's still expensive — animating it (e.g. transition: backdrop-filter 0.5s) can drop frames on integrated GPUs. Use only for static effects or smooth one-shot transitions.
  • CSS vs JS for responsive adjustments — Media queries in CSS are evaluated by the browser's layout engine with zero overhead. JS-based responsive listeners (window.matchMedia or resize events) run on every resize event and can cause main-thread congestion. Prefer CSS-only responsive patterns.

⚠️ Common pitfalls

  • Skip-link not being the first focusable element — If a nav bar, search input, or image link appears before the skip link in the DOM, keyboard users must Tab through all of them before reaching the skip link. The skip link must be literally the first child of <body>.
  • prefers-reduced-motion only partially applied — It's easy to add the media query for scroll-behavior but forget to cover transition-based animations. A common mistake: scroll-behavior: auto inside the media query, but transition: all 0.3s still fires on other elements. Always include *, *::before, *::after { transition-duration: 0.01ms !important; } inside the block.
  • 100vh on iOS causes content cut-offheight: 100vh in iOS Safari extends below the visible viewport because Safari's 100vh includes the area hidden behind the address bar. Content placed at the bottom of a 100vh element may appear cut off. Use 100dvh (iOS 15.4+) or 100svh for safe full-height sections.
  • Hover-only effects that hide content — If hover reveals information (e.g. .tooltip { display: none; } and .tooltip:hover { display: block; }), touch users cannot access it. Always provide a tap/click alternative for hover-revealed content.
  • Forgetting focus-visible for keyboard users — Custom :focus styles (like removing the outline) affect mouse clicks too. :focus-visible only shows the ring for keyboard users, keeping the design clean for mouse users while staying accessible. Supported in all modern browsers since 2022.
  • Back-to-top link without aria-label — A link with just "↑" or "Back to top" without context is ambiguous for screen readers. Always add aria-label="Back to top" or aria-label="Scroll to top of page".
  • Media query breakpoint at 600px with no touch-target sizing — WCAG 2.5.5 requires touch targets of at least 44×44px. At max-width: 600px, verify that links and buttons are large enough to tap accurately — the skip-link and back-to-top link both meet this with generous padding.

Approach

  1. Add scroll-behavior: smooth to html
  2. Add a skip-to-content link as the first element in body
  3. Write @media (max-width: 600px) to stack timeline vertically, reduce font sizes
  4. Add prefers-reduced-motion to respect user accessibility settings
  5. Add hover transitions on images and timeline rows
  6. Add a back-to-top link for long pages on mobile
Mobile CSS ▶ Run
/* Stack timeline vertically on mobile */
@media (max-width: 600px) {
  body { font-size: 16px; }
  h1 { font-size: 1.5em; }
  .timeline li {
    flex-direction: column;
    gap: 0.25em;
    align-items: flex-start;
  }
  .year-badge { min-width: 4em; }
}

/* Accessibility: skip link */
.skip-link {
  position: absolute;
  top: -100%; left: 0;
  background: #16213e;
  color: #fff;
  padding: 0.6em 1.2em;
}
.skip-link:focus { top: 0; }

/* Respect user motion preferences */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    transition-duration: 0.01ms !important;
  }
}
♿ Accessibility tip: prefers-reduced-motion is an often-overlooked media query that respects users who set their OS to reduce animation. Always include it when you add transitions or smooth scrolling.
Model
DeepSeek V4 Flash
Total Tokens
~41K
Est. Cost
$0.01
Steps
4
Tokens measured from actual output files at ~0.28 tok/byte. Input estimated from turns multiplied by system + AGENTS.md context.

✅ Tribute Page Complete

All 4 steps built. Ready to move to the next project:

02
Survey FormForm fields, validation, labels — collect user input

Lessons Learned — Build Process

The AI challenges, design insights, and pipeline improvements from building Tribute Page with AI.

🎮
Why Tribute Page Was Popular

Tribute pages succeed because semantic HTML is the foundation of the web. Before CSS frameworks and JavaScript libraries, the first thing every web developer learned was how to structure content with meaning — headings for hierarchy, paragraphs for prose, lists for grouping, images for illustration. The tribute page format is perfect for this because it's content-driven: you have a subject, a timeline, and a story to tell. The transferable principle: content structure determines accessibility and SEO. A well-structured HTML page works everywhere — screen readers, search engines, RSS readers, and text browsers — before a single line of CSS loads.

⚠️
The Problem — AI Shortcomings

The AI defaulted to complex CSS layouts before the HTML structure was solid. First pass had full hero sections with gradients and flexbox before the timeline content was even marked up. The model also struggled with semantic HTML5 elements — using generic <div> everywhere instead of <section>, <article>, <figure>. Fixing this meant rewriting the entire HTML structure because the CSS depended on the wrong selectors.

🛠️
The Fix — Pipeline Improvements

Pipelines now enforce HTML-first, CSS-last: build the semantic document structure first, verify it reads logically without styling, then add CSS. This is the same principle as mobile-first responsive design — constraints create better architecture. The tutorial steps were also restructured: Step 1 is pure HTML, Step 2 adds images and links, Step 3 introduces CSS, Step 4 polishes. No CSS before the HTML is complete.

Each app builds on the last. The bugs found in tribute-page were fixed before the next app was built — and every bug saves time on every future app.