CSS だけで Astro のボタンをホバー時に浮かせて光らせる — グラスモーフィズムから取り入れたもの
静的な Astro ブログで共通に使える CSS のホバー効果を作りました。浮き上がり、アクセントカラーの光の帯、枠線とグローに加え、Astro のスコープ付きスタイルと Pagefind の詳細度の落とし穴も扱います。

目次
はじめに
私はグラスモーフィズムのコンポーネントが好きです。ほかのサイトでよく見かけるカードは、ポインターを乗せると少し浮き上がり、光の帯が表面を横切り、縁が光ります。同じ感触をこのブログのボタンとピル型のタグにも付けたいと思いました。
最初に気になったのは、ブラウザーで JavaScript を動かす必要があるかどうかでした。このブログのページは、すべて事前にビルドした静的 HTML だからです。必要ありませんでした。効果はすべてブラウザーが描画時に適用する CSS なので、ページは以前とまったく同じように静的なままです。
実際に作ってみると、私が気に入っていたのはガラスの質感ではなく、浮き上がり、光の帯、アクセントカラーの枠線、グローといった効果だったこともわかりました。ガラスのぼかしは背景に何かがあって初めて見えるもので、このブログは無地の背景なので、最終的に取り入れませんでした。浮き上がり、光の帯、アクセントカラーの枠線、グローの 4 つは、1 つの共通クラスとして、サイト内のボタン、ピル型のタグ、拡大表示できる図版で動いています。
グラスモーフィズムとは
グラスモーフィズムは、パネルをすりガラスのように見せる UI スタイルです。主に 3 つの CSS プロパティで成り立っています。
rgba(255, 255, 255, 0.1)のような半透明の塗り。パネルの背後にあるものが透けて見えます。backdrop-filter: blur(10px)。要素そのものではなく、要素の背後にあるページの部分をぼかします。- 薄く明るい枠線。ぼかした背景の上でも、パネルの縁を見分けられるようにします。
グラスモーフィズムのパネルの見た目は、背後に何があるかで決まります。このスタイルは、鮮やかなグラデーションや写真の上で見せるのが普通です。ぼかしには、にじませる形や色が必要だからです。背後にグラデーションや写真がなければ、すりガラスのぼかしは見えません。このブログにはガラスのぼかしを見せられる背景がないので、取り入れませんでした。
気に入った効果はガラスがなくても再現できる
あのカードのどこが好きなのかを書き出してみると、すりガラスに関わるものは 1 つもありませんでした。カードは数ピクセル浮き上がり、ハイライトが表面を横切り、枠線が光ってその下にやわらかなグローが出ます。浮き上がりは transform、ハイライトは動くグラデーション、枠線とグローは border-color と box-shadow で作れるので、どれも backdrop-filter を必要としません。
このブログのページ背景は、サイトの基本となる面の色 --bg-1 の単色です(ライトモードでは #ffffff、ダークモードでは #0b1120)。単色をぼかしても同じ色にしかなりません。このブログでガラスのパネルを作っても、ただ色の付いた箱として描画されます。
ぼかしの材料を用意するためだけに、記事の背後へグラデーションを置くことも避けたいと考えました。長文は単色の面のほうが読みやすいからです。
4 つの効果をまとめた共通クラス:.hover-glint
浮き上がり、光の帯、アクセントカラーの枠線、グローの 4 つのホバー効果を、1 つのグローバルクラス .hover-glint にまとめ、4 つのカスタムプロパティで制御しています。そのカスタムプロパティの色はすべて --accent から作っています。--accent は、このブログがクリックできるテキストに使っているリンクの青です(ライトモードでは #1d4ed8、ダークモードでは #60a5fa)。
--accent は light-dark() で定義しています。light-dark() は、ライトのカラースキームでは 1 つ目の値を、ダークのカラースキームでは 2 つ目の値を選びます。そのため、グローと帯はカスタムプロパティを追加しなくてもテーマに追従します。
:root { --hover-lift: -2px; --hover-duration: 200ms; --hover-glow: 0 6px 16px color-mix(in srgb, var(--accent) 30%, transparent); --hover-sheen: linear-gradient( 100deg, transparent 40%, color-mix(in srgb, var(--accent) 15%, transparent) 50%, transparent 60% );}color-mix(in srgb, var(--accent) 30%, transparent) は、不透明度 30% のアクセントカラーです。新しい色ではなくリンクの青を選んだのは、この青がすでにクリックできるものを示しているからです。同じ青でホバー時のグローを出せば、同じ意味が伝わります。
:focus-visible 以外のルールは、2 つのメディアクエリの中に置いています。
.hover-glint:focus-visible { border-color: var(--accent); box-shadow: var(--hover-glow);}
@media (hover: hover) { .hover-glint:hover { border-color: var(--accent); box-shadow: var(--hover-glow); }}
@media (hover: hover) and (prefers-reduced-motion: no-preference) { .hover-glint { transition: transform var(--hover-duration) ease, box-shadow var(--hover-duration) ease, border-color var(--hover-duration) ease, background-position 500ms ease; }
.hover-glint:hover { transform: translateY(var(--hover-lift)); background-position: 0 0; }
.hover-glint:active { transform: none; }}-
(hover: hover)はhoverというメディア特性を調べます。使っている端末がポインターを要素の上に置けるかどうか、という特性です。hoverは置ける、という意味です。マウスや、ノートパソコンのキーボードの下にあるタッチパッドがこれにあたります。noneは置けない、という意味です。スマートフォンやタブレットのタッチスクリーンがこれにあたります。
そのため
(hover: hover)はパソコンでだけ有効になり、スマートフォンやタブレットはこのルールを無視します。こう分けているのは、タッチスクリーンではタップした後も別の場所をタップするまでホバーの効果が残ることがあり、読者が指を離した後もボタンが浮いたままになるからです。 -
prefers-reduced-motion: no-preferenceはprefers-reduced-motionというメディア特性を調べます。読者が OS で「視差効果を減らす」をオンにしているかどうか、という特性です。reduceはオン、という意味です。macOS や iOS のアクセシビリティ設定で「視差効果を減らす」をオンにした場合などです。no-preferenceはオフ、という意味です。
そのため、「視差効果を減らす」をオンにしていない読者には浮き上がりと光の帯が表示されます。この設定をオンにする読者は、画面上の動きでめまいや気分の悪さを感じることが多いので、アクセントカラーの枠線とグローだけが表示され、ボタンが浮き上がったり光の帯が横切ったりすることはありません。
-
:focus-visibleは、キーボードで操作する読者に、動きを伴わない枠線とグローを表示します。上の 2 つのメディアクエリはどちらも(hover: hover)を条件にしていて、キーボードをつないだタブレットはこの条件に合わないため、中のルールは適用されません。そのため:focus-visibleはメディアクエリの外に置いています。
この効果を適用する要素は 1 つずつ選びました。
- ピル型のタグ
- もっと見る
- いいね・共有・リンクコピーの各ボタン
- フッターのソーシャルアイコン
- ヘッダーの検索ボタン
- オフラインページの「再読み込み」ボタン
- 拡大表示を開く図版
モバイルの目次カードは対象から外しました。このカードが想定しているスマートフォンでは、ホバーが発生しないからです。検索結果の行には、枠線とグローだけを付けています。項目が密に並ぶリストで行がポインターの下で動くと、邪魔にしかならないからです。
なぜ白い帯を使わないのか
一般的なガラスのカードでは、ハイライトは不透明度の低い白い帯です。暗いパネルや彩度の高いパネルの上なら、この白い帯はよく見えます。このブログのライトモードではカードが #ffffff の上にあるので、白い帯は見えないまま横切ります。
そこで帯は、color-mix() で作った不透明度 15% のアクセントカラーにしました。この帯は白の上でも見え、ダークモードでもちゃんと光って見えるうえ、隣のグローとも色がそろいます。


#60a5fa です。帯の移動を背景レイヤーで作る
帯を横切らせる一般的な方法は、::before 疑似要素(CSS が要素の中に追加で作る箱)にグラデーションを持たせて transform で横に動かし、ボタン自体に overflow: hidden を指定して、ボタンからはみ出したグラデーションを切り取るものです。このブログではこの方法が使えません。記事横の共有レールにあるいいねボタンと共有ボタンは、ボタンの外にはみ出すツールチップで名前を表示していて、overflow: hidden はそのツールチップも一緒に切り取ってしまうからです。
overflow: hidden で ::before の帯を切り取っても、ボタン自体は問題なく見えます。しかし、ポインターがボタンに入るとボタンの外側にツールチップが表示され、そのツールチップもボタンの中の要素なので一緒に切り取られますこの問題を避けるため、帯はボタン自体の背景画像に描きました。画像の幅はボタンの 3 倍で、帯は中央の 3 分の 1 にあります。background-position は、背景画像のどの位置から表示するかを決めます。通常は 100% 0 で、ボタンには背景画像の右の 3 分の 1 が表示されます。ポインターが入ると 0 0 へ少しずつ移り、帯が左から右へ横切ります。ポインターが離れると 100% 0 へ少しずつ戻り、帯は右から左へ戻ります。背景は border-radius にも自動で沿うので、ピル型でも追加の対応はいりません。
.hover-glint { background-image: var(--hover-sheen); background-size: 300% 100%; background-repeat: no-repeat; background-position: 100% 0;}100% 0 ではボタンに背景画像の右の 3 分の 1 が表示され、帯はボタンの左にあります。背景画像が右へ動いて 0 0 になると、ボタンには左の 3 分の 1 が表示され、帯はボタンを横切って右側に出ています帯を背景画像に描くと、この効果を適用するコンポーネントはすべて 1 つのルールを守る必要があります。background-color だけを指定し、background は使いません。コンポーネント自身のルールはグローバルクラスを上書きするので、ピル型のタグに background: var(--bg-2) を書くと、気づかないうちに帯が消えてしまいます。今回この効果を適用したコンポーネントでは、すべて background を background-color に書き換えました。
枠線の色を決めるルール:Astro のスコープ付きスタイル
Astro は、コンポーネントの <style> に書いたルールをそのコンポーネントだけに適用します。これをスコープ付きスタイルと呼びます。Astro はセレクタのすべての部分に data-astro-cid-* 属性を付けるので、たとえば .social a は .social[data-astro-cid-…] a[data-astro-cid-…] になります。属性が 1 つ増えるごとに詳細度が上がるので、コンポーネントのルールは、グローバルな .hover-glint:hover や .hover-glint:focus-visible に勝つか同点になることが多くなります。
ビルド後の CSS を見ると、border-color について各コンポーネントの基本ルールとグローバルクラスのどちらが勝つかがわかります。
| 結果 | 例 | 詳細度 | この分類に入るコンポーネント |
|---|---|---|---|
| グローバルクラスが勝つ | a[data-astro-cid-…] | (0,1,1) | ピル型のタグ、「再読み込み」ボタン |
| 同点で、スタイルシートの読み込み順で決まる | .load-more-button[data-astro-cid-…] | (0,2,0) | もっと見る、図版の枠 |
| コンポーネントが勝つ | .social[data-astro-cid-…] a[data-astro-cid-…] | (0,3,1) | 検索ボタン、いいね・共有ボタン、フッターのアイコン |
いいね・共有ボタンのように詳細度で勝つか同点になるコンポーネントでは、ホバー時もキーボードフォーカス時も、グローバルクラスのアクセントカラーの枠線がコンポーネント側の設定で上書きされます。そこで、いいね・共有ボタンなどのコンポーネントでは、それぞれのルールの中でアクセントカラーの枠線を指定し、:focus-visible を :hover と並べて書きました。
.social a:hover,.social a:focus-visible { border-color: var(--accent); color: var(--text-1); background-color: var(--bg-1);}:hover だけを書くと、いいね・共有ボタンのように詳細度で勝つか同点になるコンポーネントでは、キーボードフォーカス時にグローは出ても枠線は元の色のままです。:focus-visible を加えると、フォーカス時に枠線もアクセントカラーになります。
4 重の :is(*, #\#) で Pagefind のスタイルを上書きする
このブログが使っている静的サイト向けの検索ライブラリ Pagefind は、Astro のコンポーネントよりもスタイルの上書きが難しくなっています。Pagefind のスタイルシートは、すべてのルールを :is(*, #\#) で 3 重に包んでいます。
Pagefind は :is() で 3 重に包むことで、自分のルールに ID 3 つ分の詳細度を持たせ、定義したスタイルが勝つようにしています。そこで同じ技で返し、4 重に包んで、検索結果の行のルールが ID 4 つ分の詳細度で Pagefind 本来のスタイルを上書きするようにしました。
@media (hover: hover) { :is(*, #\#):is(*, #\#):is(*, #\#):is(*, #\#) .pf-searchbox-result:hover { box-shadow: inset 0 0 0 1px var(--accent), var(--hover-glow); }}まとめ
このブログのピル型のタグ、主なボタン、拡大表示できる図版は、ポインターを乗せると 2px 浮き上がり、アクセントカラーの枠線とグローが付き、リンクの青の薄い帯が表面を横切るようになりました。この 4 つの効果は、すべて 1 つのグローバルクラスに書かれています。そのためサイトは完全に静的なままで、追加で読み込むものもありません。
Astro のサイトでこの効果を保つためのルールは 2 つです。
- この効果を適用するコンポーネントは
background-colorを指定し、backgroundは使わない。backgroundを使うと、一括指定プロパティが帯を消します。 - スコープ付きのルールの詳細度がクラス以上になるコンポーネントでは、
:hoverと:focus-visibleの両方でアクセントカラーの枠線を自分で指定する。

