CSSで、リンクの下線を左から伸ばす方法

​​CSS​HTML,Playwright

リンクにマウスを乗せたときに下線を左から伸ばすには、::after で下線を描き、transform を scaleX(0) から scaleX(1) へ transition で変えます。そのとき transform-origin: left を付けて、左の端を起点にします。起点を書かないと、下線は中央から左右に伸びます。文の中で2行に折り返すリンクでは ::after の下線が崩れるので、背景のグラデーションで描く書き方も紹介します。

補足

検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。HTML と CSS だけのページを Playwright で開いてマウスとキーボードで操作し、getComputedStyle() の値と、画面を撮って下線が描かれている範囲を記録しました。動きを減らす設定は、Playwright のエミュレーションで試しています。Firefox と Safari、スマートフォンの実機は試していません。

縦に並んだ3本のリンクに、上から順にマウスを乗せる。乗せたリンクの下線が左から伸び、右の「下線」の割合が 0% から 100% まで増える。100% になった瞬間だけ割合の表示がピンクになる。マウスが次のリンクへ移ると、前のリンクの下線は左へ縮んで消える。最後にマウスをリンクの外へ出すと、3本目の下線も左へ縮んで消える
3本のリンクに順にマウスを乗せたときの下線の動き。見やすいように実際の2倍の時間をかけて再生している(上の表示、マウスの絵、右の割合の表示は GIF 用に足したもの)

::after で下線を描き、scaleX で伸ばす

HTML は、ふつうのリンクにクラスを付けるだけです。

html
<a class="underline" href="/articles/">記事の一覧を見る</a>
css
.underline {
  position: relative; /* ::after の位置の基準にする */
  color: #067a98;
  text-decoration: none; /* ブラウザの既定の下線を消す */
}

.underline::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: -2px;
  width: 100%;
  height: 2px;
  background: currentColor; /* 文字と同じ色の線 */
  transform: scaleX(0); /* 横の長さを 0 にして隠す */
  transform-origin: left; /* 左の端を起点に伸び縮みさせる */
  transition: transform 300ms ease-out;
}

.underline:hover::after,
.underline:focus-visible::after {
  transform: scaleX(1);
}

まず text-decoration: none で、ブラウザが付ける既定の下線を消します。代わりの下線は ::after 擬似要素で描きます。::after は、要素の最後の子として作られる擬似要素です。content: "" で中身を空にし、position: absolute で、リンクの下の端より 2px 下に、幅いっぱい・高さ 2px の線として置きます。

ふだんは scaleX(0) で横の長さを 0 にして隠しておき、:hover のときに scaleX(1) で元の長さに戻します。transition で transform を 300ms かけて変えるので、線がだんだん伸びて見えます。

Chromium でマウスを乗せ、transition を一時停止して 60ms ずつ進めながら、::after の transform の値と、画面に下線が描かれている範囲を調べました。リンクの文字は、ページの左から 24px〜184px の位置にあります。

乗せてからの時間 scaleX の値 下線が描かれている範囲
乗せる前 0 なし
60ms 0.31 24px〜73px
120ms 0.57 24px〜115px
180ms 0.79 24px〜150px
240ms 0.94 24px〜174px
300ms 1 24px〜184px

左の端は 24px のまま動かず、右の端だけが伸びていきました。ease-out なので、はじめに速く伸び、終わりに近づくほどゆっくりになります。マウスを外すと、scaleX は 0 に戻り、下線は見えなくなりました。

transform-origin: left がないと、中央から伸びる

transform-origin は、transform で拡大・縮小するときの起点を決めるプロパティです。MDN の説明にあるとおり、既定の起点は center(中央)です。

起点を center にした下線で、同じように調べました。リンクの文字は 24px〜224px で、transform-origin の計算値は 100px 1px(下線の幅 200px の真ん中)でした。

乗せてからの時間 下線が描かれている範囲
60ms 93px〜155px
120ms 67px〜181px
180ms 46px〜202px
240ms 31px〜217px
300ms 24px〜224px

下線は真ん中から左右の両方へ伸びました。左から伸ばしたいときは、transform-origin: left を忘れずに書きます。

消えるときは右へ抜けさせる

上の書き方では、マウスを外すと、下線は伸びたときと逆に、右の端から左へ縮んで消えます。左から出てきて右へ抜けていく動きにしたいときは、transform-origin を、ふだんは right、乗せている間だけ left にします。

html
<a class="underline underline-through" href="/articles/">記事の一覧を見る</a>
css
/* 消えるときは右へ抜けていく */
.underline-through::after {
  transform-origin: right;
}

.underline-through:hover::after,
.underline-through:focus-visible::after {
  transform-origin: left;
}

transition に書いたのは transform だけなので、transform-origin は時間をかけずにすぐ切り替わります。乗せた瞬間に起点が左になり、そこから伸びます。外した瞬間には起点が右になり、右の端に向かって縮みます。

