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:
- How HTML provides structure and meaning.
- How CSS controls layout and visual design.
- How JavaScript adds interactivity and dynamic behavior.
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 & 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
- HTML describes what content is. CSS describes how it looks. JavaScript describes how it behaves.
- Never use
<div>for interactive actions — use<button>. Never use<div>for navigation — use<nav>. - CSS custom properties are your design token system. Define once, use everywhere.
box-sizing: border-boxprevents layout surprises — always set it globally.- JavaScript state is a variable that tracks the current condition of your UI. Every feature you build will have some form of state.
- Keep your HTML clean and let JavaScript manipulate classes — this separates concerns and keeps code maintainable.
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.
