Arkendia Classrooms
Lesson 20 of 21 · 25 min

Building the complete portfolio, step by step

This is the lesson where everything comes together. Following your brief, you will build the whole portfolio in six small steps, with every piece of code explained. Type the code rather than pasting it if you can, because typing is how it sticks. Replace the sample content with your own, and adapt the colors to your palette. At each step, save and look at the result in your browser.

Before you start

You should have your portfolio folder with an index.html, a css/style.css and your images in images/. If you used the earlier lessons’ practice files, you can start fresh: it is easier to follow with a clean slate. We will build a single page. The full source of both files is provided across the steps below.

Step 1: the HTML skeleton and head

Replace the contents of index.html with this base. Notice everything from the previous chapters: language, encoding, viewport, a unique title and description, a favicon, and the stylesheet.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Maya Carter – Photographer and illustrator</title>
  <meta name="description" content="Portfolio of Maya Carter, photographer and illustrator: street portraits, botanical drawings and commissions.">
  <link rel="icon" href="favicon.svg" type="image/svg+xml">
  <link rel="stylesheet" href="css/style.css">
</head>
<body>
  <!-- Steps 2 to 5 go here -->
</body>
</html>

Step 2: header and hero

Inside body, start with the skip link, the header with its navigation, then the hero section in main.

<a class="skip-link" href="#main">Skip to main content</a>

<header class="site-header">
  <a class="site-header__logo" href="index.html">Maya Carter</a>
  <nav aria-label="Main">
    <ul class="menu">
      <li><a href="#projects">Projects</a></li>
      <li><a href="#about">About</a></li>
      <li><a href="#contact">Contact</a></li>
    </ul>
  </nav>
</header>

<main id="main">
  <section class="hero container">
    <div class="hero__text">
      <p class="eyebrow">Photographer and illustrator</p>
      <h1>I tell stories with light and ink.</h1>
      <p>Based in Lyon, I take portraits and draw plants. Take a look at my recent work or tell me about your project.</p>
      <a class="button" href="#contact">Work with me</a>
    </div>
    <img class="hero__image" src="images/hero.jpg" alt="Maya photographing a market stall at sunrise" width="900" height="700">
  </section>
  <!-- Step 3 goes here -->
</main>

Points to notice: the logo is a link to the home page; the menu is a list inside a labeled nav; there is exactly one h1; the hero image has dimensions so the layout will not jump, and it is not lazy-loaded because it is visible at once. The classes follow the BEM convention from chapter 5.

Step 3: the projects gallery

Still inside main, after the hero:

<section class="section" id="projects">
  <div class="container">
    <h2>Projects</h2>
    <div class="gallery">
      <article class="card">
        <img class="card__image" src="images/project-1.jpg" alt="A cyclist crossing a rainy street at dusk" width="800" height="533" loading="lazy">
        <div class="card__body">
          <h3 class="card__title">Street portraits</h3>
          <p>A series of portraits of people I meet in my neighborhood.</p>
        </div>
      </article>
      <article class="card">
        <img class="card__image" src="images/project-2.jpg" alt="Ink drawing of a fern leaf" width="800" height="533" loading="lazy">
        <div class="card__body">
          <h3 class="card__title">Botanical drawings</h3>
          <p>Ink drawings of plants from the local market.</p>
        </div>
      </article>
      <article class="card">
        <img class="card__image" src="images/project-3.jpg" alt="Wedding rings on a wooden table" width="800" height="533" loading="lazy">
        <div class="card__body">
          <h3 class="card__title">Small celebrations</h3>
          <p>Relaxed photographs for weddings and family events.</p>
        </div>
      </article>
    </div>
  </div>
</section>
<!-- Step 4 goes here -->

Each project is an article because it is self-contained. The three images sit below the fold, so they are lazy-loaded.

Step 4: about and contact

<section class="section section--tinted" id="about">
  <div class="container about">
    <img class="about__photo" src="images/about.jpg" alt="Portrait of Maya holding a camera" width="600" height="600" loading="lazy">
    <div>
      <h2>About me</h2>
      <p>I have been working with images for ten years. I like honest light, quiet streets and plants that nobody notices.</p>
      <p>I work with individuals and small businesses, and I always start by listening.</p>
    </div>
  </div>
</section>

