Astroのスタイリング

Astroでは様々なUIコンポーネントフレームワークを共存させることができます。そこで、ちょっと悩ましいのがスタイリングです。

それぞれのフレームワークごとに対応すれば問題はありませんが、可能ならば、すべてまとめて対応したいところです。ある程度はCSS Modulesで対応できそうですが、ベースとなるAstroコンポーネントのスタイリング機能も気になります。

そこで、どんな特徴があるのか、ちょっと確認してみました。

<style>を使ったスタイリング

Astroコンポーネントの中で<style>を使ってCSSルールを追加すると、自動的にスコープされます。そのスコープの実現方法を確認しておきます。

まず、以下のようなAstroコンポーネントを用意します。

parent.astro
---
---
<div>
<h1>Astro</h1>
<p>コンポーネントのスタイリング</p>
</div>

このコンポーネントは、以下のようなコードとして出力されます。

<div>
<h1>Astro</h1>
<p>コンポーネントのスタイリング</p>
</div>

このコンポーネントに<style>を追加し、CSSのルールを追加します。

parent.astro
---
---
<div>
<h1>Astro</h1>
<p>コンポーネントのスタイリング</p>
</div>
<style>
h1 {
color: hotpink;
}
p {
font-size: 20px;
}
</style>

すると、コンポーネント内のすべての要素に、コンポーネントに振られたIDがクラスとして追加されます。

<div class="astro-5BHW53E2">
<h1 class="astro-5BHW53E2">Astro</h1>
<p class="astro-5BHW53E2">コンポーネントのスタイリング</p>
</div>

さらに、適用したスタイルにも同様のクラスが追加され、コンポーネントレベルのスコープが実現されます。

h1:where(.astro-5BHW53E2) {
color: hotpink;
}
p:where(.astro-5BHW53E2) {
font-size: 20px;
}

こうした処理はstyled-jsxと似ています。Astroではクラス名が「:where()」に含まれ、詳細度を上げないように出力されるのが面白いところです。

子コンポーネント

次のような子コンポーネントを用意して、先ほどのコンポーネントに追加してみます。

child.astro
<p>子コンポーネントのスタイリング</p>
<style>
p {
color: royalblue;
}
</style>
parent.astro
---
import Child from "../components/child.astro"
---
<div>
<h1>Astro</h1>
<p>コンポーネントのスタイリング</p>
<Child />
</div>
<style>
h1 {
color: hotpink;
}
p {
font-size: 20px;
}
</style>

すると、その結果として出力されるコードは、次のようになり、コンポーネントごとにスコープが構成されているのが確認できます。

<div class="astro-5BHW53E2">
<h1 class="astro-5BHW53E2">Astro</h1>
<p class="astro-5BHW53E2">コンポーネントのスタイリング</p>
<p class="astro-P7YZEBTU">子コンポーネントのスタイリング</p>
</div>

親コンポーネントから子コンポーネントにスタイルをあてる場合、子コンポーネントのクラス名はわかりませんし、スタイルにも(親コンポーネントの)クラスが追加されてしまいます。
そこで、:global()セレクタを使って、クラスを持った要素を起点にしてグローバルスタイルを適用することになります。

<style>
h1 {
color: hotpink;
}
div :global(p) {
font-size: 20px;
}
</style>

すると、以下のような CSS となり、子コンポーネントの<p>にもスタイルがあたります。:global()をうまく使うあたりは、styled-jsxはもちろん、CSS Modulesにも近い感じです。

h1:where(.astro-5BHW53E2) {
color: hotpink;
}
div:where(.astro-5BHW53E2) p {
font-size: 20px;
}

<style>に is:global 属性を追加して、グローバルスタイルを扱うこともできます。

子要素

子コンポーネントが子要素を扱えるように拡張します。

child.astro
<p>子コンポーネントのスタイリング</p>
<div><slot /></div>
<style>
div {
border: dotted 2px royalblue;
}
p {
color: royalblue;
}
</style>

そして、親コンポーネントを以下のようにします。

