# 我用純 CSS 讓 Astro 部落格的按鈕在滑鼠移入時浮起、掃過一道光，留下玻璃擬態裡我喜歡的效果

> 我用一個共用的 CSS class，為靜態的 Astro 部落格加上浮起、強調色掃光、強調色邊框和光暈四種滑鼠移入效果，並說明 Astro 作用域樣式與 Pagefind 帶來的特異性陷阱。

- Source: https://oharu121.com/zh-tw/blog/astro-css-hover-lift-glint-glassmorphism-accent-glow/
- Published: 2026-11-02T07:51:38+09:00
- Tags: CSS, Astro, 玻璃擬態

---
## 引言

我很喜歡玻璃擬態（glassmorphism）風格的元件。我在其他網站上常看到這樣的卡片：滑鼠移上去時會微微浮起，表面有一道光掃過，邊緣也會亮起來。我也想讓這個部落格的按鈕和膠囊標籤有同樣的感覺。

我最先擔心的是，這個效果會不會需要在瀏覽器裡執行 JavaScript，因為這個部落格的每一頁都是事先建置好的靜態 HTML。**答案是不需要。整個效果都是瀏覽器在繪製時套用的 CSS，所以頁面依然和之前一樣是純靜態的**。

實際做下來的過程中我發現，我喜歡的那些特徵其實都不是玻璃質感本身，而是浮起、掃光、強調色邊框和光暈等效果。玻璃的模糊效果需要在有背景的情況下才看得出來，但是這個部落格是素色背景，所以最後並沒有採用。現在浮起、掃光、強調色邊框和光暈都寫在同一個共用 class 裡，套用在網站的按鈕、膠囊標籤和可放大的圖表上。

## 玻璃擬態是什麼

玻璃擬態是一種把面板做成毛玻璃質感的 UI 風格，主要靠下面三個 CSS 屬性：

1. 半透明的填色，例如 `rgba(255, 255, 255, 0.1)`，讓面板後面的東西透出來。
2. `backdrop-filter: blur(10px)`，會模糊元素後方的頁面區域，而不是元素本身。
3. 一圈淡淡的淺色邊框，讓面板的邊緣在模糊的背景上看得出來。

**玻璃擬態的效果取決於面板後面有什麼**。這種風格通常會放在鮮豔的漸層或照片上展示，因為模糊需要有形狀和顏色才能暈開。如果背後沒有漸層或照片的話，毛玻璃就看不出模糊的效果了。這個部落格並沒有合適的背景來呈現玻璃的模糊效果，因此我沒有採用。

  放在深色漸層頁面上的玻璃擬態卡片，背景會模糊地透出來；滑鼠移上去時，卡片會浮起，邊緣也會變亮

## 我喜歡的效果就算不用玻璃也能呈現出來

我把自己喜歡那些卡片的地方一一列出來之後，發現沒有一項跟毛玻璃有關。卡片會往上浮起幾個像素，表面有一道高光掃過，邊框會亮起來，底下還有柔和的光暈。**浮起靠 `transform`，高光靠移動的漸層，邊框和光暈靠 `border-color` 和 `box-shadow`，沒有一個需要 `backdrop-filter`**。

這個部落格的頁面背景是單一純色 `--bg-1`，也就是網站的基底背景色（淺色主題下是 `#ffffff`，深色主題下是 `#0b1120`）。**單一顏色模糊之後還是同一個顏色，所以在這裡做玻璃面板，只會呈現成一個普通的半透明色塊**。

我也不想只為了讓模糊有東西可以暈開，就在文章後面放一層漸層，因為長篇文字放在純色的底上最好讀。

## 將四種效果合為一體的共用 class：`.hover-glint`

我把這四種滑鼠移入效果（浮起、掃光、強調色邊框和光暈）寫成一個全域 class `.hover-glint`，用四個自訂屬性來控制。這四個自訂屬性裡的顏色全部來自 `--accent`，也就是這個部落格用在可點擊文字上的連結藍（淺色主題下是 `#1d4ed8`，深色主題下是 `#60a5fa`）。

`--accent` 是用 `light-dark()` 定義的。這個函式在淺色配色下取第一個值，在深色配色下取第二個值。**所以光暈和光帶不需要新增額外的 token，就會跟著主題切換**。

```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` 之外，其餘規則都放在兩個媒體查詢裡面：

