Astroで Tailwind CSSの @applyを利用してブログ投稿などを装飾する方法

Astroで Tailwind CSSの @applyを利用してブログ投稿などを装飾する方法

まず前提としてAstroではコンポーネント内のstyle要素でスコープされたCSSスタイルが記述できます。

<style>
h1 {
 font-weight: 700;
 font-size: 1.25rem;
 line-height: 1.75rem;
}
h2 {
 font-weight: 700;
 font-size: 1.125rem;
 line-height: 1.75rem;
}
p {
 font-size: 1rem;
 line-height: 1.5rem;
} 
</style>
<h1>見出し1</h1>
<h2>見出し2</h2>
<p>本文</p>

このスタイルはコンポーネント内のh1要素やp要素のみで有効でコンポーネント外のh1要素やh2要素、p要素に影響を与えません。

Tailwind CSSを導入している場合はsytle要素内で@​applyが利用できるので以下のようにTailwind CSSのユーティリティベースでスタイルを構築することも可能です。

<style>
h1 {
 @​apply text-xl font-bold
}
h2 {
 @​apply text-lg font-bold
}
p {
 @​apply text-base
} 
</style>
<h1>見出し</h1>
<h2>見出し2</h2>
<p>本文</p>

ただ、このような記述をする必要とするケースはほとんどないでしょう。

HTMLがAstroの管理下にありますのでTailwind CSSのclassはHTMLのclass属性として定義すれば良いからです。

<h1 class="text-xl font-bold">見出し1</h1>
<h2 class="text-lg font-bold">見出し2</h2>
<p class="text-base">本文</p>

しかし、HTMLにclassを指定できない場合は有用になってきます。

ブログ投稿などのAstroで管理されていない外部のコンテンツを表示

ヘッドレスCMSから取得したコンテンツを表示するときやブログ投稿を表示する場合など、コンテンツ内にすでにHTMLが存在してclassを追加するのが簡単ではないケースがあります。

h1要素はAPIで取得したtitleを表示して、その下にAPIで取得したbodyをHTMLに変換して表示するケース場合では以下のような記述でしょう。

<h1>{title}</h1>
<div set:html={body} />

マークダウンで管理している場合はbodyの内容を<Content />として取得できるので以下のような記述になります。
参考: AstroでMDファイルの管理にContent Collectionsを利用する – to-R Media

<h1>{title}</h1>
<div>
  <Content />
</div>

このようなケースのスタイリングは何通りかあります。

Tailwind CSSの子孫セレクタを定義

スタイリングが複雑でなければTailwind CSSで定義されている子孫セレクタを利用してスタイリングするとよいでしょう。

<h1 class="text-xl font-bold">{title}</h1>
<div class="[&_h2]:text-lg [&_h2]:font-bold [&_p]:text-base">
  <Content />
</div>

Tailwind CSSで[&_XXX]:として指定した要素の子要素のスタイリングができます。

この方法はスタイリングが複雑になってくると管理が難しくなることです。

実際にとある案件でこの方法で指定を行なおうとしたところ以下のような複雑な指定が必要になったケースがあります。

<div class="mt-6 text-black xsm:mt-10 [&_h1]:mt-6 [&_h1]:font-bold [&_h1]:leading-[1.6] xsm:[&_h1]:mt-10 xsm:[&_h1]:text-xl [&_h2]:mt-6 [&_h2]:font-bold [&_h2]:leading-[1.6] xsm:[&_h2]:mt-10 xsm:[&_h2]:text-xl [&_h3]:mt-6 [&_h3]:font-bold [&_h3]:leading-[1.6] xsm:[&_h3]:mt-10 xsm:[&_h3]:text-xl [&_h4]:mt-6 [&_h4]:font-bold [&_h4]:leading-[1.6] xsm:[&_h4]:mt-10 xsm:[&_h4]:text-xl [&_h5]:mt-6 [&_h5]:font-bold [&_h5]:leading-[1.6] xsm:[&_h5]:mt-10 xsm:[&_h5]:text-xl [&_p]:mt-4 [&_p]:text-sm [&_p]:leading-[1.8] xsm:[&_p]:text-base [&_a]:text-[#297a89] xsm:[&_a]:duration-[0.6s] xsm:[&_a]:ease-in-out [&_th]:w-[37.5%] [&_th]:border-collapse [&_th]:border [&_th]:border-[#d7dada] [&_th]:bg-[#eaebeb] [&_th]:py-[0.68rem] [&_th]:px-3 [&_th]:text-left [&_th]:text-xs [&_th]:font-bold [&_th]:leading-[1.5] xsm:[&_th]:w-auto xsm:[&_th]:min-w-[7.5rem] xsm:[&_th]:px-[0.87rem] xsm:[&_th]:pt-[0.81rem] xsm:[&_th]:pb-4 xsm:[&_th]:text-sm [&_td]:w-[62.5%] [&_td]:border-collapse [&_td]:border [&_td]:border-[#d7dada] [&_td]:py-[0.68rem] [&_td]:px-3 [&_td]:text-left [&_td]:text-xs [&_td]:leading-[1.5] xsm:[&_td]:max-w-[43rem] xsm:[&_td]:px-[0.87rem] xsm:[&_td]:pt-[0.81rem] xsm:[&_td]:text-sm sm:[&_a]:hover:opacity-60 [&_ul]:mt-6 [&_ul]:flex [&_ul]:flex-col [&_ul]:gap-[0.25rem] [&_li]:relative [&_li]:pl-6 [&_li:before]:absolute [&_li:before]:top-[50%] [&_li:before]:left-2 [&_li:before]:h-2 [&_li:before]:w-2 [&_li:before]:translate-y-[-50%] [&_li:before]:bg-[#297a89] [&_table]:mt-6 [&_table]:w-full [&_table]:border-collapse [&_table]:border [&_table]:border-[#d7dada] [&>:first-child]:mt-0">
  <Content />
