Back to Articles
TutorialsPublished August 22, 202614 min read

Web Foundations Lab: Build Your First Profile Card with HTML, CSS, and JavaScript

"A beginner-focused, hands-on lab that teaches the fundamentals of HTML, CSS, and JavaScript by building a functional, styled, and interactive developer profile card from scratch."
Raphael Johnathan F. Flores

Raphael Johnathan F. Flores

2x AWS Certified Cloud Architect

#HTML#CSS#JavaScript#Web Foundations#Beginner#Hands-On Lab
Web Foundations Lab: Build Your First Profile Card with HTML, CSS, and JavaScript

What We Are Building

In this lab, you will build a Developer Profile Card — a small but complete web component that shows a profile photo, a name, a role, a list of skills, and a contact button that reveals your email on click. No frameworks. No build tools. Just a text editor and a browser.

By the end of this lab you will understand:

No prior experience required. All you need is VS Code and a browser.


Project Setup

Create a new folder called profile-card/ and open it in VS Code. Inside, create three files:

profile-card/
  index.html
  styles.css
  script.js

Step 1 — HTML: The Skeleton

HTML is a markup language — it describes the structure and meaning of content. Open index.html and write the following:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Developer Profile Card</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>

    <main class="page-center">
      <article class="profile-card" id="profile-card">

        <!-- Avatar -->
        <figure class="avatar-wrapper">
          <img
            class="avatar"
            src="https://api.dicebear.com/8.x/initials/svg?seed=RF"
            alt="Profile avatar for Raphael Flores"
            width="96"
            height="96"
          />
        </figure>

        <!-- Identity -->
        <div class="identity">
          <h1 class="name">Raphael Flores</h1>
          <p class="role">Cloud Architect &amp; Front-End Developer</p>
        </div>

        <!-- Skills -->
        <ul class="skills-list" aria-label="Technical skills">
          <li class="skill-tag">HTML</li>
          <li class="skill-tag">CSS</li>
          <li class="skill-tag">JavaScript</li>
          <li class="skill-tag">AWS</li>
        </ul>

        <!-- Contact button -->
        <button class="contact-btn" id="contact-btn" type="button">
          Show Contact
        </button>

        <!-- Contact info — hidden by default -->
        <p class="contact-info hidden" id="contact-info">
          [email protected]
        </p>

      </article>
    </main>

    <script src="script.js"></script>
  </body>
</html>

What This Markup Establishes

Element Role
<!DOCTYPE html> Declares the document as HTML5 — always the first line.
<html lang="en"> Root element. lang attribute supports screen readers and search engines.
<meta charset="UTF-8"> Enables full Unicode character support.
<meta name="viewport"> Makes the page render correctly on mobile devices.
<link rel="stylesheet"> Connects the external CSS file.
<main> Semantic landmark for the page’s primary content.
<article> A self-contained piece of content — semantically correct for a profile card.
<figure> Wraps the avatar image as a self-contained media element.
<h1> One per page. The top-level heading — the person’s name in this case.
<ul> + <li> An unordered list for skills — semantically better than <div> tags.
<button> The correct element for an interactive action. Never use <div> as a button.
id="..." Unique identifiers used by JavaScript to select specific elements.
class="hidden" A CSS class we will define to toggle visibility with JavaScript.

Notice that the <script> tag is at the bottom of <body>, not inside <head>. This ensures the HTML is fully parsed and all elements exist in the DOM before JavaScript tries to select them.


Step 2 — CSS: The Visual Layer

CSS controls how every element looks. Open styles.css.

The CSS Reset and Custom Properties

Every browser applies its own default styles. We reset these to start from a clean, predictable baseline, then define our design tokens as CSS custom properties:

/* Reset */
*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

