Astro での CSS カスケードレイヤーの順序の管理:問題ケースと解決方法
意図しないレイヤー順序になるケース
CSS の @layer
を使う場合は、@layer
ステートメントアットルール を CSS の先頭に書きたいと私は思っています。
/* @layerステートメントアットルールをCSSの最初に書きたい */ @layer reset, base, components;
Astro では、どうすれば @layer
ステートメントアットルールを CSS の最初に書けるのでしょう?
何も考えずに @layer
ステートメントアットルールを書いたスタイルシートを レイアウト でインポートした場合、意図したレイヤーの順序と異なる結果になるかもしれません。意図しないレイヤー順序になるケースを StackBlitz で再現しました。BUTTON の背景色を赤にしているのですが、意図しないレイヤー順序のために赤になりません。
やっていることは、src/css/style.css
に CSS を書いています。レイヤーは reset
→ components
の順序です。また、全てのスタイルを削除する The New CSS Reset をリセット CSS で使うために、reset
レイヤーとして npm パッケージから読み込んでいます。
@layer reset, components; @import url('the-new-css-reset/css/reset.css') layer(reset);
そのスタイルシートを src/layouts/BaseLayout.astro
でインポートしています。
--- import '@css/style.css'; --- <!doctype html> <html lang="ja"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <slot /> </body> </html>
また、コンポーネントの src/components/Button.astro
を作っています。<style>
を書き、components
レイヤーとして <button>
の背景色を赤にしています。
--- --- <button type="button">BUTTON</button> <style> @layer components { button { background-color: red; } } </style>
そして、src/pages/index.astro
で layouts/BaseLayout.astro
と components/Button.astro
をインポートします。この時、components/Button.astro
を先に書いています。
--- import Button from '@components/Button.astro'; import BaseLayout from '@layouts/BaseLayout.astro'; --- <BaseLayout> <h1>TEST</h1> <Button /> </BaseLayout>
この場合、期待したレイヤーの順序になりません。期待とは逆の components
→ reset
の順になります。そのため、<button>
には The New CSS Reset の all: unset
が適用され背景色は赤になりません。