parent.astro
---
import Child from "../components/child.astro"
---
<div>
<h1>Astro</h1>
<p>コンポーネントのスタイリング</p>
<Child>
<p>子要素のスタイリング</p>
</Child>
</div>
<style>
h1 {
color: hotpink;
}
p {
font-size: 20px;
}
</style>

出力されるコードは、このようになります。

<div class="astro-5BHW53E2">
<h1 class="astro-5BHW53E2">Astro</h1>
<p class="astro-5BHW53E2">コンポーネントのスタイリング</p>
<p class="astro-P7YZEBTU">子コンポーネントのスタイリング</p>
<div class="astro-P7YZEBTU">
<p class="astro-5BHW53E2">子要素のスタイリング</p>
</div>
</div>

ここで問題になるのが子要素のクラスです。子コンポーネントではなく、親コンポーネントのクラスを持っていることがわかります。つまり、親コンポーネントのスコープの影響下にいるのです。

そのため、親コンポーネントのスタイルが簡単に当たってしまいますので注意が必要でしょう。このあたりの処理はstyled-jsxと同じです。

外部スタイルシート

もちろん、外部スタイルも使えます。他のフレームワークと同様に、importすればグローバルスタイルとして適用されます。スコープされることはありません。

---
import "../styles/style.css"
---
h1 {
text-decoration: underline;
}

@importでもインポートできます。ただし、グローバルとスコープされたスタイルの両方の形で出力されてしまうため、あまり使い所はなさそうです。

<style>
@import url("../styles/style.css");
</style>
h1:where(.astro-5BHW53E2) {
text-decoration: underline;
}
h1 {
text-decoration: underline;
}

propsでスタイルを渡すのは使えない?

子コンポーネントでスタイルをpropsとして受け取り、適用したいというケースもあります。そのため、次のようにしてみます。

child.astro
---
const { style } = Astro.props
---
<p>子コンポーネントのスタイリング</p>
<style set:html={style}></style>

親コンポーネントで次のようにスタイルを渡します。

parent.astro
---
import Child from "../components/child.astro"
---
<div>
<h1>Astro</h1>
<p>コンポーネントのスタイリング</p>
<Child style="p {color: royalblue;}" />
</div>

しかし、出力されるコードは次のようになります。propsとしてスタイルを渡してもスコープが構成されず、<style>がそのまま出力されています。

<div class="astro-5BHW53E2">
<h1 class="astro-5BHW53E2">Astro</h1>
<p class="astro-5BHW53E2">コンポーネントのスタイリング</p>
<p>子コンポーネントのスタイリング</p>
<style>p {color: royalblue;}</style>
</div>

スコープが構成されるには「<style>が空ではない」という条件がありますし、is:inline扱いなためスコープが生じないというのもあります。

別の<style>がある場合は、そちらにはスコープが構成されます。

クラスを渡す

Astroではpropsでクラスを渡すという方法が利用できます。そこにはスコープされた親のクラスが自動的に付加されます。

たとえば、子コンポーネントでクラスを受け取るようにします。

child.astro
---
const { class: className } = Astro.props
---
<p class={className}>子コンポーネントのスタイリング</p>

親コンポーネントではクラスを渡します。クラスに適用するスタイルは<style>で指定します。

parent.astro
---
import Child from "../components/child.astro"
---
<div>
<h1>Astro</h1>
<p>コンポーネントのスタイリング</p>
<Child class="text" />
</div>
<style>
.text {
color: royalblue;
}
</style>

出力されるコードは次のようになります。渡したクラス「text」にはスコープされた親のクラス「astro-5BHW53E2」が付加され、親のスコープでスタイルがあたります。

<div class="astro-5BHW53E2">
<h1 class="astro-5BHW53E2">Astro</h1>
<p class="astro-5BHW53E2">コンポーネントのスタイリング</p>
<p class="text astro-5BHW53E2">子コンポーネントのスタイリング</p>
</div>
.text:where(.astro-5LZXASO4) {
color: royalblue;
}

styled-jsxによく似た使い心地で、コードと合わせて1ファイルで管理できるあたりは便利なシステムです。ただし、突っ込んだ使い方をする場合はクセが見えてくるため、注意が必要でしょう。

Astroでは標準でCSS Modulesも利用できますので、好みや用途に応じて使い分けるのが良さそうです。