<section class="section" id="contact">
  <div class="container">
    <h2>Contact</h2>
    <p>Tell me about your project and I will reply within a few days.</p>
    <form class="form" action="#" method="post">
      <div class="form__field">
        <label for="c-name">Your name</label>
        <input type="text" id="c-name" name="name" required autocomplete="name">
      </div>
      <div class="form__field">
        <label for="c-email">Your email</label>
        <input type="email" id="c-email" name="email" required autocomplete="email">
      </div>
      <div class="form__field">
        <label for="c-message">Your message</label>
        <textarea id="c-message" name="message" rows="6" required minlength="10"></textarea>
      </div>
      <button class="button" type="submit">Send message</button>
    </form>
  </div>
</section>

Step 5: the footer

After the closing </main> tag:

<footer class="site-footer">
  <p>&copy; Maya Carter. All rights reserved.</p>
</footer>

At this stage the page works but is unstyled. Check the structure: the outline of headings, the menu links jumping to sections, and the form validation messages. Fix any structural issue now, before styling.

Step 6: the CSS, in layers

Write css/style.css from top to bottom, in the organization from chapter 5. First, variables, reset and base styles:

/* 1. Variables */
:root {
  --color-primary: #0f766e;
  --color-primary-dark: #115e59;
  --color-accent: #f59e0b;
  --color-text: #1f2937;
  --color-bg: #f9fafb;
  --color-surface: #ffffff;
  --color-tint: #ecfdf5;
  --color-border: #e2e8f0;
  --radius: 0.75rem;
  --shadow: 0 2px 8px rgb(0 0 0 / 12%);
  --font-body: system-ui, "Segoe UI", Arial, sans-serif;
  --font-heading: Georgia, "Times New Roman", serif;
}

/* 2. Base */
*, *::before, *::after { box-sizing: border-box; }
body {
  margin: 0;
  font-family: var(--font-body);
  line-height: 1.6;
  color: var(--color-text);
  background-color: var(--color-bg);
}
img { max-width: 100%; height: auto; display: block; }
h1, h2, h3 {
  font-family: var(--font-heading);
  line-height: 1.2;
}
h1 { font-size: clamp(2rem, 1.2rem + 3vw, 3.25rem); }
h2 { font-size: clamp(1.5rem, 1.1rem + 1.5vw, 2.25rem); }
a { color: var(--color-primary); }
:focus-visible {
  outline: 3px solid var(--color-primary);
  outline-offset: 3px;
}
html { scroll-behavior: smooth; }
section[id] { scroll-margin-top: 5rem; }

Everything reusable lives in variables. Headings scale fluidly thanks to clamp(), focus is visible, and anchor jumps leave room for the sticky header.

Next, layout helpers and the header:

/* 3. Layout */
.container {
  width: 90%;
  max-width: 64rem;
  margin: 0 auto;
}
.section { padding-block: clamp(2.5rem, 6vw, 4.5rem); }
.section--tinted { background-color: var(--color-tint); }

.skip-link {
  position: absolute;
  left: 0.5rem;
  top: -4rem;
  padding: 0.5rem 1rem;
  background: var(--color-surface);
  z-index: 100;
}
.skip-link:focus { top: 0.5rem; }

.site-header {
  position: sticky;
  top: 0;
  z-index: 10;
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: 0.5rem 1rem;
  padding: 0.75rem 5%;
  background-color: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
}
.site-header__logo {
  font-family: var(--font-heading);
  font-size: 1.25rem;
  font-weight: 700;
  text-decoration: none;
  color: var(--color-text);
}
.menu {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1rem;
  list-style: none;
  margin: 0;
  padding: 0;
}
.menu a {
  display: inline-block;
  padding: 0.5rem 0.25rem;
  text-decoration: none;
  color: var(--color-text);
}
.menu a:hover { color: var(--color-primary); text-decoration: underline; }

Then the components: hero, button, cards, about, form and footer:

/* 4. Components */
.hero {
  display: grid;
  gap: 2rem;
  padding-block: clamp(2rem, 6vw, 4.5rem);
}
.eyebrow {
  margin: 0 0 0.5rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 0.875rem;
  color: var(--color-primary-dark);
}
.hero__image { border-radius: var(--radius); }

.button {
  display: inline-block;
  padding: 0.75rem 1.5rem;
  border: 0;
  border-radius: 999px;
  background-color: var(--color-primary);
  color: #ffffff;
  font: inherit;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
  transition: background-color 0.2s ease, transform 0.2s ease;
}
.button:hover { background-color: var(--color-primary-dark); transform: translateY(-2px); }