/* Design tokens */
:root {
  --bg-page:        #f1f5f9;
  --bg-card:        #ffffff;
  --color-text:     #0f172a;
  --color-muted:    #64748b;
  --color-accent:   #2563eb;
  --color-tag-bg:   #eff6ff;
  --color-tag-text: #1d4ed8;
  --font-sans:      'Segoe UI', system-ui, sans-serif;
  --font-mono:      'Consolas', 'Courier New', monospace;
  --radius-card:    16px;
  --radius-tag:     6px;
  --shadow-card:    0 4px 24px rgba(0, 0, 0, 0.08);
  --transition:     200ms ease;
}

Custom properties (variables) defined on :root are available everywhere in the stylesheet. Change --color-accent once and every element that references it updates automatically.

Page Layout

body {
  font-family: var(--font-sans);
  background-color: var(--bg-page);
  color: var(--color-text);
  min-height: 100vh;
}

/* Flexbox — centers the card vertically and horizontally */
.page-center {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  padding: 2rem;
}

display: flex turns .page-center into a flex container. justify-content: center centers children on the horizontal axis. align-items: center centers them on the vertical axis.

The Card

.profile-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.25rem;
  background-color: var(--bg-card);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card);
  padding: 2.5rem 2rem;
  width: 100%;
  max-width: 360px;
  text-align: center;
  transition: box-shadow var(--transition);
}

/* Elevate the card on hover for a subtle interactive feel */
.profile-card:hover {
  box-shadow: 0 8px 40px rgba(0, 0, 0, 0.12);
}

flex-direction: column stacks children vertically. gap adds consistent spacing between each child — no manual margins needed.

Avatar, Identity, Skills, and Button

/* Avatar */
.avatar-wrapper {
  margin: 0;
}

.avatar {
  width: 96px;
  height: 96px;
  border-radius: 50%;
  border: 3px solid var(--color-accent);
  display: block;
}

/* Identity */
.name {
  font-size: 1.375rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--color-text);
}

.role {
  font-size: 0.875rem;
  color: var(--color-muted);
  margin-top: 0.25rem;
}

/* Skills list — horizontal flexbox row */
.skills-list {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.5rem;
  list-style: none;
}

.skill-tag {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  font-weight: 600;
  background-color: var(--color-tag-bg);
  color: var(--color-tag-text);
  padding: 0.25rem 0.75rem;
  border-radius: var(--radius-tag);
  border: 1px solid #bfdbfe;
}

/* Contact button */
.contact-btn {
  font-family: var(--font-sans);
  font-size: 0.875rem;
  font-weight: 600;
  color: #ffffff;
  background-color: var(--color-accent);
  border: none;
  border-radius: 8px;
  padding: 0.625rem 1.5rem;
  cursor: pointer;
  transition: background-color var(--transition), transform var(--transition);
  width: 100%;
}

.contact-btn:hover {
  background-color: #1d4ed8;
}

.contact-btn:active {
  transform: scale(0.97);
}

/* Contact info */
.contact-info {
  font-family: var(--font-mono);
  font-size: 0.875rem;
  color: var(--color-accent);
}

/* Utility class — toggled by JavaScript */
.hidden {
  display: none;
}

Notice the .hidden class at the bottom. It has only one job: display: none. JavaScript will add and remove this class to show and hide the contact email.


Step 3 — JavaScript: The Behavior Layer

JavaScript runs in the browser and can read, modify, and respond to everything on the page. Open script.js.

// 1. Select the elements we need
const contactBtn  = document.getElementById('contact-btn');
const contactInfo = document.getElementById('contact-info');

// 2. Track whether the contact info is visible
let isVisible = false;

// 3. Respond to a button click
contactBtn.addEventListener('click', () => {
  isVisible = !isVisible;   // toggle the boolean

  if (isVisible) {
    contactInfo.classList.remove('hidden');
    contactBtn.textContent = 'Hide Contact';
  } else {
    contactInfo.classList.add('hidden');
    contactBtn.textContent = 'Show Contact';
  }
});

