# CSS だけで Astro のボタンをホバー時に浮かせて光らせる — グラスモーフィズムから取り入れたもの

> 静的な Astro ブログで共通に使える CSS のホバー効果を作りました。浮き上がり、アクセントカラーの光の帯、枠線とグローに加え、Astro のスコープ付きスタイルと Pagefind の詳細度の落とし穴も扱います。

- Source: https://oharu121.com/ja/blog/astro-css-hover-lift-glint-glassmorphism-accent-glow/
- Published: 2026-11-02T07:51:38+09:00
- Tags: CSS, Astro, グラスモーフィズム

---
## はじめに

私はグラスモーフィズムのコンポーネントが好きです。ほかのサイトでよく見かけるカードは、ポインターを乗せると少し浮き上がり、光の帯が表面を横切り、縁が光ります。同じ感触をこのブログのボタンとピル型のタグにも付けたいと思いました。

最初に気になったのは、ブラウザーで JavaScript を動かす必要があるかどうかでした。このブログのページは、すべて事前にビルドした静的 HTML だからです。**必要ありませんでした。効果はすべてブラウザーが描画時に適用する CSS なので、ページは以前とまったく同じように静的なままです**。

実際に作ってみると、私が気に入っていたのはガラスの質感ではなく、浮き上がり、光の帯、アクセントカラーの枠線、グローといった効果だったこともわかりました。ガラスのぼかしは背景に何かがあって初めて見えるもので、このブログは無地の背景なので、最終的に取り入れませんでした。浮き上がり、光の帯、アクセントカラーの枠線、グローの 4 つは、1 つの共通クラスとして、サイト内のボタン、ピル型のタグ、拡大表示できる図版で動いています。

## グラスモーフィズムとは

グラスモーフィズムは、パネルをすりガラスのように見せる UI スタイルです。主に 3 つの CSS プロパティで成り立っています。

1. `rgba(255, 255, 255, 0.1)` のような半透明の塗り。パネルの背後にあるものが透けて見えます。
2. `backdrop-filter: blur(10px)`。要素そのものではなく、要素の背後にあるページの部分をぼかします。
3. 薄く明るい枠線。ぼかした背景の上でも、パネルの縁を見分けられるようにします。

**グラスモーフィズムのパネルの見た目は、背後に何があるかで決まります**。このスタイルは、鮮やかなグラデーションや写真の上で見せるのが普通です。ぼかしには、にじませる形や色が必要だからです。背後にグラデーションや写真がなければ、すりガラスのぼかしは見えません。このブログにはガラスのぼかしを見せられる背景がないので、取り入れませんでした。

  暗いグラデーションのページに置いたグラスモーフィズムのカード。背景がぼけて透けて見え、ポインターを乗せるとカードが浮き上がって縁が明るくなります

## 気に入った効果はガラスがなくても再現できる

あのカードのどこが好きなのかを書き出してみると、すりガラスに関わるものは 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 つ目の値を選びます。**そのため、グローと帯はカスタムプロパティを追加しなくてもテーマに追従します**。

