メインコンテンツまで移動する

Astro での CSS カスケードレイヤーの順序の管理:問題ケースと解決方法

  • 公開日

Akira Web クリエイター

意図しないレイヤー順序になるケース

CSS の @layer を使う場合は、@layer ステートメントアットルール を CSS の先頭に書きたいと私は思っています。

/* @layerステートメントアットルールをCSSの最初に書きたい */
@layer reset, base, components;

Astro では、どうすれば @layer ステートメントアットルールを CSS の最初に書けるのでしょう?

何も考えずに @layer ステートメントアットルールを書いたスタイルシートを レイアウト でインポートした場合、意図したレイヤーの順序と異なる結果になるかもしれません。意図しないレイヤー順序になるケースを StackBlitz で再現しました。BUTTON の背景色を赤にしているのですが、意図しないレイヤー順序のために赤になりません。

やっていることは、src/css/style.css に CSS を書いています。レイヤーは resetcomponents の順序です。また、全てのスタイルを削除する 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.astrolayouts/BaseLayout.astrocomponents/Button.astro をインポートします。この時、components/Button.astro を先に書いています。

---
import Button from '@components/Button.astro';
import BaseLayout from '@layouts/BaseLayout.astro';
---

<BaseLayout>
  <h1>TEST</h1>
  <Button />
</BaseLayout>

この場合、期待したレイヤーの順序になりません。期待とは逆の componentsreset の順になります。そのため、<button> には The New CSS Reset の all: unset が適用され背景色は赤になりません。

デベロッパーツールの CSS レイヤで components より reset が優先されているのが分かります。そのため、CSS リセットが <button> に適用されています。

ビルド後の 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.astrolayouts/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 が先に出現します。意図しないレイヤー順序になる可能性が結局あるため、レイアウトを入れ子にしない場合にのみ有効な方法に思えます。

フォローする