會做零 JS 的 .astro 元件之後(Day 5),接著就會碰到樣式問題。如果你從 React 轉過來,可能會擔心:CSS 是全域的,我在這個元件寫 .title { },會不會不小心套到別頁那個也叫 .title 的東西?
在 Astro,元件裡的 <style> 預設就是 scoped,樣式不會外流。接下來只要判斷這段 CSS 是元件專用,還是全站共用:元件專用就留在 scoped,全站共用就放到全域。
<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>
這裡面連 table、code 這種很通用的選擇器都有,但它們全部自動 scoped 到這一頁,不會去動到別頁的 table 或 code。框架元件也有同一套概念: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.scss 的 main、h1、input 基礎排版,一條都沒進去。那兩份地基樣式從頭到尾沒有生效過。網站看起來大致正常,是各元件自己的 scoped 樣式在頂著。
Astro 只處理它認得的建置輸入。frontmatter 裡的 import 是輸入,<link href> 對它只是一串字,不會被解析、打包、加上雜湊檔名。前面的 import '../styles/global.css' 會把樣式帶進建置流程;SCSS 也一樣,import 進來才會被編譯。
還有第三種取向:utility-first。不寫語意化的 class,直接用 class="mt-4 flex gap-2" 這種原子 class 把樣式組出來,Tailwind 是代表。它的好處是快、間距一致、不用煩惱命名。
這個專案沒有裝 Tailwind(package.json 和 astro.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 把這條判準寫成了一份安裝前的檢查清單(暫定)。
三層樣式可以用下面幾條規則判斷:
<style> 裡就好。global.css,匯到 layout。判斷時先看這段樣式屬於元件還是整個站。屬於元件就用 scoped,屬於整站就放進 global。
<style> 會像傳統全域 CSS 一樣漏出去。 Astro 預設就是 scoped;要改成全域,得自己用 is:global opt-out。global.css。 全域檔案會越來越長,也很難判斷每條規則由誰使用。共用地基放在 global,元件專屬樣式留在元件。驗收時,請確認自己能說明:Astro 的 <style> 預設 scoped、不會漏,也不需要額外設定。拿到一段 CSS,先判斷它屬於元件(留 scoped)還是全站(進 global);reset 和設計 token 放在全域,元件專屬樣式留在元件。第三層 utility 是可選的,這個內容站選擇不使用。
.astro 元件不在瀏覽器裡跑(Day 5),但頁面有時還是想加一點點互動,例如小按鈕或捲動效果,還不到要整包框架下去。Day 7 談頁面裡的原生 <script>,以及「到底需不需要框架」這條界線。
本日程式碼:step-06|只看這天的改動:step-05...step-06