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.
Basic HTML Structure
CompleteWhy 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 HTML —
main,section,h1-h2tell 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.
- Lists —
ulandlifor 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) plusdisplay: blockin 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 saysthe author doesn't care about structure
. - Missing
langon<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.
<!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>Image & Caption
CompleteWhy 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 singlefigure
landmark, letting users skip or inspect it as a whole.figcaption— Visible description tied to the image. Unlikealt, it's always on screen and supplements rather than replaces the alt text.altattribute — Describes the image for screen readers and when images fail to load. Every<img>must have one — even decorative images usealt=""to signalskip me
to assistive tech.max-width: 100%— Makes images responsive automatically by preventing overflow. Combined withheight: 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/figcaptionvs plaindiv+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 fixedwidth— Fluid sizing prevents horizontal scroll on mobile without media queries. Tradeoff: images can become very large on wide screens; pair withmax-width: 800pxon thefigurecontainer to cap maximum size.border-radius: 8pxwithbox-shadow— Gives the image a polished, card-like appearance without needing background images. Tradeoff:box-shadowtriggers 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+heightattributes for aspect ratio — Settingwidth="400" height="300"on the<img>tells the browser the aspect ratio before the image loads, preventing Cumulative Layout Shift (CLS). CSSmax-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 likeloading="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
altis 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), usealt=""to hide it from screen readers.figcaptionvsalt— They serve different purposes:altreplaces the image when it cannot be loaded;figcaptionis 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. Thefigcaptionitself 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: transformpromotes the element to its own compositor layer, avoiding repaints on animation. Overuse wastes GPU memory — apply it on:hoveror via JS, not permanently.- Avoid
transition: all— On hover,transition: all 0.3striggers style recalc for every animatable property. Explicitly list properties:transition: transform 0.3s, box-shadow 0.3s. - CLS prevention — Without
width/heightattributes, the browser reserves zero space for the image until it loads, then reflows the page. Set explicit dimensions on<img>and let CSSmax-width: 100%scale down.
⚠️ Common pitfalls
- Missing
altattribute — Screen readers read the image filename aloud (e.g.IMG_0042.JPG
). This is the most common and most impactful accessibility failure. - Fixed image dimensions —
width="800"withoutmax-width: 100%causes horizontal scroll on mobile. Always combine fixed dimensions with fluid CSS. - Forgetting
height: auto— Without it, an image withmax-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
srcshows the browser's broken-image icon and thealttext. Always verify image URLs resolve to 200 before deploying.
<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>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;
}Timeline & Styling
CompleteWhy 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 selectors —
body { },h1 { }style all elements of that type. Simple but powerful — they set baseline defaults before class-based overrides. - Box model —
padding,margin,border,width. Every element is a box; understanding how these properties interact (and howbox-sizing: border-boxchanges the math) is essential CSS knowledge. - Flexbox —
display: flexwithalign-itemsandgapfor 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,.eventscope 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.75emis 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-bottomseparators vsgapspacing — Lines between timeline entries provide clear visual separation and guide the eye vertically. Tradeoff: they add visual noise on short timelines. An alternative isgap-only spacing with a subtle background tint.- Dark badges (
#16213e) on warm background (#faf8f5) — High contrast for readability. The dark badge colour matches a typicalnavy
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: 4pxon 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: 5emon 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 timeline —
display: 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@mediaoverrides. ::beforepseudo-elements for decorative markers — Instead ofborder-bottom, useli::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/summaryfor 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 withmin-heightand nested flex containers — test if using complex flex layouts. gapin 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-righton 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-filtersupport — 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 vsciteattribute — The<cite>element wraps the attribution text (— Alan Turing
) and is visible to all users. Theciteattribute 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#666caption text on the same background is about 5:1 — also passing. list-style: noneand screen readers — Removing list bullets does not remove list semantics. Screen readers still announceList with 3 items
. However, some older screen readers may drop the role iflist-style: noneis combined withdisplay: flexon<li>— addingrole="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-visibleis the modern pattern for keyboard-only focus rings.
Performance notes
- Avoid
transition: all— A globaltransition: all 0.3sonaorlicauses 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 — eachwill-changecreates a new compositor layer.box-shadowrepaints — The blockquote'sbox-shadowis applied once, not animated, so it paints only on initial render. Animatingbox-shadowis expensive — avoidtransition: box-shadowunless 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: swapto prevent invisible text during load.
⚠️ Common pitfalls
list-style: noneremoving semantics — In some older browsers (particularly Safari with VoiceOver),display: flexonlicombined withlist-style: noneonulcauses the list role to be dropped. Fix: addrole="list"to the<ul>.- Forgetting
box-sizing: border-box— The defaultcontent-boxmodel 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 everybackgrounddeclaration. Use CSS custom properties from the start. gapunsupported in older Safari — The flexboxgapshorthand was late to arrive in Safari (14.1+, March 2021). If analytics show Safari 14 users, addmargin-right: 0.75emon 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 theciteattribute to identify the source. - Missing
min-widthon badges — Withoutmin-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
- Replace the plain
ulwith a class-basedul class="timeline" - Wrap each year in a
span class="year-badge"for the dark badge style - Add a
blockquotewith a meaningful quote andciteattribution - Add a footer link section with an external Wikipedia reference
- Polish typography: line-height, letter-spacing, font sizes
<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>.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; }
Responsive & Polish
CompleteWhy 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. The600pxthreshold 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 —
:hoveron 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@supportsprogressive enhancement — unsupported browsers see a solid fallback.
Design decisions & tradeoffs
max-width: 600pxbreakpoint vsmin-widthmobile-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: smoothvs 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:none —
position: absolute; top: -100%hides the link visually but keeps it in the DOM and focusable.display: noneremoves 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 !importantkills vs removing transitions — Setting duration to 0.01ms is safer than removingtransitionproperties entirely because it works regardless of how transitions were defined (inline, in a library, or in a third-party widget). The!importantensures 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 ofmax-width. Results in less override CSS and better performance on mobile (less to un-do). Recommended for greenfield projects. - JS
Intersection Observerfor scroll animations — Triggers transitions when elements enter the viewport. More powerful thanscroll-behaviorbut 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. Complementsprefers-reduced-motion. Addbody { --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 (wherehover
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 withscroll-behavior: smooth. :focus-visiblevs:focus—:focus-visibleshows 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.100vhiOS Safari bug — On iOS Safari,100vhincludes the address bar's retracted area, causing elements to extend below the visible viewport. The fix: use100dvh(dynamic viewport height, supported in iOS 15.4+) or100lvh, 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 text —
background-clip: text+-webkit-text-fill-color: transparentfor 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-motionis not optional — Approximately 1 in 3 people experience motion sensitivity from scrolling animations and parallax effects.scroll-behavior: smoothis mild, but combined with animated transitions it can trigger vestibular disorders. Always include theprefers-reduced-motionoverride when you add any animation.aria-labelon nav links — The back-to-top link should havearia-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
:hovereffect 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: nonewithout providing a replacement — an accessibility violation.
Performance notes
transition: allis expensive on mobile CPUs — On low-end Android phones, atransition: all 0.3son every.timeline lican cause visible jank during scroll. Always specify properties:transition: background-color 0.2s, transform 0.2s.will-changecreates compositor layers — Eachwill-changedeclaration 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 animations —
scroll-behavior: smoothruns 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-filteris 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.matchMediaorresizeevents) 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-motiononly partially applied — It's easy to add the media query forscroll-behaviorbut forget to cover transition-based animations. A common mistake:scroll-behavior: autoinside the media query, buttransition: all 0.3sstill fires on other elements. Always include*, *::before, *::after { transition-duration: 0.01ms !important; }inside the block.100vhon iOS causes content cut-off —height: 100vhin iOS Safari extends below the visible viewport because Safari's100vhincludes the area hidden behind the address bar. Content placed at the bottom of a100vhelement may appear cut off. Use100dvh(iOS 15.4+) or100svhfor 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-visiblefor keyboard users — Custom:focusstyles (like removing the outline) affect mouse clicks too.:focus-visibleonly 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 addaria-label="Back to top"oraria-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
- Add
scroll-behavior: smoothtohtml - Add a skip-to-content link as the first element in
body - Write
@media (max-width: 600px)to stack timeline vertically, reduce font sizes - Add
prefers-reduced-motionto respect user accessibility settings - Add hover transitions on images and timeline rows
- Add a back-to-top link for long pages on mobile
/* 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;
}
}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.Lessons Learned — Build Process
The AI challenges, design insights, and pipeline improvements from building Tribute Page with AI.
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 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.
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.