TutorialBeginner

I Built a Profile Card With Zero Divs — Here's the Challenge

15 min read

Prerequisites

  • A code editor (VS Code recommended)
  • A modern browser (Chrome, Firefox, or Edge)
  • Basic understanding of what HTML tags and CSS properties are
HTML5CSS3Semantic HTMLFlexboxCSS Custom Properties

The Constraint

⚠️The Rule
Zero <div> tags allowed. Every piece of content must use a semantic HTML5 element. If you reach for a <div>, stop and find the right element.

I used to wrap everything in <div> tags. It works, but it tells the browser — and screen readers — nothing about the content. So I gave myself this constraint to learn the right element for each role. By the end, I used 7 semantic elements, 5 CSS selector types, and finally understood when to use rem vs em vs px. Here's the full walkthrough — and the challenge for you to try it yourself.

What I Built

A centered profile card on a dark page — white rectangle, max-width 480px, rounded corners, shadow. It displays a developer's photo, bio, skills, and contact links. Here's the final result:

💡Live Demo
See the finished profile card here — circular photo, bio section, skill pill badges, contact links, and a footer. All built with zero <div> tags. Think you can build it? Follow along.

I broke the card into these sections from top to bottom: a header with the photo, name, and title. An About section with a short bio. A Skills section with wrapped pill badges. A Fun Fact callout box. A Contact section with email and social links. And a footer with copyright text. Each separated by a thin border divider.

I used these 8 semantic elements — no <div> anywhere:

ElementMeaningWhere
<main>Primary page content (one per page)Wraps the entire card
<article>Self-contained contentThe profile card itself
<header>Introductory content for its parentPhoto + name + title
<section>Thematic grouping with a headingAbout, Skills sections
<aside>Tangentially related contentFun fact callout
<nav>Navigation linksContact links
<footer>Footer for its parentCopyright notice
<figure>Self-contained mediaProfile photo

Step 1 — Project Setup + CSS Reset

I started with two files: index.html and styles.css. The foundation — boilerplate every project starts with.

Challenge: Try writing the HTML boilerplate yourself — with lang="en", the viewport meta tag, and a linked stylesheet. Then check my version below.

Design Tokens

I defined every color, size, and spacing value as CSS custom properties upfront. This is the full design token block I used:

💡Why This Matters
lang="en" — Screen readers use this to choose pronunciation rules. Without it, a French screen reader might read English with French pronunciation. box-sizing: border-box — Without it, width: 200px + padding: 20px + border: 2px = 244px total. With border-box, 200px means 200px. viewport meta — Without it, mobile browsers render at ~980px width and zoom out.
Checkpoint
At this point, opening index.html shows a blank white page. That's correct — the reset is working.

Step 2 — Page Background + Card Centering

Next I needed the dark navy page with a white card centered in the middle. Flexbox does the heavy lifting here. I wrapped the content in <main> and <article> — no <div> needed.

💡Why This Matters
<main> — Every page must have exactly one. Tells screen readers "this is the primary content" so users can skip directly to it. <article> — Self-contained content that could be extracted and placed on another page and still make sense. That's the test for using <article>. min-height: 100vh — 100vh means "at least as tall as the browser window." Combined with Flexbox centering, the card is perfectly centered. We use min-height (not height) so the page can grow if the content is taller than the viewport. rem for fonts, px for border-radius — rem scales with user font preferences (accessibility). Border-radius is decorative — you want exactly 16px regardless of font settings.
Checkpoint
Dark navy page with a white rounded rectangle centered in the middle. Empty for now — but the structure is solid.

Step 3 — Card Header (Photo, Name, Title)

The introductory part of the card: a circular photo, bold name, and muted job title. I used <header> for the intro and <figure> for the photo, with border-radius: 50% to make it circular.

💡Why This Matters
<header> inside <article> — It's not just for the page top. It means "introductory content for its parent." The photo and name introduce the card. <figure> — Wraps self-contained media. Semantically more correct than a bare <img> — the portrait is a meaningful standalone element. object-fit: cover — The source is 300×300, displayed at 160×160. cover fills and crops. Without it, a non-square source would be stretched. border-radius: 50% — On a square element (equal width and height), 50% creates a perfect circle.
Checkpoint
White card with a circular photo centered at the top, "Sarah Chen" in bold below, "Full-Stack Developer" in gray.

Step 4 — About Section

A thematic grouping with a heading — exactly what <section> is for. I styled the heading as a small uppercase label to act as a section marker.

💡section vs article
A <section> is a thematic grouping that needs its parent for context. The "About" section only makes sense as part of this card. An <article> stands alone — you could paste it on any other page and it still makes sense.
💡letter-spacing in em, not rem
em is relative to the element's own font size. If you change the h2 font-size, letter-spacing scales proportionally. rem would stay fixed relative to the root, potentially looking too tight or too loose.

Step 5 — Section Dividers (The + Combinator)

No extra HTML needed here. I used the adjacent sibling combinator in CSS to add borders between sections without giving the first section an unwanted top border.

💡Why the + combinator?
section + section means "a section that directly follows another section." It skips the first one — no unwanted top border on About. Borders in px: Borders should be exactly 1px. Using rem could round to 0px or 2px at different font sizes.

