Styles & Scripts
scoped style เป็น default
หัวข้อที่มีชื่อว่า “scoped style เป็น default”บล็อก <style> ในไฟล์ .astro จะถูก scope ไว้เฉพาะ component นั้น โดยอัตโนมัติ Astro เขียน selector ของคุณใหม่ (เติม hash ต่อ component) ดังนั้น rule ของ h1 ตรงนี้จะไปเลอะ h1 ของอีก component ไม่ได้
<h1>Title</h1><p>Body</p><style> h1 { color: rebeccapurple; } /* only THIS component's h1 */ p { line-height: 1.6; }</style>คุณเขียน selector ธรรมดาตรงไปตรงมา แล้วได้ CSS เฉพาะ component — ไม่ต้องมี naming convention ไม่ต้อง CSS-in-JS นี่คือ default ที่คุณจะใช้แทบทุกที่
opt out ด้วย is:global
หัวข้อที่มีชื่อว่า “opt out ด้วย is:global”บางครั้งคุณอยากให้ rule มีผลทั้งไซต์จริง ๆ — CSS reset, typography พื้นฐาน, theme เติม is:global เพื่อ opt ทั้งบล็อกออกจาก scoping
<style is:global> :root { --brand: #883AEA; } body { margin: 0; font-family: system-ui; }</style>คุณยัง escape scope ทีละ selector ได้ด้วยฟังก์ชัน :global() ภายในบล็อกที่ scoped ตามปกติ
<style> /* scoped, except this one rule reaches into slotted/child content */ :global(.markdown-body a) { color: var(--brand); }</style>ส่งค่าเข้า CSS ด้วย define:vars
หัวข้อที่มีชื่อว่า “ส่งค่าเข้า CSS ด้วย define:vars”ค่าใน frontmatter อยู่ฝั่ง server ส่วน CSS เป็น text นิ่ง ๆ เพื่อเชื่อมสองอย่างนี้ define:vars จะฉีดค่า JavaScript เข้าไปเป็น CSS custom property บนบล็อก style
---const accent = "#883AEA";const gap = "1.5rem";---<div class="card">Styled by props</div><style define:vars={{ accent, gap }}> .card { border: 2px solid var(--accent); padding: var(--gap); }</style>Astro จะออก inline style ที่พก --accent กับ --gap มาด้วย ดังนั้น prop ขับ style ได้โดยไม่ต้องมี JavaScript ฝั่ง client เลย เป็นวิธีสะอาด ๆ ในการทำให้สีหรือ spacing ของ component ปรับได้ผ่าน props
<script> tag: ถูก process และ bundle โดย default
หัวข้อที่มีชื่อว่า “<script> tag: ถูก process และ bundle โดย default”<script> ในไฟล์ .astro รันบน browser — แต่ Astro มองเป็น module: script จะถูก process, bundle, compile TypeScript และ resolve import เหมือน JS อื่น ๆ ใน build ของคุณ
<button id="menu">Menu</button><script> // Bundled module script. Runs in the browser. import { toggle } from '../scripts/menu'; document.getElementById('menu')?.addEventListener('click', toggle);</script>นี่คือ client JavaScript จริง ๆ — จุดเดียวในโมดูลนี้ที่โค้ดไปถึง browser เหมาะกับงาน DOM แบบ imperative เล็ก ๆ: ต่อ event listener, อ่าน localStorage, init widget จากภายนอก
opt out ด้วย is:inline
หัวข้อที่มีชื่อว่า “opt out ด้วย is:inline”ถ้าคุณต้องการให้ script ออกมา ตรงตามที่เขียนเป๊ะ ๆ — ไม่ถูก bundle หรือ process (เช่น snippet analytics แบบ inline หรือ script ที่ต้องรันก่อน hydration) ให้เติม is:inline
<script is:inline> window.dataLayer = window.dataLayer || [];</script>inline script ข้าม bundling ดังนั้น import ในนั้นจะไม่ถูก resolve และไม่ถูก dedupe ข้ามทั้งหน้า ใช้เฉพาะตอนที่คุณต้องการ tag ดิบ ๆ ไม่ถูกแตะจริง ๆ
จุดสำคัญ: script ไม่ใช่ island
หัวข้อที่มีชื่อว่า “จุดสำคัญ: script ไม่ใช่ island”จุดนี้ทำมือใหม่งงบ่อย <script> รันบน browser แต่ ไม่ ทำให้ component รอบ ๆ กลายเป็น island ไม่มีการ hydrate component ไม่มี framework ไม่มี re-render — เป็นแค่ script ธรรมดาที่แก้ DOM ที่ Astro ผลิตออกมาแล้ว
<!-- This runs, but nothing is "hydrated". It's a script, not an island. --><div id="counter">0</div><script> let n = 0; const el = document.getElementById('counter'); document.getElementById('inc')?.addEventListener('click', () => { el.textContent = String(++n); });</script>เมื่อคุณอยากได้ความ interactive ระดับ component — component React/Vue/Svelte ที่มี state แล้ว hydrate — นั่นคือ island สร้างด้วย client directive และเป็นเนื้อหาทั้งโมดูลถัดไป กฎง่าย ๆ: หยิบ <script> มาใช้กับงาน DOM แบบ imperative เล็ก ๆ; หยิบ island มาใช้เมื่ออยากได้ UI component ที่มี state รันบน client
flowchart TB scr["plain script tag"] --> dom["runs in browser, manipulates existing DOM"] dom --> nohy["no component hydration"] isl["island with a client directive"] --> hyd["framework component hydrates"] hyd --> state["has its own state and re-renders"]