.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: 1.5rem;
}
.card {
  overflow: hidden;
  background-color: var(--color-surface);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  transition: box-shadow 0.2s ease, transform 0.2s ease;
}
.card:hover { box-shadow: 0 8px 20px rgb(0 0 0 / 18%); transform: translateY(-3px); }
.card__image { width: 100%; aspect-ratio: 3 / 2; object-fit: cover; }
.card__body { padding: 1rem 1.25rem 1.5rem; }
.card__title { margin: 0 0 0.5rem; }

.about { display: grid; gap: 2rem; align-items: center; }
.about__photo { width: 100%; max-width: 20rem; border-radius: 50%; }

.form { display: grid; gap: 1.25rem; max-width: 36rem; }
.form__field { display: grid; gap: 0.35rem; }
.form input, .form textarea {
  padding: 0.7rem 0.9rem;
  border: 1px solid #94a3b8;
  border-radius: 0.5rem;
  font: inherit;
}
.form input:invalid:not(:placeholder-shown),
.form textarea:invalid:not(:placeholder-shown) { border-color: #b91c1c; }

.site-footer {
  padding: 1.5rem 5%;
  text-align: center;
  background-color: #134e4a;
  color: #ffffff;
}
.site-footer p { margin: 0; }

Finally, the responsive enhancements and the reduced-motion respect:

/* 5. Responsive */
@media (min-width: 50em) {
  .hero { grid-template-columns: 1.1fr 1fr; align-items: center; }
  .about { grid-template-columns: 20rem 1fr; }
}

/* 6. Preferences */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation: none !important;
    transition: none !important;
  }
}

How the pieces fit together

  • The header is a Flexbox container that wraps on narrow screens, and stays visible thanks to position: sticky.
  • The hero is a Grid: a single column by default, two columns from 50em (mobile first).
  • The gallery is a Grid with auto-fit and minmax, so it adapts without media queries.
  • Cards use BEM names, variables for the look and a subtle transition.
  • The form uses native validation, visible labels and a clear focus style.
  • Accessibility comes from semantic elements, the skip link, contrast and focus styles, alternative texts and reduced motion.

Testing as you go

  1. Resize the window from very narrow to very wide, and look for overflow or awkward spacing.
  2. Navigate the whole page by keyboard: the skip link, menu, button and form fields.
  3. Open the Network panel and make sure there is no red 404 (an image path or the favicon).
  4. Inspect a card and check which rules apply and which variables are used.

Tip. Make one change at a time and save often. If something breaks, undo the last change: it is usually a missing brace, semicolon or closing tag. The Elements panel and the HTML validator are your safety net.

Warning. The form in this page is not connected to anything: submitting it just reloads the page. In the next lesson we discuss options to receive messages, and for now it is a perfectly valid demonstration of structure and validation. Do not advertise it as working until you connect it.

Your turn

  1. Build the page following the six steps, with your own content, colors and images.
  2. Check that the headings outline is logical: one h1, h2 for each section, h3 for each card.
  3. Customize at least three things: the palette, one font and the card layout (for example a featured card that spans two columns on wide screens).
  4. Test on a narrow window, a wide window and with the keyboard only.
  5. Validate the HTML with the W3C checker and fix errors.
See a solution

If your page does not look like you expect, compare it step by step with the code above. The most common issues are:

  • No styles at all: the path in the link element is wrong; check the Network panel for a 404 on style.css.
  • Missing images: file names or extensions do not match. Remember lowercase names.
  • The gallery shows one column only: the three article elements must be direct children of the .gallery container.
  • The hero does not split in two: widen your window beyond 50em (800 pixels at default text size), and make sure the media query is not inside another rule.

To make the first card span two columns on wide screens, add to the end of your responsive section:

@media (min-width: 64em) {
  .card:first-child { grid-column: span 2; }
}

Key takeaways

  • Build in order: head, structure, content, then CSS from general (variables, base) to specific (components, responsive).
  • Semantic HTML, BEM classes and CSS variables keep the code readable and easy to change.
  • Mobile first with Grid and Flexbox, plus fluid sizes, gives a layout that adapts to any screen.
  • Accessibility is built in: skip link, labels, focus styles, contrast, alt text and reduced motion.
  • Test continuously: resizing, keyboard, Network panel and the HTML validator.
Advertisement
Log in to save your progress. Log in