CSSで縦の線と点のタイムラインを作る方法
縦の線と点で出来事を並べるタイムラインは、<ol> の各 <li> に ::before で点を、::after で線を描けば作れます。線は「その項目の点の中心から、次の項目の点の中心まで」の長さにし、最後の項目では描きません。点は z-index で線より上に出します。点の上からの位置を lh 単位で1行目の高さから計算すると、文字の大きさを変えても、点が日付の横からずれません。
検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。HTML と CSS だけのページを、Playwright を使って幅 360px の画面で開き、スクリーンショットの画素を読んで、線と点の位置を調べました。Firefox と Safari、スマートフォンの実機、スクリーンリーダーでの読み上げは試していません。

HTML は ol と li で書く
出来事は時間の順に並ぶので、順序に意味のあるリストを表す <ol> で書きます。日付は <time> に入れ、datetime 属性に機械が読める形の日付を書きます。
<ol class="timeline" role="list">
<li>
<time datetime="2026-11-01">11月1日</time>
<p>下書きを書き始めました。</p>
</li>
<li>
<time datetime="2026-11-02">11月2日</time>
<p>画像を3枚作りました。</p>
</li>
<!-- … -->
</ol>
role="list" を付けているのは、CSS の list-style: none で番号を消すと、Safari がそのリストをリストとして扱わなくなるためです。MDN の list-style のページによると、Safari は list-style: none のリストを、<nav> の中にある場合を除いて、アクセシビリティツリーの上でリストとして扱いません。role="list" を付けると、見た目を変えずにリストの意味を戻せます。
点と線を疑似要素で描く CSS
.timeline {
--dot: 14px; /* 点の直径 */
--line: 2px; /* 線の太さ */
--x: 8px; /* 線の中心の、左端からの位置 */
list-style: none;
margin: 0;
padding: 0;
}
.timeline li {
position: relative;
padding: 0 0 24px 32px;
line-height: 1.6;
}
.timeline time { display: block; font-weight: bold; color: #067a98; }
.timeline p { margin: 4px 0 0; font-size: 14px; }
/* 点:中心が線の中心と、1行目の高さの中央に来るように置く */
.timeline li::before {
content: "";
position: absolute;
left: calc(var(--x) - var(--dot) / 2);
top: calc(0.5lh - var(--dot) / 2);
width: var(--dot);
height: var(--dot);
box-sizing: border-box;
border: 3px solid #00c8e8;
border-radius: 50%;
background: #fff;
z-index: 1; /* 線より上に描く */
}
/* 線:この項目の点の中心から、次の項目の点の中心まで */
.timeline li:not(:last-child)::after {
content: "";
position: absolute;
left: calc(var(--x) - var(--line) / 2);
top: 0.5lh;
bottom: -0.5lh;
width: var(--line);
background: #00c8e8;
}
li に position: relative を付け、点と線は position: absolute で li の左側の余白に置きます。MDN の position のページにあるとおり、absolute の要素は、いちばん近い位置指定された祖先を基準に置かれ、top や left はその包含ブロックの端からの距離です。ここでは position: relative を付けた li が、いちばん近い位置指定された祖先なので、li が包含ブロックになります。::before と ::after は content がないと描かれないので、空の文字列を入れています。
線と点の中心を合わせるために、左の位置は「線の中心の位置 --x から、それぞれの幅の半分を引いた値」にしています。検証環境で計算値を見ると、点の left は 1px(直径 14px なので中心は 8px)、線の left は 7px(太さ 2px なので中心は 8px)で、中心が同じ位置になりました。
線は、最後の点より下に伸ばさない
線をリスト全体に1本だけ引くこともできます。ol に position: relative を付けて、ol::before を top: 0 から bottom: 0 まで伸ばす形です。ただ、この形では線の長さがリストの高さで決まるので、最後の項目の本文の下まで線が続きます。

幅 360px で測ったところ、リスト全体に線を引いた形では、線が最後の点の下端より 78px 下まで続き、最初の点の上にも約 6px はみ出しました。記事の CSS では、線は最初の点から始まり、最後の点で終わりました。点と点の間で、線の色が途切れた行はありませんでした。
項目ごとに線を引くときは、線の上端を自分の点の中心(top: 0.5lh)、下端を li の下端からさらに 0.5lh 下(bottom: -0.5lh)にします。li には上下の margin を付けていないので、次の li はこの li の下端からすぐに始まり、そこから 0.5lh 下が次の項目の点の中心になります。検証環境では、文字が 16px のときも 22px のときも、点と点の間で線が途切れた行はなく、線は最後の点で終わりました。最後の項目は :last-child で見分け、:not(:last-child) で線を描かないようにしています。
点の中に線が見えるときは、点に z-index を付ける
点を白抜きの丸にして z-index を付けないと、点の中の下半分に線が見えます。MDN の ::before のページによると、z-index を指定しないとき、::after は ::before より後に描かれるので上に重なります。線(::after)が点の中心から始まるため、同じ li の点(::before)の下半分に線が重なります。

点の中心から 2px 下の画素の色を調べると、z-index なしでは、線のある1〜3番目の点で水色、線のない最後の点で白でした。点に z-index: 1 を付けると、4つとも白になりました。
文字の大きさを変えても点がずれないように、lh で位置を決める
点の上からの位置を top: 6px のように決めると、文字の大きさが 16px のときは日付の横に来ても、文字を大きくするとずれます。そこで、1行の高さを表す lh 単位を使い、点の中心が1行目の高さの中央に来るようにしています。MDN の CSS の長さの単位のページでは、lh は、その要素の line-height の計算値と同じ長さだと説明されています。

li の文字を 22px にして、点の中心と、日付の行の中心の差を測りました。
| 点の top | 16px のとき | 22px のとき |
|---|---|---|
6px |
0.2px | 4.2〜4.8px 上にずれる |
calc(0.5lh - 7px) |
0.2px | 0.4px 以内 |
16px のとき、li の line-height の計算値は 25.6px で、点の top の計算値は 5.8px(25.6px の半分から 7px を引いた値)になりました。22px のときは line-height が 35.2px、点の top は 10.6px で、文字に合わせて点が下がります。疑似要素の lh は、li の1行の高さで計算されていることが分かります。線の top と bottom にも 0.5lh を使っているので、線も点の中心どうしをつなぎます。





