CSSのみで外部リンクにアイコンをつける

Category: Tech
Tag: CSS
thumbnail
完成品

https://mi.moris.day/@moris
ソースコード

要件

  1. 外部リンクにのみアイコンをつける
  2. 訪問済みのリンクは色を変える
  3. とにかく軽量 → Font Awesomeなどのwebフォント、JavaScript等は使わずにhtml&CSSのみで実装

表示するアイコン

Google Material SymbolsのOpen In Newというアイコンを使いました。人力圧縮でオリジナルの300Bから190Bまで圧縮されています。

<svg viewBox="0 0 99 99" xmlns="http://www.w3.org/2000/svg"><path d="M10,99C5,99 0,94 0,89V10C0,5 5,0 10,0H50V10H10V89H89V50H99V89c0 5-5 10-10 10ZM37,70l-7-7L80,10H60V0H99V39H89V19Z"/></svg>

オリジナル: https://fonts.google.com/icons?selected=Material+Symbols+Outlined:open_in_new:FILL@0;wght@400;GRAD@0;opsz@24

外部リンクに一致するCSSセレクタ

このブログはrehype-external-linksというパッケージで外部リンクにtarget="_blank"を設定しています。大体のサイトで同じだと思うので、これを属性セレクタで一致させます。正確にはtarget="_blank"は新規タブで開くことを意味しているのでサイト内リンクの可能性もあるのですが、アイコンの名前もOpen In Newですし、まぁいいでしょう。

a[target="_blank"] {
    /* ここにスタイル */
}

アイコン画像をつける

CSSセレクタは完成したのでアイコンを付けましょう。これは::after擬似要素一択ですね、content<svg>要素を入れて.....

svgがテキストとして表示される
😭😭😭

どうやらcontentプロパティにhtml要素を入れることはできないようです。
https://developer.mozilla.org/ja/docs/Web/CSS/content

しかしurl()で画像を入れることはできるようなのでdataURLでsvgを入れると

svgが正しく表示される
成功しました✨

ですが実はこれ、2つ目の要件「訪問済みのリンクは色を変える」を達成できません。(詳細は後述)
そのため、試行錯誤をした結果background-colorを設定してmask-imageで切り取ることにしました。

a[target="_blank"]::after {
    background-color: blue;
    mask-image: url('data:image/svg+xml;utf-8,<svg viewBox="0 0 99 99" xmlns="http://www.w3.org/2000/svg"></svg>');
}

訪問済みのリンクで色を変える

当初、CSSで訪問済みの要素に一致させるにはそう:visited!ということで、こんな感じのCSSにしたのですが、全て未訪問として表示されてしまいました。

a::after {
    content: url('data:image/svg+xml;utf-8,<svg fill="未訪問のときの色"></svg>');
}

a:visited::after {
    content: url('data:image/svg+xml;utf-8,<svg fill="訪問済みのときの色"></svg>');
    /* 効かない */
}

MDNによると、プライバシー保護のため:visited擬似クラスで適用されたスタイルには制限があり、利用可能なプロパティは一部の色に関する物のみのようです。
https://developer.mozilla.org/ja/docs/Web/CSS/:visited#プライバシー上の制約

colorプロパティは使えるのでFont Awesomeなどのアイコンフォントなら:visitedで色を変えることができますが、要件の通りwebフォントはナシなので却下。
また、fillプロパティも使えますがbackground-imageなどで設定されたsvgには適用できないのでこれも却下。

そのため先述のbackground-colorで背景色を設定してmask-imageで切り取るという方針になったのです。

a[target="_blank"]::after {
    content: '';
    mask-image: url('data:image/svg+xml;utf-8,<svg viewBox="0 0 99 99" xmlns="http://www.w3.org/2000/svg"></svg>');
    background-color: blue;
}
a[target="_blank"]:visited::after {
    background-color: purple
}

実装

これで必要なものは揃ったので微調整して完成です✨

a {
    color: blue;
}
a:visited {
    color: purple;
}
a[target="_blank"]::after {
    content: '';
    display: inline-block;
    width: .5em;
    height: .5em;
    margin-inline: 2px;
    background-color: currentColor;
    mask-image: url('data:image/svg+xml;utf-8,<svg viewBox="0 0 99 99" xmlns="http://www.w3.org/2000/svg"><path d="M10,99C5,99 0,94 0,89V10C0,5 5,0 10,0H50V10H10V89H89V50H99V89c0 5-5 10-10 10ZM37,70l-7-7L80,10H60V0H99V39H89V19Z"/></svg>');
    vertical-align: super; /* テキストよりちょっと上に配置 */
}

currentColor!?

突然出てきたcurrentColorですが、これはcolorプロパティの値を表すキーワード値で、要するに文字と一緒の色にするよってことです。
「テキストとアイコンで別々に色を設定」ではなく「テキストで色を設定し、アイコンはcurrentColorで追従」とすることでちょっとだけシンプルに記述できます。

このcurrentColor、borderやsvgのfillなどに便利なのでぜひ覚えておいてください。