2つの書き方で、乗せて伸ばしきった後にマウスを外し、150ms たったところで止めて比べました。どちらも、そのときの scaleX は 0.32 です。

リンクの文字 外してから 150ms の下線 起点(transform-origin)
.underline だけ 24px〜184px 24px〜74px(左に残る) 0px 1px(左)
.underline-through も付ける 24px〜164px 120px〜164px(右に残る) 140px 1px(右)

.underline だけのときは左の端に向かって縮み、.underline-through も付けたときは右の端に向かって縮みました。

2行に折り返すリンクは、背景のグラデーションで描く

::after で描く書き方は、リンクが1行に収まるときに向いています。文の途中にあるリンクが2行に折り返すと、下線が崩れます。

2つの画面を縦に並べたもの。上の「::after で描いた下線」では、2行に折り返したリンクのうち、2行目の「字」の下にだけ短い線が出ている。下の「背景で描いた下線」では、1行目と2行目のリンクの文字の下に、それぞれ線が出ている
2行に折り返すリンクにマウスを乗せたときの下線。上が ::after の書き方、下が背景のグラデーションの書き方

検証用のページで、幅 260px の段落の中に、2行に折り返すリンクを置いて調べました。リンクの文字は、1行目が 168px〜280px、2行目が 24px〜224px の位置にあります。::after の書き方では、マウスを乗せても1行目には下線が出ず、2行目の 168px〜224px にだけ、長さ 56px ほどの線が出ました。::after の幅の計算値は 55.5px で、1行目の文字の始まり(168px)と2行目の文字の終わり(224px)の、横の位置の差(56px)とほぼ同じでした。上の画像のように、リンクの一部の下にだけ短い線が出るので、下線には見えません。

文の中のリンクには、下線を背景の画像として描く書き方が使えます。

html
<p>文の途中にある <a class="underline-bg" href="/articles/">2行に折り返す長めのリンクの文字</a> です。</p>
css
.underline-bg {
  color: #067a98;
  text-decoration: none;
  background-image: linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat;
  background-position: left bottom;
  background-size: 0% 2px; /* 幅 0%、高さ 2px */
  transition: background-size 300ms ease-out;
}

.underline-bg:hover,
.underline-bg:focus-visible {
  background-size: 100% 2px;
}

linear-gradient(currentColor, currentColor) は、文字と同じ色1色だけの背景画像です。これを background-position: left bottom で左下に置き、background-size の幅を 0% から 100% に変えます。高さは 2px なので、背景の画像が下線の代わりになります。左の端に置いた画像の幅が広がるので、transform-origin を書かなくても左から伸びます。

1行のリンクで、乗せてから 150ms のところで止めると、background-size は 68.4643% 2px で、下線は文字の左の端(24px)から 120px まで描かれていました。

2行に折り返すリンクでは、伸ばしきると、1行目は 168px〜280px、2行目は 24px〜224px と、どちらの行も文字の下全体に線が出ました。乗せてから 150ms のところでは、1行目は端まで伸びきり、2行目は 24px〜126px まで伸びていました。150ms の時点で1行目はもう伸びきっていたので、下線は1行目の端まで伸びてから、2行目の左の端に続くと考えられます。

このとき、リンクの box-decoration-break の計算値は slice でした。MDN の説明では、slice のとき、要素は行に分かれていないものとして描かれ、それが行ごとに切り分けられます。1行目と2行目をつなげた1本の長い行として背景を描いているので、下線も1行目から2行目へ続けて伸びます。

キーボードで選んだときも伸ばす

:hover はマウスを乗せたときにしか当てはまらないので、それだけでは、キーボードで選んだときに下線が出ません。どちらの書き方でも、:focus-visible に同じ指定をしています。:focus-visible は、フォーカスを目に見える形で示すべきだとブラウザが判断したときに当てはまる擬似クラスです。

ページを開いて Tab キーを1回押すと、最初のリンクに :focus-visible が当てはまり、scaleX は 1 になって下線が出ました。マウスでクリックしてからリンクの外へマウスを出したときは、フォーカスはリンクに残っていましたが、:focus-visible は当てはまらず、scaleX は 0 に戻りました。

動きを減らす設定では、伸ばさずにすぐ出す

OS で動きを減らす設定をオンにしている人には、prefers-reduced-motion の reduce が当てはまります。このときは transition を止め、下線をすぐに出します。

css
/* 動きを減らす設定のときは、伸ばさずにすぐ出す */
@media (prefers-reduced-motion: reduce) {
  .underline::after,
  .underline-bg {
    transition: none;
  }
}

Playwright で動きを減らす設定をオンにしてマウスを乗せると、動いている transition は1つもありませんでした。乗せた直後から、::after の書き方は scaleX が 1、背景の書き方は background-size が 100% 2px になり、どちらも文字の下全体に下線が出ていました。下線が出ることは変わらないので、どのリンクにマウスが乗っているかは、この設定でも分かります。

​Posted by 86