What Each Line Does

Line 1-2 — DOM Selection: document.getElementById finds an element by its id attribute. This returns a reference to the live DOM element — any changes to it are immediately reflected on the page.

Line 5 — State Variable: let isVisible = false is a state variable — a value that tracks what condition the UI is currently in. This is the core concept behind every interactive UI in any framework.

Line 8 — Event Listener: addEventListener('click', callback) registers a function to run every time the button is clicked. The callback is an arrow function () => { ... }.

Line 9 — Toggle Logic: isVisible = !isVisible flips the boolean with the ! (NOT) operator. true becomes false and vice versa on every click.

Lines 11-16 — DOM Update: classList.remove('hidden') makes the email visible by removing the class that hides it. classList.add('hidden') re-hides it. textContent updates the button label to match the current state.

Adding a Skill Dynamically

Let’s add one more interactive feature — the ability to add a new skill by pressing Enter in a text input. Add this below the existing code in script.js:

// 4. Dynamic skill addition
const skillsList = document.querySelector('.skills-list');

// Create an input and add-button for new skills
const addSkillInput = document.createElement('input');
addSkillInput.type        = 'text';
addSkillInput.placeholder = 'Add a skill...';
addSkillInput.className   = 'skill-input';

const addSkillBtn = document.createElement('button');
addSkillBtn.type      = 'button';
addSkillBtn.textContent = 'Add';
addSkillBtn.className = 'add-skill-btn';

// Insert them after the skills list
skillsList.insertAdjacentElement('afterend', addSkillBtn);
skillsList.insertAdjacentElement('afterend', addSkillInput);

// Listen for the Add button click
addSkillBtn.addEventListener('click', () => {
  const value = addSkillInput.value.trim();

  if (!value) return;   // do nothing if input is empty

  const newTag = document.createElement('li');
  newTag.className = 'skill-tag';
  newTag.textContent = value;
  skillsList.appendChild(newTag);

  addSkillInput.value = '';  // clear the input after adding
  addSkillInput.focus();     // return focus to the input for quick re-entry
});

// Also allow pressing Enter to add
addSkillInput.addEventListener('keydown', (event) => {
  if (event.key === 'Enter') {
    addSkillBtn.click();
  }
});

Add the styles for the input and add-button at the bottom of styles.css:

/* Skill input and add-button */
.skill-input {
  font-family: var(--font-sans);
  font-size: 0.875rem;
  border: 1px solid #cbd5e1;
  border-radius: 8px;
  padding: 0.5rem 0.75rem;
  width: 100%;
  outline: none;
  transition: border-color var(--transition);
}

.skill-input:focus {
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
}

.add-skill-btn {
  font-family: var(--font-sans);
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--color-accent);
  background-color: var(--color-tag-bg);
  border: 1px solid #bfdbfe;
  border-radius: 8px;
  padding: 0.5rem 1rem;
  cursor: pointer;
  width: 100%;
  transition: background-color var(--transition);
}

.add-skill-btn:hover {
  background-color: #dbeafe;
}

What You Built and What It Demonstrates

Feature Technology Concept Learned
Page structure and semantic elements HTML Document anatomy, semantic tags, accessibility attributes
Centered card layout CSS Flexbox display: flex, justify-content, align-items
Design tokens and reusable values CSS Custom Properties :root variables, var()
Hover and active states CSS Pseudo-classes :hover, :active, transition
Show/hide contact info JavaScript DOM getElementById, classList, textContent
Toggle state management JavaScript Boolean state variable, ! operator
Dynamic element creation JavaScript DOM createElement, appendChild, insertAdjacentElement
Keyboard accessibility JavaScript Events keydown, event.key

Key Principles to Carry Forward

This profile card is your first full-stack front-end project. From here, the progression is: Git for version control, Tailwind CSS for utility-first styling, Vue or React for component-driven architecture, and AWS for deployment.

Explore All Articles