Making It Look Good: Styling Your Hand-Coded CV with CSS
Part 2 of 6 in the series “Hand-Code Your CV: From Empty Folder to Live on Azure”
In Part 1 we built a plain but properly structured CV page in a single index.html file. It’s readable, but it looks like it was printed on a dot-matrix printer.
In this post we’ll add CSS (Cascading Style Sheets). The idea behind it is the one we met last time: HTML describes what things are, and CSS describes how they look. Keeping the two apart is one of the most useful ideas on the web, and it’s the foundation of every styling tool you’ll meet later, from Sass to Tailwind.
Step 1: Create the stylesheet and link it
In your my-cv folder, create a new file called style.css. Then tell the page about it by adding one line inside the <head> of index.html:
<link rel="stylesheet" href="style.css">
Put a first rule in style.css to prove it’s connected:
body {
font-family: system-ui, sans-serif;
color: #1f2933;
}
Save both files, refresh the browser, and the font changes. If nothing happens, check that the file name in href matches exactly, and that both files are in the same folder.
How a CSS rule works
Every rule has the same shape:
selector {
property: value;
}
The selector says what to style. The properties say which aspects. The values say how.
The selectors you’ll use most:
- Element selectors such as
h2orptarget every element of that type. - Class selectors such as
.jobtarget elements withclass="job". Classes are reusable and are what you’ll use most. - ID selectors such as
#abouttarget the one element with thatid. Use them sparingly, because they’re hard to override. - Descendant selectors such as
.job h3target anh3inside something with the classjob.
The word “Cascading” in the name refers to how the browser settles conflicts. When two rules disagree, the more specific selector wins, and if they’re equally specific, the one that comes later wins. If a style ever behaves unexpectedly, that’s almost always why. Your browser’s developer tools (press F12) will show you exactly which rules apply to any element.
Step 2: Custom properties for colours
Rather than scattering colour codes throughout the file, define them once as CSS variables (officially “custom properties”) on the :root element:
:root {
--bg: #ffffff;
--text: #1f2933;
--muted: #52606d;
--accent: #0b6bcb;
--card: #f0f4f8;
}
body {
margin: 0;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
line-height: 1.6;
color: var(--text);
background: var(--bg);
}
Now changing the whole colour scheme is a five-line edit. In Part 3, we’ll flip these variables to build a dark mode, which is a neat trick that only works because we did this now.
Step 3: The box model
Everything on a web page is a rectangular box. Each box has, from the inside out: content, padding (space inside the border), border, and margin (space outside it).
By default, width only measures the content, so adding padding makes a box larger than you asked for. This catches everybody out. The near-universal fix is to add this at the top of your stylesheet:
* {
box-sizing: border-box;
}
Now width includes padding and border, and sizes behave the way you’d expect.
Let’s use boxes to keep the CV readable. Long lines of text on a wide screen are tiring to read, so we’ll constrain the width and centre the content:
main,
footer {
max-width: 48rem;
margin: 0 auto;
padding: 0 1rem;
}
margin: 0 auto means “no top or bottom margin, and share the leftover horizontal space equally”, which centres the block. The rem unit is relative to the browser’s base font size, which is more accessible than fixed pixels because it respects a visitor’s own settings.
Step 4: Style the pieces
Now for the headings, links and job entries. Note the class attributes we’ll use; add class="job" to each <article> and class="dates" to the date paragraphs in your HTML.
h2 {
border-bottom: 2px solid var(--accent);
padding-bottom: 0.25rem;
}
a {
color: var(--accent);
}
.job {
background: var(--card);
border-radius: 8px;
padding: 1rem 1.25rem;
margin-bottom: 1rem;
}
.job h3 {
margin: 0;
}
.dates {
margin: 0;
color: var(--muted);
font-size: 0.9rem;
}
Refresh and the page finally starts to look like a CV.
Step 5: Layout with Flexbox
Let’s put the header content on one row: the name on the left and, in Part 3, a button on the right. We’ll wrap the name and tagline in a div and give the header a class:
<header class="site-header">
<div>
<h1>Alex Morgan</h1>
<p class="tagline">Software developer who likes understanding how things work.</p>
</div>
<button id="theme-toggle" type="button">Dark mode</button>
</header>
.site-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
max-width: 48rem;
margin: 0 auto;
padding: 2rem 1rem 1rem;
}
.site-header h1 {
margin: 0;
}
.tagline {
margin: 0.25rem 0 0;
color: var(--muted);
}
display: flex turns an element into a flex container, and its children line up in a row. justify-content controls spacing along the row, and align-items controls alignment across it. Flexbox is the modern answer to “how do I line things up?”, and it replaced a horrifying era of layout hacks.
While we’re here, let’s turn our future skills list into a row of little “pills”. We’ll fill in the list itself with JavaScript next time, but the styling can be ready now:
.tags {
list-style: none;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.tags li {
background: var(--accent);
color: var(--bg);
padding: 0.25rem 0.75rem;
border-radius: 999px;
font-size: 0.9rem;
}
button {
font: inherit;
padding: 0.5rem 1rem;
border: 1px solid var(--accent);
border-radius: 6px;
background: transparent;
color: var(--accent);
cursor: pointer;
}
footer {
color: var(--muted);
padding-bottom: 2rem;
}
To go with this, add a new section to index.html, between Experience and Contact:
<section id="skills">
<h2>Skills</h2>
<ul id="skills-list" class="tags"></ul>
</section>
It will be empty for now. That’s expected.
Step 6: Make it work on a phone
Most visitors will look at your CV on a phone at some point. Because we set the viewport tag in Part 1 and used flexible widths and rem units, most of the work is already done. We’ll add one media query to stack the header on narrow screens:
@media (max-width: 30rem) {
.site-header {
flex-direction: column;
align-items: flex-start;
}
}
A media query applies its rules only when the condition is true. Here, that’s when the screen is 30rem wide or narrower. To test it, narrow your browser window or use the device toolbar in developer tools (F12, then the phone icon).
Checkpoint
Your folder now holds index.html and style.css. A good habit at this point: change one colour variable and see the whole page change. That’s the power of the cascade and custom properties working for you.
It’s worth stepping back to notice what you’ve just done by hand. Grid systems, component libraries, utility classes and CSS-in-JS are all just different ways of writing the rules you wrote in this post, generated at scale. Now you’ll know what they’re generating.
What’s next
The page looks right, but nothing on it does anything. In Part 3 we’ll add JavaScript: a dark mode button, a skills list built from data, and an automatic copyright year. We’ll also see how that little skills-list trick is the seed of what frameworks like React are built on.
Next: Part 3, Bringing it to Life with JavaScript.