iT邦幫忙

2026 iThome 鐵人賽

DAY 6
0
Modern Web

用 Astro 打造 Content-first 前端網站:30 天從靜態內容到會員、資料庫與選型(3rd)系列 第 6

元件元件裡的 CSS 會不會漏出去污染別頁?Astro 樣式怎麼分 scoped、global、utility?

  • 分享至 

  • xImage
  •  

會做零 JS 的 .astro 元件之後(Day 5),接著就會碰到樣式問題。如果你從 React 轉過來,可能會擔心:CSS 是全域的,我在這個元件寫 .title { },會不會不小心套到別頁那個也叫 .title 的東西?

在 Astro,元件裡的 <style> 預設就是 scoped,樣式不會外流。接下來只要判斷這段 CSS 是元件專用,還是全站共用:元件專用就留在 scoped,全站共用就放到全域。

Astro 的 <style> 預設不會漏

在元件裡放一段 <style>,它就自動被限制在這個元件裡。Astro 官方文件的說法如下:

Add a <style> tag directly within an Astro component to apply styles. These styles are scoped to the component by default.
(在 Astro 元件裡直接放 <style> 標籤就能套用樣式。這些樣式預設會被限制在該元件的範圍內。)

來源:Astro 官方 Styling 文件,查證日 2026-07-21。

過去要靠 BEM 命名規範、CSS Modules、CSS-in-JS 才能達到的「別讓樣式漏出去」,Astro 把這項隔離設成預設值。使用這項預設時,不必再加設定。

原理發生在 build 階段。Astro 會為這個元件的元素加上資料屬性,例如 data-astro-xxxxxx,再把你寫的選擇器改成「只匹配帶這個屬性的元素」。因此 h1 { color: red } 會被編譯成類似 h1[data-astro-xxxxxx] 的規則,只套用到這個元件的 h1。scope 在編譯期完成,不需要執行期的 JavaScript。

這個實作專案裡就有現成例子。src/pages/bench/vue-directives.astro 底部有一整段頁面級的 <style>

<style>
  .proof { max-width: 1120px; margin: 0 auto; }
  .summary { display: grid; grid-template-columns: repeat(2, 1fr); }
  table { width: 100%; border-collapse: collapse; }
  code { font-family: ui-monospace, monospace; }
  /* …還有更多 */
</style>

這裡面連 tablecode 這種很通用的選擇器都有,但它們全部自動 scoped 到這一頁,不會去動到別頁的 tablecode。框架元件也有同一套概念:src/components/FeedbackForm.vue 用的是 Vue 的 <style scoped>,機制不同、目的一樣,都是把樣式關在元件裡。

什麼時候該用全域樣式:地基那一層

基礎 reset、字體、行高、連結顏色,還有設計 token(顏色、間距這些)本來就該全站一致。把它們塞進每個元件的 scoped 裡,你會重複寫很多次,也很容易各寫各的,慢慢就不一致了。

全域層放的是「每一頁都該一樣」的地基。Astro 給兩種寫法。一種是在元件裡開一段不 scoped 的樣式,用 <style is:global> 明講「這段我要全域」。另一種更常見,也是這個專案用的:把一個 global.css 匯進 layout。Astro 官方文件示範在 .astro 的 frontmatter 寫一行 import:

---
import '../styles/global.css';
---

匯進 layout 一次,所有套用這個 layout 的頁面都會載入。這個專案把全域樣式放在 src/styles/global.css,並由 BaseLayout.astro 在頂端 import。檔案裡只放全站共用的地基:

:root {
  /* 設計 token 住在全域層:改一個變數,用到它的地方全站一起變 */
  --color-text: #1f2933;
  --color-accent: #2563eb;
  --content-width: 46rem;
}
body { font-family: var(--font-sans); line-height: 1.7; }
main { max-width: var(--content-width); margin: 0 auto; }
/* 標題、段落、表格、code 的基礎排版… */

全域樣式仍有明確邊界:套 BaseLayout 的頁面(首頁、文章頁、demo 頁)會載入這層;套 BenchLayout 的測試頁刻意沒有 import,所以不會載入。在 Astro,「全域」是掛在某個 layout 上的共用地基,範圍仍由你決定。

CSS 變數(那些 --color-*)適合放在這一層。:root 定義一次 --color-accent,任何元件的 scoped 樣式要用就寫 var(--color-accent);日後要換主色,只改這個變數,全站就會一起變。這也是 token 該放全域的原因。若要從元件 frontmatter 把動態值傳進 scoped 樣式,Astro 另有 define:vars 指令,這篇不會用到。

全域樣式也可能失效,而且不容易察覺。以下是一個上線中的多語系品牌官網,它用這種方式載入全站的地基樣式:

<!-- src/layouts/Layout.astro 的 <head> 裡 -->
<link rel="stylesheet" href="/src/styles/layouts/Layout.scss" />
<link rel="stylesheet" href="/src/styles/pages/common.scss" />