</div>

ここまでくると人間が理解して管理する難易度が高くなってしまいます。

やはり、セレクタ毎にスタイリングの定義を行うのがよさそうです。

globals.cssで定義

シンプルなのはglobals.cssで定義する方法でしょう。

HTMLに識別子となるclassを付与しておき、

<h1 class="text-xl font-bold">{title}</h1>
<div class="entry">
  <Content />
</div>

その識別子となるclassの子孫セレクターとして定義を行っていきます。

.entry h2{
    @​apply text-lg font-bold
}
.entry p{
    @​apply text-base
}

記述自体はシンプルになりましたが、この方法ではこのような記述が必要な箇所が増えていくとglobals.cssが肥大化して複雑になっていく恐れがあります。

実装とスタイリングの距離が離れていることも気になります。

また、識別子となるclassはグローバルに定義されているので他の箇所とバッティングしない命名規則などの設計も行わなくてはいけません。

グローバルスタイル

実装とスタイリングの距離が離れている点はstyle要素is:global属性を利用すると解決することができます。

以下のように記述を行うとglobals.cssで定義するのと同じ挙動を行います。

<style is:global>
.entry h2{
    @​apply text-lg font-bold
}
.entry p{
    @​apply text-base
}
</style>
<div class="entry">
  <Content />
</div>

これでglobals.cssが肥大化も防ぐことができますが、識別子となるclassの命名規則などの設計は必要になってきます。

:global()セレクタを利用

結論としては:global()を利用しましょうなのですがAstroではブログの投稿や、CMSを使用したドキュメントなど、コンテンツがAstroの外にあるものをスタイルするのに最適な方法として :global()セレクタが用意されています。
参考:CSSとスタイル 🚀 Astroドキュメント

以下のように指定を行うとコンポーネント内のclass属性「entry」が付与された子要素のみにスタイリングを反映することができます。

<style>
.entry :global(h2){
    @​apply text-lg font-bold
}
.entry :global(p){
    @​apply text-base
}
</style>
<h1 class="text-xl font-bold">{title}</h1>
<div class="entry">
  <Content />
</div>

最初に説明したとおりこの記述はコンポーネント内の子要素のみに有効になりますので同様の命名を行っていても他のコンポーネントの記述とバッティングする必要がありません。

独自のclass名を避ける

セレクタの起点となるentryというclassが気になる場合もあるでしょう。Tailwind CSSを利用している場合には極力独自の命名は避けたほうが混乱が少ないです。

そういった場合にはスタイリング用のラッパーコンポーネントを用意すると良いでしょう。

ContentsWrapコンポーネントとして以下のようなコンポーネントを用意しておき、

<style>
div :global(h1){
    @​apply text-lg font-bold
}
div :global(p){
    @​apply text-base
}
</style>
<div>
  <slot />
</div>

<Contents />の読み込み時にラップして使うことでフックとなるclassも不要となります。

<h1 class="text-xl font-bold">{title}</h1>
<ContentsWrap>
  <Content />
</ContentsWrap>

このようにAstroを利用する場合はコンポーネントスコープに閉じたスタイル指定を心がけ、Tailwind CSSを利用する場合は独自のclassなどは指定しないようにするとよいでしょう。