```css title="src/styles/global.css"
: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 つのメディアクエリの中に置いています。

```css title="src/styles/global.css"
.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;
  }
}
```

> **ちなみに**
>
> **メディアクエリとは**
>
> メディアクエリは、`@media` の後ろの括弧に書いた条件が成り立つときだけ有効になる CSS です。たとえば `(max-width: 600px)` は画面幅が 600px 以下、`(prefers-color-scheme: dark)` は読者がダークモードにしている、`(hover: hover)` は主な入力装置がクリックせずに要素の上に止まれる、という条件です。マウスなら成り立ち、タッチスクリーンでは成り立ちません。

- **`(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% のアクセントカラーにしました。この帯は白の上でも見え、ダークモードでもちゃんと光って見えるうえ、隣のグローとも色がそろいます。

*Image: ライトモードでホバーした Load more ボタン。青い枠線、その下のやわらかな青いグロー、左端から入ってくる薄い青の帯が見える*

*ライトモードでホバーした Load more。アクセントカラーの帯は白の上でも見えます*

*Image: ダークモードでホバーした Astro のタグ。黒に近い背景の上に、明るい青の枠線とその下の青いグローが見える*

*ダークモードでホバーした Astro のタグ。アクセントカラーは `#60a5fa` です。*

## 帯の移動を背景レイヤーで作る

帯を横切らせる一般的な方法は、`::before` 疑似要素（CSS が要素の中に追加で作る箱）にグラデーションを持たせて `transform` で横に動かし、ボタン自体に `overflow: hidden` を指定して、ボタンからはみ出したグラデーションを切り取るものです。このブログではこの方法が使えません。記事横の共有レールにあるいいねボタンと共有ボタンは、ボタンの外にはみ出すツールチップで名前を表示していて、`overflow: hidden` はそのツールチップも一緒に切り取ってしまうからです。

*Figure — OverflowClipsTooltip: `overflow: hidden` で `::before` の帯を切り取っても、ボタン自体は問題なく見えます。しかし、ポインターがボタンに入るとボタンの外側にツールチップが表示され、そのツールチップもボタンの中の要素なので一緒に切り取られます*

**この問題を避けるため、帯はボタン自体の背景画像に描きました。画像の幅はボタンの 3 倍で、帯は中央の 3 分の 1 にあります**。`background-position` は、背景画像のどの位置から表示するかを決めます。通常は `100% 0` で、ボタンには背景画像の右の 3 分の 1 が表示されます。ポインターが入ると `0 0` へ少しずつ移り、帯が左から右へ横切ります。ポインターが離れると `100% 0` へ少しずつ戻り、帯は右から左へ戻ります。背景は `border-radius` にも自動で沿うので、ピル型でも追加の対応はいりません。

```css title="src/styles/global.css"
.hover-glint {
  background-image: var(--hover-sheen);
  background-size: 300% 100%;
  background-repeat: no-repeat;
  background-position: 100% 0;
}
```

*Figure — SheenBandPosition: ボタン自体は動かず、動くのはその下の背景画像です。`100% 0` ではボタンに背景画像の右の 3 分の 1 が表示され、帯はボタンの左にあります。背景画像が右へ動いて `0 0` になると、ボタンには左の 3 分の 1 が表示され、帯はボタンを横切って右側に出ています*

帯を背景画像に描くと、この効果を適用するコンポーネントはすべて 1 つのルールを守る必要があります。**`background-color` だけを指定し、`background` は使いません**。コンポーネント自身のルールはグローバルクラスを上書きするので、ピル型のタグに `background: var(--bg-2)` を書くと、気づかないうちに帯が消えてしまいます。今回この効果を適用したコンポーネントでは、すべて `background` を `background-color` に書き換えました。

> **気を付けて**
>
> **`background` は書かれていないサブプロパティを初期値に戻す**
>
> `background` は一括指定プロパティで、`background-color`、`background-image`、`background-position` など背景のサブプロパティをまとめて設定します。`background: var(--bg-2)` だけを書くと、色以外のサブプロパティはすべて初期値に戻り、`background-image` は `none` になって帯も消えます。

## 枠線の色を決めるルール：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` に勝つか同点になることが多くなります**。

> **ちなみに**
>
> **詳細度の数え方**
>
> 2 つのルールが同じプロパティを設定するとき、ブラウザーは詳細度の高いほうを適用します。詳細度は（ID の数, クラス・属性・疑似クラスの数, 要素の数）と書き、左から比べます。`.hover-glint:hover` はクラス 1 つと疑似クラス 1 つなので (0,2,0) です。`.social a` は Astro の処理後に `.social[data-astro-cid-…] a[data-astro-cid-…]` になり、クラス 1 つ、属性 2 つ、要素 1 つで (0,3,1) となるので、(0,2,0) より高く、コンポーネントのルールが勝ちます。

ビルド後の 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` と並べて書きました。

```css title="src/components/SiteFooter.astro"
.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()` は括弧の中に並べたセレクタのどれかに一致し、詳細度はその中で最も高いものになります。`#\#` は ID セレクタですが、ページ上に id が `#` の要素はないので、詳細度を上げるためだけの技です。`*` はすべての要素に一致するので、`:is(*, #\#)` はすべての要素を選びながら、詳細度は ID 1 つ分として数えられます。

Pagefind は `:is()` で 3 重に包むことで、自分のルールに ID 3 つ分の詳細度を持たせ、定義したスタイルが勝つようにしています。**そこで同じ技で返し、4 重に包んで、検索結果の行のルールが ID 4 つ分の詳細度で Pagefind 本来のスタイルを上書きするようにしました**。

```css title="src/styles/global.css"
@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 つです。

1. **この効果を適用するコンポーネントは `background-color` を指定し、`background` は使わない**。`background` を使うと、一括指定プロパティが帯を消します。
2. **スコープ付きのルールの詳細度がクラス以上になるコンポーネントでは、`:hover` と `:focus-visible` の両方でアクセントカラーの枠線を自分で指定する**。

## 参考リンク

- [MDN: backdrop-filter](https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop-filter)
- [MDN: メディアクエリの使い方](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Media_queries/Using)
- [MDN: hover メディア特性](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/hover)
- [MDN: prefers-reduced-motion](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion)
- [WCAG 2.2 Understanding 2.3.3: Animation from Interactions](https://www.w3.org/WAI/WCAG22/Understanding/animation-from-interactions.html)
- [MDN: :is() とその詳細度の計算方法](https://developer.mozilla.org/en-US/docs/Web/CSS/:is)
- [MDN: 一括指定プロパティと、書かれていないサブプロパティのリセット](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Cascade/Shorthand_properties)
- [Astro ドキュメント: スコープ付きスタイル](https://docs.astro.build/en/guides/styling/#scoped-styles)
- [Interaction Design Foundation: グラスモーフィズムとは](https://ixdf.org/literature/topics/glassmorphism)
