I Built a Profile Card With Zero Divs — Here's the Challenge
Prerequisites
- A code editor (VS Code recommended)
- A modern browser (Chrome, Firefox, or Edge)
- Basic understanding of what HTML tags and CSS properties are
The Constraint
<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, CSS Grid for skills, :has() for dark mode, <meter> for skill proficiency bars, and a print stylesheet — all with zero JavaScript. 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, animated entrance. It features a dark mode toggle (no JS), skill proficiency meters, link icons via ::before pseudo-elements, and a print stylesheet. Here's the final result:
I broke the card into these sections from top to bottom: a dark mode toggle using <details>. A header with the photo, name, and title. An About Me section with a short bio. A Skills section with a 4-column grid of icons, names, and <meter> proficiency bars. A Fun Fact callout box. A Contact section with icon-prefixed links. And a footer with copyright text.
I used these 8 semantic elements — no <div> anywhere:
| Element | Meaning | Where |
|---|---|---|
| <main> | Primary page content (one per page) | Wraps the entire card |
| <article> | Self-contained content | The profile card itself |
| <header> | Introductory content for its parent | Photo + name + title |
| <details> | Disclosure widget (open/close) | Dark mode toggle |
| <section> | Thematic grouping with a heading | About, Skills sections |
| <aside> | Tangentially related content | Fun fact callout |
| <nav> | Navigation links | Contact links |
| <footer> | Footer for its parent | Copyright notice |
| <figure> | Self-contained media | Profile photo |
| <meter> | Scalar measurement within a range | Skill proficiency bars |
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:
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.
Step 3 — Dark Mode Toggle + Card Header
I added a dark mode toggle using <details> — a native HTML disclosure widget. When it's open, CSS :has() detects the state and swaps all the design tokens. No JavaScript needed. Then the header with the circular photo, bold name, and muted job title.
summary is visually hidden behind a moon emoji using text-indent: -9999px and ::before pseudo-element. object-fit: cover — The source image is rectangular but displayed at 160×160. cover fills and crops. Without it, the image would be stretched.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> is a thematic grouping that needs its parent for context. The "About Me" 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.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.
section + section means "a section that directly follows another section." It skips the first one — no unwanted top border on About. Note that section + aside removes the top border but keeps the spacing — the aside sits tight against the skills section. Borders in px: Borders should be exactly 1px. Using rem could round to 0px or 2px at different font sizes.Step 6 — Skills Grid (CSS Grid + <meter>)
This was a fun one — instead of simple pill badges, I used a 4-column CSS Grid with skill icons (emoji), names, and <meter> elements for proficiency bars. Each skill card has a hover effect that lifts it slightly.
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.
Step 8 — Contact Links (::before Icons + Attribute Selectors)
I styled links differently based on their href attribute — no extra classes needed. The email link turns red automatically using [href^="mailto:"]. I also added ::before pseudo-elements with icons for each link type — email gets ✉, LinkedIn gets 💼, GitHub gets 💻, Twitter gets 🌐.
width: 1.5em on the pseudo-element gives all icons the same width so the link text aligns vertically. Attribute selectors — [href^="mailto:"] matches links starting with "mailto:". [href*="github"] matches links containing "github" anywhere. If you add another GitHub link later, it gets the icon automatically. :focus styling — Keyboard users navigate with Tab. Without :focus, they can't see where they are. This is a WCAG 2.1 accessibility requirement.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, Animation + Mobile
The finishing touches: a deeper shadow on hover (the transition was already set in Step 2), the @keyframes fadeSlideUp animation for the entrance, and responsive adjustments for mobile — the skills grid drops to 2 columns.
animation-name and animation-duration. 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. Grid columns: 2 on mobile — 4 columns are too tight on small screens. The grid drops to 2 columns for readability.Step 11 — Dark Mode with :has()
This is the CSS that powers the dark mode toggle I added in Step 3. When the <details> element is open, body:has(details[open]) matches and swaps all the design tokens to dark colors. Every element that uses var(--color-*) updates automatically.
:has() selects an element based on what it contains. body:has(details[open]) means "select body when it contains a details element that is open." This is a CSS-only state machine — no JavaScript, no event listeners, no state management. Custom properties cascade — Since every color references var(--color-*), overriding the custom properties on body cascades to every descendant. One rule changes the entire theme.Step 12 — Print Stylesheet
The final touch — a print stylesheet that strips out decorative elements and makes the card printer-friendly. The dark mode toggle is hidden, shadows become borders, the photo is removed, and all text turns black.
Acceptance Criteria
Here's the checklist I used to verify the build:
- Zero <div> tags in the HTML
- Uses all semantic elements: <main>, <article>, <header>, <details>, <section> (×2), <aside>, <nav>, <footer>, <figure>, <meter>
- Dark mode toggle works via :has(details[open]) — no JavaScript
- Skills displayed in a 4-column CSS Grid with icons and <meter> proficiency bars
- Contact links have ::before pseudo-element icons that align vertically
- Card has fadeSlideUp entrance animation
- Print stylesheet hides decorative elements
- Hover effects on card and skill items
- :focus styles on all links (keyboard accessible)
- All values use CSS custom properties from :root
- Responsive: skills grid drops to 2 columns on mobile, photo shrinks
- Valid HTML — passes W3C Validator
Quick Reference
CSS Units — When to Use What
| Unit | Relative To | Use For | Example |
|---|---|---|---|
| rem | Root font size (16px) | Font sizes, spacing | font-size: 1.75rem |
| em | Element's own font size | Component-scoped padding | padding: 0.375em |
| px | Absolute | Borders, shadows, decorative | border: 1px solid |
| % | Parent dimension | Widths, radius, meter fill | width: 100% |
| vh | Viewport height | Full-page layouts | min-height: 100vh |
CSS Selector Types Used
| Type | Syntax | Example |
|---|---|---|
| Class | .name | .skills-list, .skill-icon |
| Descendant | A B | section h2, nav a |
| Pseudo-class | :state | :hover, :focus, :has() |
| Pseudo-element | ::before | nav a::before for link icons |
| Attribute | [attr] | a[href^="mailto:"], a[href*="github"] |
| Adjacent sibling | A + B | section + section, aside + nav |