CSS Tutorial
CSS Project: Timeline
A vertical timeline of events with a center line, dots, and alternating cards.
What you will build
A vertical history of Harbor studio: four seasons from the first press to the cafe door that opened next to it. A line runs down the center. Each event is a card. Dots sit on the line. On a wide window the cards alternate left and right. On a narrow one they stack on the right so nothing collides.
The line and the dots are CSS, not extra images. position and a::before pseudo-element draw the spine. Grid places the cards.
Open an example with Try it in CSS. That loads /css/try — a live page preview.
Properties you will use
| Property | Job on this page |
|---|---|
position: relative / absolute | Pins the spine and the dots |
::before | Draws the center line and each event dot |
display: grid | Two columns that alternate the cards |
grid-column | Sends odd events left and even events right |
@media | Collapses to one column on a phone |
The list is still an ol. CSS paints the timeline. Do not fake order with randomdivs if the dates have a sequence.
Build in slices
First one event card and a vertical line. The list item is relative. The line is an absolute::before on the list.
Example
<style>
body { font-family: Georgia, serif; background: #e0f2fe; color: #0c4a6e; margin: 1.5rem; }
.timeline {
position: relative;
margin: 0;
padding: 0;
list-style: none;
}
.timeline::before {
content: "";
position: absolute;
left: 1rem;
top: 0;
bottom: 0;
width: 3px;
background: #0284c7;
}
.event {
position: relative;
margin: 0 0 1.25rem 2.4rem;
background: #fff;
border-radius: 0.85rem;
padding: 0.9rem 1rem;
box-shadow: 0 8px 18px rgba(12, 74, 110, 0.1);
}
.event::before {
content: "";
position: absolute;
left: -1.72rem;
top: 1.1rem;
width: 0.85rem;
height: 0.85rem;
border-radius: 50%;
background: #fff;
border: 3px solid #0284c7;
}
time { display: block; color: #0284c7; font-weight: 700; font-size: 0.9rem; }
</style>
<ol class="timeline">
<li class="event">
<time datetime="2019">2019</time>
<h2>First press</h2>
<p>Harbor studio rented the chart-room loft and ran menus for the cafe downstairs.</p>
</li>
</ol>Then, on a wide window, switch to two columns so cards alternate. Odd items go left. Even items go right. The spine moves to the center.
Example
<style>
@media (min-width: 44rem) {
.timeline::before { left: 50%; transform: translateX(-1px); }
.event {
width: calc(50% - 2rem);
margin: 0 0 1.4rem;
}
.event:nth-child(odd) { margin-right: auto; }
.event:nth-child(even) { margin-left: auto; }
.event:nth-child(odd)::before { left: auto; right: -2.05rem; }
.event:nth-child(even)::before { left: -2.05rem; }
}
</style>Widen and narrow the preview at /css/try. Dots should stay on the line. If a card covers the spine, the offset on ::before is wrong.
Complete page
Four dated events. The complete stylesheet uses a single-column spine on small screens and the alternating layout from 44 rem up. Copy the whole document into the CSS editor.
Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Harbor studio — from press to cafe</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
background: #e0f2fe;
color: #0c4a6e;
font-family: Georgia, "Times New Roman", serif;
padding: 2rem 1.25rem 2.5rem;
}
h1 { text-align: center; margin: 0 0 0.35rem; }
.lead { text-align: center; color: #075985; margin: 0 0 1.8rem; }
.timeline {
position: relative;
list-style: none;
margin: 0 auto;
padding: 0;
max-width: 52rem;
}
.timeline::before {
content: "";
position: absolute;
left: 1rem;
top: 0.4rem;
bottom: 0.4rem;
width: 3px;
background: #0284c7;
}
.event {
position: relative;
margin: 0 0 1.3rem 2.5rem;
background: #fff;
border-radius: 0.9rem;
padding: 0.95rem 1.05rem 1.05rem;
box-shadow: 0 10px 22px rgba(12, 74, 110, 0.12);
}
.event::before {
content: "";
position: absolute;
left: -1.78rem;
top: 1.15rem;
width: 0.85rem;
height: 0.85rem;
border-radius: 50%;
background: #fff;
border: 3px solid #0284c7;
}
time { display: block; color: #0284c7; font-weight: 700; font-size: 0.88rem; }
h2 { margin: 0.15rem 0 0.35rem; font-size: 1.15rem; }
p { margin: 0; color: #075985; line-height: 1.45; }
@media (min-width: 44rem) {
.timeline::before {
left: 50%;
transform: translateX(-1.5px);
}
.event {
width: calc(50% - 2.1rem);
margin: 0 0 1.5rem;
}
.event:nth-child(odd) { margin-right: auto; }
.event:nth-child(even) { margin-left: auto; }
.event:nth-child(odd)::before {
left: auto;
right: -2.15rem;
}
.event:nth-child(even)::before { left: -2.15rem; }
}
</style>
</head>
<body>
<h1>From press to cafe door</h1>
<p class="lead">Four seasons at Harbor studio. The line is CSS. The dates are the story.</p>
<ol class="timeline">
<li class="event">
<time datetime="2019">2019</time>
<h2>First press</h2>
<p>The chart-room loft took a small press. Menus for the cafe downstairs were the first paid work.</p>
</li>
<li class="event">
<time datetime="2021">2021</time>
<h2>Open mornings</h2>
<p>Harbor studio unlocked at 8:00 so bakers could proof dough while prints dried on the line.</p>
</li>
<li class="event">
<time datetime="2023">2023</time>
<h2>Workshop passes</h2>
<p>Weekday, Harbor, and Season seats went on the board. The cafe kept a tab for studio members.</p>
</li>
<li class="event">
<time datetime="2025">2025</time>
<h2>Shared door</h2>
<p>A second door opened onto Pier 2. Visitors step into the studio or the cafe without crossing the street.</p>
</li>
</ol>
</body>
</html>Practice tasks
- Add a fifth event for 2026. Keep the alternating pattern on wide windows.
- Change the spine color to
#0c4a6eand the dots to#38bdf8. - Lower the breakpoint to
36remand check the stack at /css/try.