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
に設定を追加します。
// astro.config.mjs
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
を使いインポートする場合も同じことができます。
/* src/css/layer-statement.css */
@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 が先に出現します。意図しないレイヤー順序になる可能性が結局あるため、レイアウトを入れ子にしない場合にのみ有効な方法に思えます。