```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 query）是只有條件成立時才會生效的 CSS，條件寫在 `@media` 後面的括號裡。例如 `(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` 這個媒體特性，也就是讀者有沒有在作業系統裡開啟「減少動態效果」。
  - `reduce` 代表有開啟，例如在 macOS 或 iOS 的輔助使用設定裡打開了「減少動態效果」。
  - `no-preference` 代表沒有開啟。

  因此沒有開啟「減少動態效果」的讀者會看到浮起和掃光。會開啟這個設定的讀者，通常是畫面上的移動會讓他們頭暈或不舒服，所以他們只會看到強調色邊框和光暈，不會看到按鈕浮起或光帶掃過。
- **`:focus-visible`** 讓使用鍵盤操作的讀者也能看到邊框和光暈，但不會有移動效果。上面兩個媒體查詢的條件都是 `(hover: hover)`，接上鍵盤的平板不符合這個條件。如果同樣放在媒體查詢裡的話就不會套用，**所以 `:focus-visible` 特意放在媒體查詢之外**。

套用這個效果的元素是我一個一個挑出來的：

- 膠囊標籤
- 「載入更多」按鈕
- 按讚、分享和複製連結按鈕
- 頁尾的社群圖示
- 頁首的搜尋按鈕
- 離線頁面上的「重新載入」按鈕
- 可以開啟放大檢視的圖表

手機版的目錄卡片沒有套用這個效果，因為這張卡片是為手機設計的，而手機永遠不會觸發滑鼠移入。另外搜尋結果列只套用邊框和光暈，因為在密集的結果列表裡，結果列如果在鼠標移動時跳動，只會造成干擾。

## 為什麼不使用白色作為光帶？

一般玻璃卡片上的高光是一道低不透明度的白色光帶，疊在深色或高彩度的面板上很明顯。**但是在這個部落格的淺色主題下，卡片是放在 `#ffffff` 上的，所以白色光帶掃過去會完全看不見**。

我改用強調色來做光帶，透過 `color-mix()` 調成不透明度 15%。這道光帶在白色背景上依然看得見，在深色主題上也一樣會發光，顏色也和旁邊的光暈一致。

*Image: 淺色主題下滑鼠移入的「載入更多」（Load more）按鈕，有藍色邊框，底下有柔和的藍色光暈，還有一道淡淡的藍色光帶正從左側邊緣進入*

*淺色主題下滑鼠移入的「載入更多」按鈕，強調色的光帶在白色背景上依然看得見*

*Image: 深色主題下滑鼠移入的 Astro 膠囊標籤，在接近黑色的背景上有淺藍色邊框，底下有藍色光暈*

*深色主題下滑鼠移入的 Astro 膠囊標籤，此時的強調色是 `#60a5fa`*

## 把掃光做成背景圖層

做掃光效果的常見方法，是用 `::before` 虛擬元素（CSS 在元素裡額外產生的一個方塊）放一道漸層，再用 `transform` 讓漸層橫向移動，同時在按鈕本身加上 `overflow: hidden`，把超出按鈕的漸層裁掉。但這個部落格不能這樣做。文章分享側欄裡的按讚和分享按鈕，會把名稱顯示成懸在按鈕外面的工具提示，而 `overflow: hidden` 會把這些工具提示一起裁掉。

*Figure — OverflowClipsTooltip: `::before` 光帶被 `overflow: hidden` 裁切後，按鈕本身看起來沒問題，但鼠標進入按鈕時會在外側顯示 tooltip。因為 tooltip 也是按鈕裡的元素，所以也會一起被裁掉*

**為了避開這個問題，我把光帶畫在按鈕本身的背景圖上。這張背景圖的寬度是按鈕的三倍，光帶位於中間那三分之一**。`background-position` 決定背景圖從哪個位置開始顯示。平常是 `100% 0`，按鈕顯示的是背景圖的右邊三分之一；鼠標移入時逐漸往 `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` 時，按鈕顯示的是背景圖的右邊三分之一，光帶在按鈕左側；背景圖往右移到 `0 0` 時，按鈕顯示的是左邊三分之一，光帶已經掃過按鈕，到了右側*

把光帶做成背景圖之後，套用這個效果的元件都必須遵守一條規則：**只設定 `background-color`，絕不使用 `background`**。元件自己的規則又會蓋掉全域 class，所以如果在膠囊標籤裡設定 `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-…]`。**每多一個屬性，特異性就高一級，所以元件裡的規則常常會勝過或等於全域的 `.hover-glint:hover` 和 `.hover-glint:focus-visible`**。

> **話說**
>
> **特異性怎麼算**
>
> 兩條規則設定同一個屬性時，瀏覽器會套用特異性較高的那一條。特異性寫成（ID 數, class・屬性・虛擬類別數, 元素數），從左邊開始比大小。`.hover-glint:hover` 有一個 class 和一個虛擬類別，所以是 (0,2,0)。`.social a` 經過 Astro 處理後變成 `.social[data-astro-cid-…] a[data-astro-cid-…]`，有一個 class、兩個屬性和一個元素，是 (0,3,1)，比 (0,2,0) 高，所以元件的規則勝出。

從建置後的 CSS 可以看出，在 `border-color` 上，各個元件的基本規則和全域 class 誰會勝出：

| 結果 | 例子 | 特異性 | 屬於這一類的元件 |
| --- | --- | --- | --- |
| 全域 class 勝出 | `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` 就會讓邊框也顯示聚焦時的顏色。

## 用四層 `:is(*, #\#)` 蓋過 Pagefind 的樣式

這個部落格使用的靜態搜尋函式庫 Pagefind，樣式比 Astro 元件更難覆蓋。Pagefind 的樣式表把每一條規則都用 `:is(*, #\#)` 包了三層。

> **話說**
>
> **Pagefind 怎麼墊高特異性**
>
> `:is()` 會符合括號裡列出的任何一個選擇器，特異性則採用其中最高的那一個。`#\#` 是一個 ID 選擇器，但頁面上沒有任何元素的 id 是 `#`，所以它只是用來增加特異性的技巧。`*` 會符合每一個元素，因此 `:is(*, #\#)` 依然會選取所有元素，特異性卻算成一個 ID。

Pagefind 用三層 `:is()` 的包裝，讓自己的規則有三個 ID 的特異性，以確保其定義的樣式能勝出；**所以我用同樣的招式回敬，包了四層，讓搜尋結果列的規則以四個 ID 勝出，蓋掉原本 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，加上強調色邊框和光暈，還會有一道淡淡的連結藍光帶掃過。這四種效果全部寫在同一個全域 class 裡，所以網站依然是完全靜態的，也不會多載入任何東西。

在 Astro 網站上，有兩條規則能讓這個效果正常運作：

1. **套用這個效果的元件只設定 `background-color`，絕不使用 `background`**，否則簡寫屬性會把光帶刪掉。
2. **如果元件作用域規則的特異性勝過或等於這個 class，就要在 `: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() 以及 :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)