Step 6 — Skill Pills (Flexbox + nth-child)

This was a fun one — pill badges that wrap to new lines, with alternating styles using :nth-child. No extra classes needed in the HTML.

💡Why This Matters
Pill padding in em — em is relative to the element's own font size (0.8125rem here). If you change the font, padding scales proportionally — the pill stays balanced. border-radius: 9999px — Any value larger than half the element's height = fully rounded pill. 9999px works regardless of height. :nth-child(even) — Selects every 2nd item (2, 4, 6, 8). Creates visual variety with no extra classes. Pure CSS.
Checkpoint
"SKILLS" heading with 8 pills — odd ones filled gray, even ones outlined red. Hover turns any pill solid red. A divider line now appears between About and Skills.

Step 7 — Fun Fact Aside

I wanted a fun fact callout — content tangentially related to the main profile. <aside> is the semantic element for exactly this. Screen readers identify it as supplementary content that users can skip.

💡When to use <aside>
Tangentially related content — a fun fact, a callout, a sidebar tip. The left accent bar (border-left: 3px solid) is a common visual pattern for callouts.

Step 8 — Contact Links (Attribute Selectors)

I styled links differently based on their href attribute — no extra classes needed. The email link turns red automatically using [href^="mailto:"]. This was one of the more satisfying CSS tricks in the build.

💡Why This Matters
<nav> for contact links — The spec says <nav> is for "a section that links to other pages or parts within the page." Contact links navigate to external pages — that qualifies. Attribute selectors [href^="mailto:"] — ^= means "starts with." The email link gets red styling automatically. If you add another mailto link later, it's styled automatically too. :focus styling — Keyboard users navigate with Tab. Without :focus, they can't see where they are. This is a WCAG 2.1 accessibility requirement.
Checkpoint
"CONTACT" heading with 4 links. Email is red, others are gray. Hovering turns them dark red. Tab through them — you should see a red outline around the focused link.

Step 9 — Footer

<footer> isn't limited to the page bottom. It represents footer content for its nearest sectioning ancestor — here, the <article>. The card's copyright belongs inside the card's footer.

Step 10 — Card Hover + Mobile Responsiveness

The finishing touches: I added a deeper shadow on hover (the transition was already set in Step 2), and responsive adjustments for mobile.

💡Why This Matters
align-items: flex-start on mobile — On short mobile screens, a vertically centered card gets cut off at both top and bottom. flex-start pins it to the top so users scroll down naturally. max-width: 767px (desktop-first) — Base CSS is for desktop, mobile overrides with reduced sizes. Mobile-first (min-width) is equally valid — you should be comfortable with either.
You did it!
That's the complete build — circular photo, semantic HTML, pill badges, contact links, responsive. Zero divs. All semantic. If you followed along or tried it yourself, check your result against the acceptance criteria below.
* * *

Acceptance Criteria

Here's the checklist I used to verify the build:

  • Zero <div> tags in the HTML
  • Uses all 7 semantic elements: <header>, <main>, <article>, <section> (×2), <aside>, <nav>, <footer>
  • Uses <figure> with <img> for the profile photo with meaningful alt text
  • 5 CSS selector types: class, descendant, pseudo-class, attribute, combinator
  • Hover effects on card, links, and skill pills
  • :focus styles on all links (keyboard accessible)
  • Uses rem for fonts, em for pill padding, px for borders/shadows, % for widths, vh for body height
  • All values use CSS custom properties from :root
  • Responsive: fills width on mobile (< 768px), centered with max-width on desktop
  • Valid HTML — passes W3C Validator

Quick Reference

CSS Units — When to Use What

UnitRelative ToUse ForExample
remRoot font size (16px)Font sizes, spacingfont-size: 1.75rem
emElement's own font sizeComponent-scoped paddingpadding: 0.375em
pxAbsoluteBorders, shadows, decorativeborder: 1px solid
%Parent dimensionWidths, radiuswidth: 100%
vhViewport heightFull-page layoutsmin-height: 100vh

CSS Selector Types Used

TypeSyntaxExample
Class.name.profile-card, .skills-list
DescendantA B.profile-card header h1
Pseudo-class:state:hover, :focus, :nth-child(even)
Attribute[attr]a[href^="mailto:"]
Adjacent siblingA + Bsection + section
* * *

Bonus Challenges

Finished the main build? Here are some extra challenges I've been experimenting with — no JavaScript allowed for any of them.

1. Dark Mode with :has()

Add a <details><summary>Toggle Dark Mode</summary></details> at the top. Use body:has(details[open]) to swap CSS variables to dark colors. Zero JavaScript.

2. Print Stylesheet

Add @media print { } that hides the photo, removes backgrounds, converts shadows to borders, and makes all text black.

3. Animated Entrance

Use @keyframes fadeSlideUp with opacity: 0 → 1 and translateY(20px) → 0. Apply to the card on page load.

4. Link Icons via ::before

Add pseudo-elements with icons (✉, 💻, 🔗, 🌐) before each contact link. Give them a fixed width so icons align vertically.

5. Skill Proficiency Bars

Add a <meter> element after each skill. Style it to show a colored progress bar (React: 90%, Python: 60%, etc).