💡

知らない間にUtility-first CSSが結構進歩していた【UnoCSS】

に公開

ここ数年はSveltekitが個人的なお気に入りです。なんといっても状態管理やアニメーションなど、WEBアプリ開発に必要なあれこれが最初から一通り揃っていて、すぐにモノづくりに取りかかれるのが良いです。

個人的にSveltekit、もといSvelteで特に気に入っているのは、HTMLやCSS本来の表現力がほぼそのまま活かせるところです。HTMLのaria-属性やCSS変数、calc()関数などを駆使すれば、JavaScriptに頼らずとも、そこそこリッチなUIが作れます。

なので4年ぐらい前にTailwindCSSが流行り出した時も、私は我関せずで生のCSSやCSS Modulesでスタイリングしていました。classにズラーっとユーティリティクラスを連ねるUtility-first CSSは、私にはどうにも邪道な代物に見えました。

しかし先日ひょんなことからUnoCSSを試してみたら、これがなかなか使いやすく、「これならUtility-first CSSもありかも知れない」と考えを少し改めました。

https://unocss.dev/

TailwindCSSに対する個人的な偏見

CSSの読み込みが重くなりそう

TailwindCSSを初めて見た時の私の感想は、「それなら必要最低限のユーティリティクラスを自分たちで定義して使った方がいいのでは?」ということでした。だってあらゆるスタイルをユーティリティクラスとして定義すれば、どう考えたって膨大な数になります。

実際TailwindCSSを採用する場合は、未使用のユーティリティクラスを削除するためにPurgeCSSなどのプラグインと併用するのが推奨されていたように記憶しています。それでも不要なユーティリティクラスが削除されるのは本番ビルドだけでしょうし、開発時にはどうしたってCSSの読み込みが重くなりそうです。

定義されているクラス名を調べるのがだるい

「まあ、それでも一応」とTailwindCSSを試してみた際も、充てたいスタイルが定義されているユーティリティクラスを公式ドキュメントで調べながらスタイリングしていくのが、なかなかだるかった記憶があります。一応ユーティリティクラスは規則的に命名されているので、ある程度慣れたら「このクラス名で定義されてそう」と推察できるのですが、たまに欲しいスタイルが定義されていなかったり、ちょっと混乱するような命名がなされていたり(フォントサイズを指定するクラスがfont-ではなくtext-だったり)で、「これなら自分で1からCSS書いた方が早いよ!」と思ったことを覚えています。(クラス名については、ひょっとしたらVSCodeの拡張機能などで推論できたりしたのかもしれませんが)

上記の不満が解消されていたUnoCSS

スタイリングに必要なユーティリティクラスを予め用意しているTailwindCSSに対し、UnoCSSは必要なユーティリティクラスをオンデマンドに生成するという手法が取られているようです。なのでUnoCSSは厳密にはTailwindCSSのようなPostCSSプラグインではなく、ビルドツールのようなCSSエンジン、らしいです。(公式サイトからの受け売り)

要は、以下のようなユーティリティクラスを使用したら、

<h1 class="mb-4">First article title</h1>

以下のようなスタイルクラスが生成されるということです。

.mb-4 {
    margin-bottom: 1rem;
}

calc()関数も使えるのは個人的に感動しましたね。

<div class="w-[calc(100%-300px)]"></div>
.w-\[calc\(100\%-300px\)\] {
    width: calc(100% - 300px);
}

拡張性によってUtility-firstと表現の柔軟性を両立

UnoCSSでは、Preset機能でTailwindCSSと同じユーティリティクラスを使用したり、あるいは自分で1からユーティリティクラスのルールを定義することもできます。これによりUtility-firstでありながら、生のCSSに匹敵しうる柔軟な表現力を獲得しています。

rules

uno.config.tsを使って以下のように、独自ルールのユーティリティクラスを定義できます。

uno.config.ts
import { defineConfig } from 'unocss';

export default defineConfig({
    rules: [
        ['dimdark', { color: '#797a83' }],
        ['smokelight', { color: '#e4eaf4' }]
    ],
})

定義したクラスは以下のように使用できます。

<h1 class="bg-smokelight text-dimdark">First article sentence.</h1>
.text-dimdark {
    color: '#797a83';
}

.bg-smokelight {
    background-color: '#e4eaf4';
}

正規表現や関数を使えば、以下のような動的なルールも定義できてしまいます。すごい。

uno.config.ts
import { defineConfig } from 'unocss';

export default defineConfig({
    rules: [ // 公式サイトのサンプルより
      [/^m-(\d+)$/, ([, d]) => ({ margin: `${d / 4}rem` })],
      [/^p-(\d+)$/, match => ({ padding: `${match[1] / 4}rem` })],
    ]
})
<h1 class="p-6">First article sentence.</h1>
<p class="m-4">first article sentence.</p>
.p-6 {
    padding: 1.5rem;
}

.m-4 {
    margin: 1rem;
}

shortcuts

任意のユーティリティクラスをひとまとめのセットとして使いたい場合は、shortcutsに定義します。このshortcutsにはrulesで定義した独自ルールも指定できます。

uno.config.ts
import { defineConfig } from 'unocss';

export default defineConfig({
    rules: [
        ['dimdark', { color: '#797a83' }],
        ['smokelight', { color: '#e4eaf4' }]
    ],
    shortcuts:[
        'base-title': 'bg-smokelight text-dimdark';
    ],
})
- <h1 class="bg-smokelight text-dimdark">First article sentence.</h1>
+ <h1 class="base-title">First article sentence.</h1>

この機能を使えば、見辛くなりがちな各要素のclass属性もすっきりと整理できそうです。

variants

画面幅に応じたスタイルの切り替え(レスポンシブ)や、hover時のスタイリングなどには、variantsと呼ばれる接頭辞を使います。

例えば、カーソルがリンクにhoverした際にフォントを太字にしたい場合は以下のようにします。

<div>
    <a class="hover:font-bold">External site</a>
</div>
.hover\:font-bold:hover {
  font-weight: 700;
}

さてこういうhover時のスタイルは、スマホやタブレットなどのタップデバイスでは意図しない挙動を起こすことがあるので、なるべく無効化しておきたいところです。

そういう場合も、uno.config.tsに独自のvariantsを定義することで対応できます。

uno.config.ts
import { defineConfig } from 'unocss';

export default defineConfig({
	variants: [
        (matcher) => {
            if (!matcher.startsWith("custom-hover:")) return matcher;
            const body = matcher.slice('custom-hover:'.length);

            return {
              matcher: `hover:${body}`,
              parent: '@media (any-hover: hover)',
            }
        }
    ]
});

こうすると、custom-hover:で定義されたスタイルは以下のように生成されます。

<div>
    <a class="custom-hover:font-bold">External site</a>
</div>
@media (any-hover: hover) {
    .custom-hover\:font-bold:hover {
      font-weight: 700;
    }
}

実に素晴らしいですね。

Utility-first CSSも悪くないと思えた

数年前にTailwindCSSを試してみた時と比べると、UnoCSSでは「生のCSSならこういうこともできるのに…」と感じるようなことはほぼありませんでした。Utility-first CSSでここまでの表現力を実現できているのは素直に感心しました。本来Utility-first CSSはコンポーネント指向のUI開発と相性の良いものですし、これなら積極的に採用しても良いかなと思えました。

Discussion