·18 min read·Tutorial·Beginner

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
HTML5CSS3Semantic HTMLCSS GridCSS Custom PropertiesCSS :has()

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, 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:

💡Live Demo
See the finished profile card here — circular photo, dark mode toggle, skill grid with proficiency meters, contact links with icons, animated fade-in, 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 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:

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
<details>Disclosure widget (open/close)Dark mode toggle
<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
<meter>Scalar measurement within a rangeSkill 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:

💡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>. position: relative — Needed so the dark mode toggle can be positioned absolutely inside the card. animation-name: fadeSlideUp — The card fades in and slides up on load. We'll define the keyframes in Step 10.
Checkpoint
Dark navy page with a white rounded rectangle centered in the middle. Empty for now — but the structure is solid.

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.

💡Why This Matters
<details> for dark mode — A native HTML element that toggles open/closed state without JavaScript. The 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.
Checkpoint
White card with a moon icon in the top-right corner, circular photo centered at the top, "Sarah Chen" in bold below, "Software Engineer and Web 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 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.

💡Why the + combinator?
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.

💡Why This Matters
CSS Grid vs Flexbox — Grid gives us a proper 4-column layout where all cards are the same width. Flexbox would work for wrapping pills, but Grid is better for equal-sized grid items. <meter> — A native HTML element for displaying scalar measurements within a known range. Screen readers announce it as a meter with a value. The text content ("90%") is a fallback for browsers that don't support it. aria-hidden="true" on skill icons — The emoji icons are decorative. Screen readers should announce the skill name, not "snowflake" or "gear."
Checkpoint
"SKILLS" heading with 8 skill cards in a 4-column grid. Each card has an icon, name, and colored proficiency bar. Hovering a card lifts it with a subtle shadow.

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 (::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 🌐.

💡Why This Matters
::before for icons — No extra HTML or icon libraries needed. The icons are CSS-only using Unicode characters. 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.
Checkpoint
"CONTACT" heading with 4 links, each prefixed by an icon. Email is red with an envelope, others are gray with their respective icons. 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, 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.

💡Why This Matters
fadeSlideUp animation — The card starts invisible and 20px below, then fades in and slides up over 1 second. Applied in Step 2 via 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.

💡Why This Matters
CSS :has() selector — Often called the "parent selector," :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.

💡Testing print styles
In Chrome DevTools, press Ctrl+Shift+P (or Cmd+Shift+P on Mac) and type "Rendering." Enable "Emulate CSS media type: print" to preview your print styles without actually printing.
* * *

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

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, radius, meter fillwidth: 100%
vhViewport heightFull-page layoutsmin-height: 100vh

CSS Selector Types Used

TypeSyntaxExample
Class.name.skills-list, .skill-icon
DescendantA Bsection h2, nav a
Pseudo-class:state:hover, :focus, :has()
Pseudo-element::beforenav a::before for link icons
Attribute[attr]a[href^="mailto:"], a[href*="github"]
Adjacent siblingA + Bsection + section, aside + nav