ビルド後の CSS を確認すると components/Button.astro
に書いた @layer components
ブロックアットルールが CSS の先頭に出力されます。その後に reset
レイヤーが出現するため、ブラウザは components
レイヤーより reset
レイヤーが優先だと解釈します。
<!-- ビルド後の <head> にある <style> --> <!-- 開発サーバーでも CSS の出現順番は同じ --> <style> @layer components { button[data-astro-cid-vnzlvqnm] { background-color: red; } } @layer reset,components; @layer reset { *:where( :not(html, iframe, canvas, img, svg, video, audio):not(svg *, symbol *) ) { all: unset; display: revert; } /* The New CSS Reset の CSS が続く */ } </style>
原因は、pages/index.astro
で layouts/BaseLayout.astro
より先に components/Button.astro
をインポートしたためです。ドキュメントの インポート順序 のヒントにLayoutコンポーネントは必ず他のインポートより先にインポートして、優先順位を一番低くしてください。
と書かれているのを守らなかったのがいけませんでした。
期待どおりのレイヤー順序にするには、何かしらの対策が必要だと感じます。
対策
何かしらの対策として思いついたのは 3 つありました。
injectScript を使って読み込む
1 つ目は、Astro Integration API のオプション injectScript
を使ってスタイルシートを読み込む方法です。astro.config.* の integrations
に設定を追加します。
import { defineConfig } from 'astro/config'; export default defineConfig({ integrations: [ { name: 'importStyleCSS', hooks: { 'astro:config:setup': ({ injectScript }) => { injectScript('page-ssr', `import '@css/style.css';`); // インポートエイリアスを使っていない場合 // injectScript('page-ssr', `import './src/css/style.css';`); }, }, }, ], });
これで全てのページのフロントマターに import '@css/style.css';
が自動的に追加されます。フロントマターに自分で書くインポートより先でのインポートになります。
また、レイアウトに書いたインポートは不要になるため削除します。
--- // 不要になったため削除 // import '@css/style.css'; --- <!doctype html> <html lang="ja"> <!-- 省略 --> </html>
この injectScript
を使ったスタイルシートの読み込みは、@astrojs/tailwind が @tailwind base;
などを CSS に加えるために行っている方法です。
1 つ注意点があり、<head>
内の <link>
と <style is:inline>
は、インポートで読み込む CSS より先に出現します。
<html lang="ja"> <head> <!-- この 2 つの CSS はインポートで読み込む CSS より出現順序が先になります --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs" /> <style is:inlne> /* 何かしらの CSS */ </style> </head> </html>
私は、この injectScript
を使う方法を採用しました。
is:inline を使って head 内に書く
2 つ目は、<head>
内の <style is:inline>
で @layer
ステートメントアットルールを書く方法です。
--- import '@css/style.css'; --- <!doctype html> <html lang="ja"> <head> <!-- style を追加 --> <style is:inline> @layer reset, components; </style> </head> <body> <slot /> </body> </html>
そして、スタイルシートから @layer
ステートメントアットルールを削除します。
/* 不要になったため削除 */ /* @layer reset, components; */ @import url('the-new-css-reset/css/reset.css') layer(reset);
また、@layer
ステートメントアットルールのみを書いたスタイルシートを ?raw
を使いインポートする場合も同じことができます。
@layer reset, components;
--- import '@css/style.css'; import rawLayerStatement from '@css/layer-statement.css?raw'; --- <!doctype html> <html lang="ja"> <head> <!-- style を追加 --> <style is:inline set:html={rawLayerStatement}></style> </head> <body> <slot /> </body> </html>
ただ、<style is:inline>
も ?raw
も Astro の自動 CSS 処理が効きません。そのため、単純な CSS しか書けないと思います。
ESLint で import の順番ルールを作る
3 つ目は ESLint で import
の順番にルールを適用し、レイアウトを必ず先にインポートする方法です。import
の順番にルールを適用する方法として、eslint-plugin-import の import/order が有名です。このブログでは、import/order より簡易な eslint-plugin-simple-import-sort を使っています。
eslint-plugin-simple-import-sort であれば、オプションの groups
を使いコンポーネントより先にレイアウトをインポートするように設定できます。
rules: { 'simple-import-sort/imports': [ 'error', { groups: [ ['^\\u0000'], ['^node:'], ['^@?\\w'], ['^'], // ルールを追加 ['^@layouts/', '^@components/'], ['^\\.'], ], }, ], 'simple-import-sort/exports': 'error', }
ルールを設定後に、このように書いたとします。
--- import Button from '@components/Button.astro'; import BaseLayout from '@layouts/BaseLayout.astro'; import type { SomeType } from '@types'; ---
Prettier も設定していれば、自動的にこのように並び替えることができます。@layouts/
と @components/
が 1 つのグループになり、@layouts/
が先になります。
--- import type { SomeType } from '@types'; import BaseLayout from '@layouts/BaseLayout.astro'; import Button from '@components/Button.astro'; ---
ただ、この方法は、レイアウトの入れ子 を作成する場合に問題が出るかもしれません。ドキュメントの例にある src/layouts/BlogPostLayout.astro
を作ったとします。
--- import BaseLayout from './BaseLayout.astro'; const { frontmatter } = Astro.props; --- <BaseLayout url={frontmatter.url}> <h1>{frontmatter.title}</h1> <h2>投稿者: {frontmatter.author}</h2> <slot /> </BaseLayout>
この場合、layouts/BaseLayout.astro
より先に layouts/BlogPostLayout.astro
がインポートされます。そのため、layouts/BlogPostLayout.astro
に CSS を書く場合は、その CSS が先に出現します。意図しないレイヤー順序になる可能性が結局あるため、レイアウトを入れ子にしない場合にのみ有効な方法に思えます。