在傳統多頁網站或從 CDN 引入樣式時,在 <head> 放一個 <link> 指到檔案不需要打包工具處理。搬到 Astro 後,這兩行不會生效;build 不報錯,畫面看起來也還可以,問題反而不容易察覺。

我把那個專案複製一份實測。astro build 跑完,dist/ 底下沒有 src/styles/ 這個目錄,但產出的 HTML 裡那兩行 <link> 還在,路徑一字未改。再去翻打包出來的 CSS,Layout.scss 那條 html, body { … }common.scssmainh1input 基礎排版,一條都沒進去。那兩份地基樣式從頭到尾沒有生效過。網站看起來大致正常,是各元件自己的 scoped 樣式在頂著。

Astro 只處理它認得的建置輸入。frontmatter 裡的 import 是輸入,<link href> 對它只是一串字,不會被解析、打包、加上雜湊檔名。前面的 import '../styles/global.css' 會把樣式帶進建置流程;SCSS 也一樣,import 進來才會被編譯。

可選的第三層:utility(例如 Tailwind)

還有第三種取向:utility-first。不寫語意化的 class,直接用 class="mt-4 flex gap-2" 這種原子 class 把樣式組出來,Tailwind 是代表。它的好處是快、間距一致、不用煩惱命名。

這個專案沒有裝 Tailwind(package.jsonastro.config.mjs 都確認過,沒有這個依賴)。它是一個內容站,CSS 的表面積不大,因此範例集中在平台原生的樣式原語(scoped 加 global 加 token)。對這種規模的專案來說,少一層工具鏈,也少一組要學、要設定、要維護的東西。

使用 Tailwind,要看專案規模與 UI 的複雜度。團隊大、UI 元件多,又需要快速一致地拼畫面時,它很划算;小型內容站用原生的 scoped 加 global 也完全夠。

前面的多語系品牌官網也裝了一套 utility 框架,只開最小的預設集。跑完 build 去數,全站真正用到的 utility class 只有一個,掛在 <body> 上管字體;其餘樣式都是 .home-content-logo 這種語意化命名,寫在各元件的 scoped <style> 或獨立 SCSS 檔裡。

開專案的時候裝這一層很合理。那時你還不知道 CSS 會長多大,先把工具備好,看起來只是多一個依賴。但真的動手寫,手感還是語意化命名,utility 沒有取代掉任何東西,最後留下的是一套要設定、要跟著升級、卻沒在用的工具鏈。

安裝前先確認這一層要替你取代什麼。答得出「間距和排版以後都用 utility 寫,不再自己命名」,就裝;答不出來,先不裝,之後要補很容易。Day 27 把這條判準寫成了一份安裝前的檢查清單(暫定)。

分層規則:先問這段樣式屬於誰

三層樣式可以用下面幾條規則判斷:

  • 只有這個元件要用 → scoped。這是預設,你什麼都不用做,寫在元件的 <style> 裡就好。
  • 全站都要一致(reset、字體、連結色、設計 token)→ global。放進 global.css,匯到 layout。
  • 重複性的間距與排版原子、想要一整套 utility → 可選的第三層(Tailwind 等)。這個專案沒用。

判斷時先看這段樣式屬於元件還是整個站。屬於元件就用 scoped,屬於整站就放進 global。

幾個容易踩的點

  • 以為 Astro 的 <style> 會像傳統全域 CSS 一樣漏出去。 Astro 預設就是 scoped;要改成全域,得自己用 is:global opt-out。
  • 把該全站一致的 token 寫進某個元件的 scoped 裡。 顏色、間距這種東西一旦散在各元件,就會重複、會不一致。那是 global 的東西。
  • 反過來,把只有一個元件要用的樣式塞進 global.css 全域檔案會越來越長,也很難判斷每條規則由誰使用。共用地基放在 global,元件專屬樣式留在元件。

今日驗收

驗收時,請確認自己能說明:Astro 的 <style> 預設 scoped、不會漏,也不需要額外設定。拿到一段 CSS,先判斷它屬於元件(留 scoped)還是全站(進 global);reset 和設計 token 放在全域,元件專屬樣式留在元件。第三層 utility 是可選的,這個內容站選擇不使用。

下一步

.astro 元件不在瀏覽器裡跑(Day 5),但頁面有時還是想加一點點互動,例如小按鈕或捲動效果,還不到要整包框架下去。Day 7 談頁面裡的原生 <script>,以及「到底需不需要框架」這條界線。

本日程式碼:step-06|只看這天的改動:step-05...step-06


上一篇
astro 元件跟 React 元件差在哪?為什麼它預設不送 JavaScript?
下一篇
只想加一顆按鈕、一點互動,需要動用框架嗎?Astro 頁面裡的原生 script
系列文
用 Astro 打造 Content-first 前端網站:30 天從靜態內容到會員、資料庫與選型(3rd)9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言