Web production note

 【更新日 :

ViteでUnoCSSを利用する

Category:
開発環境

ViteでUnoCSSを導入したサンプルです。

なるべくTailwind CSSと同じ使用感で導入することを目指しました。

公式が提供しているViteプラグインを用いると、コーディング環境のベース設定で用いている vite-plugin-handlebars でコンポーネント化したhtmlタグのCSSが生成されない問題があったため、本記事では同じく公式が提供している PostCSSプラグイン を用いて実装しています。

UnoCSSとは

Tailwind CSSと同じようにユーティリティclassを生成できるCSS フレームワークで、Tailwind CSSとの互換性があり、拡張性にも優れていて高速に動作するツールです。

全てのclassをゼロから自分で生成していくことも可能ですが、公式が提供しているデフォルトのプリセット(@unocss/preset-uno)が、有名ユーティリティファーストのフレームワーク(Tailwind CSS、Windi CSS、Bootstrapなど)の共通スーパーセットとなっているので、他のツールからの乗り換えも比較的スムーズに実施できると思います。

プリセットのバリエーションについて

本記事ではデフォルトのプリセット(@unocss/preset-uno)のみ取り上げますが、アイコン用のプリセット(@unocss/preset-icons)や、classではなく属性値でstyleを設定できるプリセット(@unocss/preset-attributify)など、沢山のバリエーションがあるので是非一度公式のプリセットの項もご参照ください。

Tailwind CSSとUnoCSSの違い

Tailwind CSSを殆どカスタマイズせずに利用しているケースではわざわざ乗り換える恩恵は少ないかもしれませんが、ある程度設定を拡張して動作が重いと感じている方は一度検討してみても良いと思います。

メリット

  • 動作がTailwind CSSと比べて早い
    • 内部の仕組みが根本から異なるようです。Tailwind CSSは全て生成→利用していないものは取り除くといった挙動のようで tailwind.config.js で拡張していると段々動作が重くなっていくデメリットがありますが、UnoCSSは都度利用しているもののみ生成されるようで高速な動作を維持してくれます。
      • 根幹となるしくみの部分をはっきりと理解していないため、しくみの表現には間違いがあるかもしれませんが、Tailwind CSSは設定が多くなると動作が重くなるという点は実際に体験した感想です。
      • ※記事執筆時点で最新のTailwind CSS 3系での感想です。現在リリースが発表されている4系では仕様などがまた大幅に変わるようなので、4系以降では異なる感想になる可能性もあります。
  • ルール設定に正規表現が使える
    • [/^m-([\.\d]+)$/, ([, d]) => ({ margin: `${d}px` })] のように正規表現を使って書けるので柔軟かつシンプルにルール設定を書くことができます。
  • 拡張性が高い
    • プリセットの多さもそうですが独自ルールを追加しやすい仕組みになっているので、なるべく自分の思い描く通りにユーティリティclassを作りたい方はTailwind CSSよりオススメです。

デメリット

  • ドキュメントが少ないため構築のハードルがやや高い
    • 日本ではマイナーなようで記事執筆時点では参考にできそうなドキュメントはかなり少なかったです。
    • 正規表現は少々難しく見えますが、ChatGPTなどAIの力を適切に利用すれば大体は解決できるので細かい調整の難易度はAI登場以前よりは低いと思われます。
  • Tailwind CSSと100%同じ設定はできない
    • 互換性があるといっても100%ではないです。ほぼ同じ使い方はできそうですが、細かいところで差がありそうです。
      • group-hover設定においてホバー系のメディアクエリに格納する設定が存在していなかったのが個人的にはクリティカルな問題でした。(この問題に対する対処法は後述の「【任意】hover設定をメディアクエリに格納する」の項をご参照ください。)
    • Tailwind CSSとの差がある点は公式サイトの Differences from Tailwind CSS でも一部言及されています。
      • ※このドキュメントの1つ目に before:content-[''] は動作しないと書かれていますが、記事執筆時点で最新の v0.62.3 では上記classで ::before {content:""} が問題なく生成さたため、ドキュメントの更新が一部追いついていない可能性もあります。

Viteのベース構築

ベースとなるVite環境のインストールは完了していることが前提ですので、基本の構築は必要に応じて以下の記事などをご参照ください。

PostCSS PluginのUnoCSSをインストール

Vite環境にインストール

Viteをインストールしている環境と同じ場所にunocss@unocss/postcss をインストールします。

npm install -D unocss @unocss/postcss

※ autoprefixer が未インストールの場合は追加することを推奨します。

npm install -D autoprefixer

postcss.config.cjsへ呼び出しコードを追記

postcss.config.cjsがVite環境に存在しない場合は、vite.config.jsと同じ階層にファイルを作成してください。

postcss.config.cjs

module.exports = {
  plugins: {
    '@unocss/postcss': {},
    autoprefixer: {},
  }
};

Viteは標準でPostCSSに対応しているため、vite.config.js側に設定を追記する必要はありません。

拡張子を.jsにする場合

postcss.config.js とする場合はエラー回避のため module.exports = { を export default { に書き換えてください。

postcss.config.js

export default = {
  plugins: {
    '@unocss/postcss': {},
    autoprefixer: {},
  }
};

package.json内に記載されていますが、Viteは "type": "module" の形式です。"type": "module" を設定すると.jsファイルはES6モジュールとして扱うようになりますが、ES6モジュールではrequire、module、exportsなどの識別子が利用できなくなります。(代わりにimportとexportが利用できます。)

“type”: “module”設定でも従来のコードを利用したい場合は、JSファイルをCommonjsモジュールとして扱うように拡張子をcjsに変更します。(”type”: “module”に関係なくES6モジュールとして扱いたいJSファイルがある場合は拡張子をmjsにします。)

本記事ではベースの構築記事でpostcss.configの拡張子を.cjsにしているためそれに合わせています。

uno.config.tsの作成

uno.config.ts をvite.config.jsと同じ階層に作成し以下の初期設定を記述してください。

※監視するファイルのディレクトリ設定(filesystem)は環境に合わせて適宜調整してください。

uno.config.ts

import { defineConfig, presetUno } from 'unocss';

const config = {
  content: {
    filesystem: [
      'src/**/*.{html,js,ts,jsx,tsx,vue,svelte,astro}',
    ],
  },
  presets: [
    presetUno(),
  ],
};
export default defineConfig(config);

SCSSにUnoCSSのstyleを追加

style.scssにTailwind CSSのstyleを読み込むコードを追加します。

style.scss

@unocss;

ここまでで最低限の設定は完了したため、プロジェクトを開始してclassを追加するとstyle設定が有効化されるようになります。

<p class="text-3xl text-red font-bold">
  Hello world!
</p>

Tailwind CSSと同じclassが利用できるのはデフォルトのプリセット(@unocss/preset-uno)を読み込んでいるためです。

UnoCSSの利便性を上げる

VS Codeの拡張機能

公式が提供している拡張機能があるのでVS Codeを利用している方は導入しておきましょう。

  • 機能
    • マッチしたユーティリティの装飾とツールチップ
    • 自動読み込み設定
    • 一致したユーティリティの数

未検証ですがアイコンプリセットを利用している場合は、アイコンの情報を取得できる拡張機能もあるようです。

ESLintとの連携

こちらも未検証ですが、ESLintの設定(@unocss/eslint-config)もあり、classの並び替えをしたり特定のclassを利用した時に警告やエラーを出すなど、より細かい制御ができるようです。

ブラウザの拡張機能

UnoCSSのものではありませんが、デフォルトのプリセットを導入している場合はTailwind CSSと同じclass名を使うことができるため、ブラウザ上(Chrome)でプロパティ名などからTailwind CSSのclass名を検索できる以下の拡張機能も重宝すると思います。

利用できるclassを検索して設定の詳細を見る

まだベータ版のようですが、プリセットの導入で利用できるclass名などを検索できるインタラクティブドキュメントが公式から提供されています。

後述するrulesの正規表現やその他styleの詳細が参照できるので、自身で拡張する際の参考になりそうですし、カスタマイズする前に同等の機能が既に存在しないか確認する際にも重宝するかもしれません。

UnoCSS Interactive Docs で c-yellow と検索した結果ページを見る 新しいタブで開く

themeオプションでプリセットの初期styleを上書きする

Tailwind CSSにも用意されていますが、メディアクエリのブレイクポイント、色、font familyなどの値をプロジェクトに合わせて上書きすることができます。

themeで設定できるプロパティ一覧は公式ドキュメントに記載がありませんが、GitHubにあるThemeの型を定義しているファイルを参照することで設定可能なリストを知ることができます。

ここに記載がないものは追加不可のため、それ以外の設定を変更したい場合は後述するrulesで定義する必要があります。

※コード内のコメントでも記載していますが、Tailwind CSSとは異なるプロパティ名(「easing」と「duration」は、Tailwind CSSでは「transitionTimingFunction」「transitionDuration」)もあり、tailwind.config.js からそのまま移せない設定もありそうなので、Tailwind CSSから設定を移行する場合は一度上記リンクより有効なプロパティ名かどうか確認した方が良さそうです。

uno.config.ts

import { defineConfig, presetUno } from 'unocss';

const config = {
  content: {
    filesystem: [
      'src/**/*.{html,js,ts,jsx,tsx,vue,svelte,astro}',
    ],
  },
  presets: [
    presetUno(),
  ],

  /*
    ベース設定の上書き
      themeで設定できるプロパティ一覧
      https://github.com/unocss/unocss/blob/main/packages/preset-mini/src/_theme/types.ts#L15
  */
  theme: {
    breakpoints: {
      xs: '375px',
      sm: '640px',
      md: '782px',
      lg: '1024px',
      xl: '1440px',
    },
    colors: {
      black: {
        DEFAULT: '#333'
      },
      white: {
        DEFAULT: '#fff'
      },
     'gray': {
        DEFAULT: '#ccc',
     //Tailwind CSSと同じようにthemeで独自の設定追加も可能
        dark: '#888',
        'hoge-fuga': '#666',
      },
    },
    fontFamily: {
      //半角空ける必要がある場合はCSS上と同じようにダブルクオーテーションで囲む
      'sans': '"Hiragino Kaku Gothic ProN", sans-serif',
    },
    //transitionのイージング設定(Tailwind CSSでは transitionTimingFunction)
    easing: {
      DEFAULT: 'cubic-bezier(.16,1,.3,1)'
    },
    //transitionのアニメーション時間設定(Tailwind CSSでは transitionDuration)
    duration: {
      DEFAULT: '0.8s'
    },
  },
};
export default defineConfig(config);

その他Theme設定の詳細は以下をご参照ください。

themeオプションでCSS animation を設定する

CSS animation系のstyleもthemeオプションで設定可能です。

@keyframes の設定

theme.animation.keyframes に任意のタイムラインを設定できます。

uno.config.ts

import { defineConfig, presetUno } from 'unocss';

const config = {
  //省略

  /*
    ベース設定の上書き
      themeで設定できるプロパティ一覧
      https://github.com/unocss/unocss/blob/main/packages/preset-mini/src/_theme/types.ts#L15
  */
  theme: {
    //省略
    animation: {
      keyframes: {
        'custom-anime': `{
          /* タイムラインをここに記述 */
          0% {
            opacity: 1;
          }
          100% {
            opacity: 0;
          }
        }`
      },
    }
  },
};
export default defineConfig(config);

自作したタイムラインの呼び出し方法

設定したアニメーションは animate-キーフレーム名 で呼び出すことができます。

<div class="animate-custom-anime"></div>

キーフレームのみ設定した状態でclassを定義すると、以下の設定が出力されます。

.animate-arrow-anime {
  /* ショートハンド: name | duration | easing-function | delay */
  animation: custom-anime 1s linear 1;
}

animation-duration、animation-timing-function、animation-delayの設定を書き換える

プロパティ名がやや分かりづらいのですが、theme.animation 内に続けて durations(animation-duration)、timingFns(animation-timing-function)、 counts(animation-delay)の設定を追加し、プロパティ名をキーフレームの名前で値を定義すると設定の上書きが可能です。

この設定方法では定義された3種類しか編集できません。他のオプションを追加したりより細かくanimationの設定を書きたい場合は後述する theme.animation.properties で上書きしてください。

uno.config.ts

import { defineConfig, presetUno } from 'unocss';

const config = {
  //省略

  /*
    ベース設定の上書き
      themeで設定できるプロパティ一覧
      https://github.com/unocss/unocss/blob/main/packages/preset-mini/src/_theme/types.ts#L15
  */
  theme: {
    //省略
    animation: {
      keyframes: {
        'custom-anime': `{
          /* タイムラインをここに記述 */
          0% {
            opacity: 1;
          }
          100% {
            opacity: 0;
          }
        }`
      },
      //animation-duration
      durations: {
        'custom-anime': '0.8s'
      },
      //animation-timing-function
      timingFns: {
        'custom-anime': 'cubic-bezier(.16,1,.3,1)'
      },
      //animation-delay
      counts: {
        'custom-anime': 0
      },
    }
  },
};
export default defineConfig(config);

直上の設定でanimationの設定を上書きした結果

.animate-arrow-anime {
  /* ショートハンド: name | duration | easing-function | delay */
  animation: custom-anime 0.8s cubic-bezier(.16,1,.3,1) 0;
}

animationの設定を全て自分で定義する

上記の方法では3種類の調整しかできませんが、theme.animation.properties で定義するとanimationの値を全て自分で設定することができます。

uno.config.ts

import { defineConfig, presetUno } from 'unocss';

const config = {
  //省略

  /*
    ベース設定の上書き
      themeで設定できるプロパティ一覧
      https://github.com/unocss/unocss/blob/main/packages/preset-mini/src/_theme/types.ts#L15
  */
  theme: {
    //省略
    animation: {
      keyframes: {
        'custom-anime': `{
          /* タイムラインをここに記述 */
          0% {
            opacity: 1;
          }
          100% {
            opacity: 0;
          }
        }`
      },
      //animation設定を自分で定義する
      properties: {
        'custom-anime': {
          //ここに書いたanimation設定がそのまま出力される
          animation: '3s ease-in 1s 2 reverse both paused custom-anime'
        }
      },
    }
  },
};
export default defineConfig(config);

直上の設定でanimationの設定を上書きした結果

.animate-arrow-anime {
  /* ショートハンド: duration | easing-function | delay |
iteration-count | direction | fill-mode | play-state | name */
  animation: 3s ease-in 1s 2 reverse both paused custom-anime;
}

animationのショートハンド構文については以下などをご参照ください。

rulesオプションで独自ルールを追加する

Tailwind CSSだと theme.extend、addUtilities()、addComponents()、辺りが該当しそうな機能ですが、UnoCSSのrulesオプションでは正規表現を利用できるため非常に柔軟な設定が可能です。theme設定と同じ階層に続けて記載していきます。

uno.config.ts

//省略
const config = {
  //省略
  theme: { //省略
  },
  rules: [
    //配列の構造で任意数追加していく
    [/^m-(\d+)$/, ([, d]) => ({ margin: `${d}px` })],
    [/^p-(\d+)$/, ([, d]) => ({ padding: `${d}px` })],
  ]
};
export default defineConfig(config);

上記サンプルを実際に数値を当てはめて利用してみると以下の結果が出力されます。

/* 100の部分が抽出されて実際の値に入る */
.m-100 { margin: 100px; }
/* 3の部分が抽出されて実際の値に入る */
.p-3 { padding: 3px; }

以降に項目を分けていくつか事例を記載していきますが、それ以外のrulesの詳細は以下をご参照ください。

正規表現を用いず固定の独自classを追加する

配列ブロックに「class名の文字列」「設定したいプロパティを格納したオブジェクト」を順番に渡すと固定の設定を追加することができます。

入れ子の設定は書けないため、.hoge .fuga や.hoge::beforeなど複数のstyleを一緒に定義したい場合は後述の方法をお試しください。

uno.config.ts

//省略
const config = {
  //省略
  theme: { //省略
  },
  rules: [
    [
      'flex-center',
      {
        display: 'flex',
        'justify-content': 'center',
        'align-items': 'center'
      }
    ],
  ]
};
export default defineConfig(config);

固定の独自classにhover設定やclassの入れ子設定など複数の設定を追加する

Tailwind CSSではaddUtilities()、addComponents()で入れ子設定ができるため、直感的に設定を書くことができましたが、UnoCSSではジェネレーター関数を用いた書き方に変更する必要があります。

uno.config.ts

//省略
const config = {
  //省略
  theme: { //省略
  },
  rules: [
    [
      /^hover-opacity$/,
      function* ([], { symbols }) {
     //1件目の設定がhover-opacityに直接設定される
        yield {
          transition: 'opacity 0.8s cubic-bezier(.16,1,.3,1)',
          //設定したいプロパティが複数ある場合は続けて書く
          display: 'block',
        };
        //2件目以降は増やしたい設定の数だけ yield を追加していく
        yield {
          //${selector}に.hover-opacity が入るので任意のセレクタを追記する
          [symbols.selector]: (selector) => `${selector}:hover`,
          //設定したいプロパティ
          opacity: 0.7,
        };
        yield {
          [symbols.selector]: (selector) => `${selector}::before`,
          content: '""',
          //設定したいプロパティが複数ある場合は続けて書く
          display: 'block',
        };
      }
    ],
  ]
};
export default defineConfig(config);

上記設定を用いると以下のcssが出力されます。

.hover-opacity {
  transition: opacity 0.8s cubic-bezier(.16,1,.3,1);
  display: block;
}
.hover-opacity:hover {
  opacity: 0.7;
}
.hover-opacity::before {
  content: "";
  display: block;
}

ジェネレーター関数を用いた設定の詳細は、公式ドキュメントの Multi-selector rules の項をご参照ください。

themeで設定したベース値をrulesでも参照する

先ほどサンプルで作成した .hover-opacity のclassはイージング設定も書いていましたが、その設定値をthemeから引用することも可能です。

特に特殊な書き方ではなく、テンプレートリテラル+オブジェクトの値参照など、JavaScriptの一般的な方法で記載すれば設定を使い回すことができます。

uno.config.ts

//省略
const config = {
  //省略
  theme: {
    //省略

    //transitionのイージング設定(Tailwind CSSでは transitionTimingFunction)
    easing: {
      DEFAULT: 'cubic-bezier(.16,1,.3,1)'
    },
    //transitionのアニメーション時間設定(Tailwind CSSでは transitionDuration)
    duration: {
      DEFAULT: '0.8s'
    }
  },
  rules: [
    
    [
      /^hover-opacity$/,
      function* ([], { symbols }) {
        yield {
          transition: `opacity ${config.theme.duration.DEFAULT} ${config.theme.easing.DEFAULT}`,
        };
        //省略
      }
    ],
  ]
};
export default defineConfig(config);

正規表現を用いて値を動的に設定する

UnoCSSの醍醐味とも言える正規表現でのclass設定についてもいくつかサンプルを記載しておきます。

※leadingやtrackingなどclassの名称は Tailwind CSSに準じています。

例)class名「leading-[数字]」 出力結果=> line-height: [数字];

数値のみではなく小数点も許可することで直感的にline-heightが設定できるclassになります。

  • 使用例
    • leading-1 => line-height: 1.5;
    • leading-1.5 => line-height: 1.5;
    • leading-1.42 => line-height: 1.42;

uno.config.ts

//省略
const config = {
  //省略
  theme: { //省略
  },
  rules: [
    //line-height 例) leading-1.5 => line-height: 1.5
    [/^leading-(\d+(\.\d+)?)$/, ([, d]) => ({ 'line-height': d })],
  ]
};
export default defineConfig(config);

例)class名「tracking-[数字]」 出力結果=> letter-spacing: [数字]/1000em;

Figmaなどで参照できる設定値とletter-spacingの値を合わせるために1000で割った値を入れています。

正規表現で取得された数字のデータ型は文字列です。
割り算の場合は計算時にJavaScript側で自動的に数値として扱われますが、加算など不都合が出る場合もあるので常にデータ型を数値に変換して扱うようにした方が良いでしょう。

  • 使用例
    • tracking-30 => letter-spacing: 0.03em;
    • tracking-100 => letter-spacing: 0.1em;

uno.config.ts

//省略
const config = {
  //省略
  theme: { //省略
  },
  rules: [
    //letter-spacing 例) tracking-100 => letter-spacing: 0.1em
    [/^tracking-(\d+)$/, ([, d]) => ({ 'letter-spacing': `${Number(d) / 1000}em` })],
  ]
};
export default defineConfig(config);

例)複数の数値を抽出する(width-[数値1]/[数値2]per で値を%([数値1]/[数値2]*100)に変換できるclass)

正規表現の記述を増やすと、その分関数の引数に渡ってくる値が増える仕組みになっているため、複数の値でも問題なく処理できます。

  • 使用例
    • w-22/100per => 22%
    • w-123/1440per => 8.541%

JavaScriptで小数点が絡む計算は期待する結果にならない場合もあるため、小数点はある程度のところで切り捨てる方が良いと思います。(四捨五入などで繰り上げが発生してしまうと、利用ケースによっては親幅から溢れてしまう可能性を考慮し切り捨てが無難と判断しました。)

uno.config.ts

//省略
const config = {
  //省略
  theme: { //省略
  },
  rules: [
    //width %変換 例) w-123/1440per => 8.541%
    [/^width-(\d+)/(\d+)per$/, ([, d1, d2]) => {
      //数値1÷数値2の計算(小数点第3まで取得)
      const result =  Math.floor(Number(d1)/${Number(d2) * 1000) / 1000;
      return 'width': `${result}%`;
    }],
  ]
};
export default defineConfig(config);

rulesオプションを効率よく管理する

marginやpadding、widthなど複数のルールを用いる場合にどうすれば効率よく設定できるか考えてみます。

同じプロパティに正規表現を用いて複数のルールを設定する

複製して同じ設定を書いていくのは現実的ではない

例えばmarginにpx変換、rem変換、%変換など複数の設定を追加したい場合、以下のように1つずつ設定していけば一応動いてはくれます。

uno.config.ts

//省略
const config = {
  //省略
  theme: { //省略
  },
  rules: [
    [/^m-(\d+)$/, ([, d]) => ({ margin: `${d}px` })],
    [/^m-(\d+)ptr$/, ([, d]) => ({ margin: `${Number(d) / 16}rem` })],
    [/^m-(\d+)/(\d+)per$/, ([, d1, d2]) => {
      const result =  Math.floor(Number(d1)/${Number(d2) * 1000) / 1000;
      return 'margin': `${result}%`;
    }],
  ]
};
export default defineConfig(config);

しかし、margin-inline、margin-block、margin-top〜、padding〜、width〜など、複数のプロパティに対して全く同じ設定を入れたい場合は、内容を複製してclass名とプロパティ名の部分をその数だけ上書きしていく必要があり、変更が入った時に同じコードを何度も編集する必要が出てくるため、保守管理の観点からあまり良い方法ではありません。

同じ設定は全て関数化して出力してみる

最適解とは言えないかもしれませんが、共通設定を一元管理するため関数しました。関数は同じ uno.config.ts 内に定義して利用します。

コードが少々長くなってしまったため、先に使用方法などを記載しておきます。

  • 一元管理用関数setSpacing(prefix: string, propertyNames: string[])の引数
    • 第一引数:class名の接頭辞
    • 第二引数:プロパティ名を配列で渡す(複数のプロパティ名への対応)
  • 小数点は一律第3位まで残す(変更可能)
    • 小数点切り捨て関数 truncate_decimal(number: number, digit: number = 3) を作成して管理しています。
      • 一括変更:truncate_decimal() の第2引数digitの初期値を変更してください。
      • 個別変更:truncate_decimal() の第2引数に残したい桁数を数値で渡してください。
  • プロパティに追加されるルール
    • px設定
      • -[設定値] 例) mt-10
    • rem変換(px to rem でptr)
      • -[設定値]ptr 例) mt-20ptr
    • em変換
      • -[設定値]em 例) mt-1.5em
      • -[設定値]/[基準値]em 例) mt-16/20em
    • %変換
      • -[設定値]per 例) mt-1.5per
      • -[分子]/[分母]per 例) mt-10/100per
    • vw変換(px to vw でptvw)
      • (基準:ブレイクポイント:xs) -[設定値]ptvw 例) mt-20ptvw
      • (基準:ブレイクポイント:sm) -[設定値]ptvw-sm 例) mt-20ptvw-sm
      • (基準:ブレイクポイント:MD) -[設定値]ptvw-md 例) mt-20ptvw-md
      • (基準:ブレイクポイント:lg) -[設定値]ptvw-lg 例) mt-20ptvw-lg
      • (基準:ブレイクポイント:xl) -[設定値]ptvw-xl 例) mt-20ptvw-xl
    • clamp変換
      • -clamp-[最小]-[最大] 例) text-clamp-10-20

clampの計算方法はオンラインツール(Easy Clamp Generator)の出力結果を参考にさせていただきました。

拡張はCSS出力のパフォーマンスに影響するため関数内の不要なclass設定は必要に応じて削除するなど、適宜調整してください。

関数内でブレイクポイントの設定値を参照する箇所があるため、合わせてtheme.breakpointsの値も変数を参照するよう修正しています。

uno.config.ts

//省略

//ブレイクポイント(min-width)の設定
const BREAKPOINT_MIN_WIDTH_XS = 375;
const BREAKPOINT_MIN_WIDTH_SM = 640;
const BREAKPOINT_MIN_WIDTH_MD = 782; //782pxはWordPress(ブロックエディタ)のブレイクポイント
const BREAKPOINT_MIN_WIDTH_LG = 1024;
const BREAKPOINT_MIN_WIDTH_XL = 1440;
const BREAKPOINT_MIN_WIDTH_2XL = 1600;

// 1remあたりのピクセル値(通常16pxを仮定)(clampやrem変換の計算で用いる)
const REM_BASE = 16;

// clamp計算に用いるビューポートの最小値と最大値の基準設定
const VIEWPORT_MIN_WIDTH = BREAKPOINT_MIN_WIDTH_MD;
const VIEWPORT_MAX_WIDTH = BREAKPOINT_MIN_WIDTH_XL;
/*
 * clamp関数の計算を行う関数
 * @param {string} minValue - 描画する最小値
 * @param {string} maxValue - 描画する最大値
 * @return {string} - 計算結果
 */
function calculateClamp(minValue: string, maxValue: string): string {
  const minVal = Number(minValue);
  const maxVal = Number(maxValue);
  const minValueRem = (minVal / REM_BASE).toFixed(3); // remに変換
  const maxValueRem = (maxVal / REM_BASE).toFixed(3); // remに変換

  const slope = (maxVal - minVal) / (VIEWPORT_MAX_WIDTH - VIEWPORT_MIN_WIDTH);
  const yIntercept = minVal - slope * VIEWPORT_MIN_WIDTH;
  const slopeVw = (slope * 100).toFixed(3); // vwに変換

  const yInterceptRem = (yIntercept / REM_BASE).toFixed(3); // remに変換

  return `clamp(${minValueRem}rem, ${yInterceptRem}rem + ${slopeVw}vw, ${maxValueRem}rem)`;
}

/*
 * 小数点切り捨て関数
 * @param {number} number - 計算する値
 * @param {number} digit - 小数点第何位まで残すか
 * @return {number} - 計算結果
 */
function truncate_decimal(number: number, digit: number = 3): number {
  return Math.floor(number * Math.pow(10, digit)) / Math.pow(10, digit);
}

/*
 * vw計算を行う関数
 * @param {string} number - 計算する値
 * @param {number} base - 基準となる値
 * @return {string} - 計算結果vw
 */
function calculateVw(number: string, base: number): string {
  const result = (Number(number) / base) * 100;
  return `${truncate_decimal(result)}vw`;
}

/*
 * プロパティ名と値を取得する関数
 * @param {array} propertyNames - プロパティ名の配列
 * @param {string} value - 設定する値
 * @param {object} initialValue - 固定で設定するプロパティルール
 * @return {object} - プロパティルールを内包したオブジェクト
 */
function getPropertyNames(propertyNames: string[], value: string, initialValue: Record<string, string>): Record<string, string> {
  return propertyNames.reduce(
    (acc, propertyName) => {
      acc[propertyName] = value;
      return acc;
    },
    { ...initialValue }
  );
}

/*
 * プロパティルールを一括設定する関数
 * @param {string} prefix - class名の接頭辞
 * @param {string} propertyNames - 値を設定するプロパティ名の配列
 * @param {object} initialValue - 【任意】固定で設定するプロパティルール
 * @return {array} - プロパティルールを内包した配列
 */
function setProperty(prefix: string, propertyNames: string[], initialValue: Record<string, string> = {}): Array<[RegExp, (match: RegExpMatchArray) => Record<string, string>]> {
  return [
    //px設定 -[設定値] 例) mt-10
    [new RegExp(`^${prefix}-(\\d+)$`), ([, d]) => getPropertyNames(propertyNames, `${d}px`)],

    //rem変換(px to rem) -[設定値]ptr 例) mt-20ptr
    [new RegExp(`^${prefix}-(\\d+)ptr$`), ([, d]) => getPropertyNames(propertyNames, `${Number(d) / REM_BASE}rem`)],

    //em変換 -[設定値]em 例) mt-1.5em
    [new RegExp(`^${prefix}-(\\d+(\\.\\d+)?)em$`), ([, d]) => getPropertyNames(propertyNames, `${d}em`)],
    //em変換 -[設定値]/[基準値]em 例) mt-16/20em
    [
      new RegExp(`^${prefix}-(\\d+)/(\\d+)em$`),
      ([, d1, d2]) => {
        const result = Number(d1) / Number(d2);
        return getPropertyNames(propertyNames, `${truncate_decimal(result)}em`);
      }
    ],

    //%変換 -[設定値]per 例) mt-1.5per
    [new RegExp(`^${prefix}-(\\d+(\\.\\d+)?)per$`), ([, d]) => getPropertyNames(propertyNames, `${d}%`)],
    //%変換 -[分子]/[分母]per 例) mt-10/100per
    [
      new RegExp(`^${prefix}-(\\d+)/(\\d+)per$`),
      ([, d1, d2]) => {
        const result = (Number(d1) / Number(d2)) * 100;
        return getPropertyNames(propertyNames, `${truncate_decimal(result)}%`);
      }
    ],

    //vw変換(px to vw)(基準:BREAKPOINT_MIN_WIDTH_XS) -[設定値]ptvw 例) mt-20ptvw
    [
      new RegExp(`^${prefix}-(\\d+)ptvw$`),
      ([, d]) => {
        return getPropertyNames(propertyNames, calculateVw(d, BREAKPOINT_MIN_WIDTH_XS));
      }
    ],

    //vw変換(px to vw)(基準:BREAKPOINT_MIN_WIDTH_SM) -[設定値]ptvw-sm 例) mt-20ptvw-sm
    [
      new RegExp(`^${prefix}-(\\d+)ptvw-sm$`),
      ([, d]) => {
        return getPropertyNames(propertyNames, calculateVw(d, BREAKPOINT_MIN_WIDTH_SM));
      }
    ],

    //vw変換(px to vw)(基準:BREAKPOINT_MIN_WIDTH_MD) -[設定値]ptvw-md 例) mt-20ptvw-md
    [
      new RegExp(`^${prefix}-(\\d+)ptvw-md$`),
      ([, d]) => {
        return getPropertyNames(propertyNames, calculateVw(d, BREAKPOINT_MIN_WIDTH_MD));
      }
    ],

    //vw変換(px to vw)(基準:BREAKPOINT_MIN_WIDTH_LG) -[設定値]ptvw-lg 例) mt-20ptvw-lg
    [
      new RegExp(`^${prefix}-(\\d+)ptvw-lg$`),
      ([, d]) => {
        return getPropertyNames(propertyNames, calculateVw(d, BREAKPOINT_MIN_WIDTH_LG));
      }
    ],

    //vw変換(px to vw)(基準:BREAKPOINT_MIN_WIDTH_XL) -[設定値]ptvw-xl 例) mt-20ptvw-xl
    [
      new RegExp(`^${prefix}-(\\d+)ptvw-xl$`),
      ([, d]) => {
        return getPropertyNames(propertyNames, calculateVw(d, BREAKPOINT_MIN_WIDTH_XL));
      }
    ],

    //clamp変換 -clamp-[最小]-[最大] 例) mt-clamp-10-20
    [
      new RegExp(`^${prefix}-clamp-(\\d+)-(\\d+)$`),
      ([, d1, d2]) => {
        return getPropertyNames(propertyNames, `${calculateClamp(d1, d2)}`);
      }
    ]
  ];
}

const config = {
  //省略
  theme: {
    breakpoints: {
      xs: BREAKPOINT_MIN_WIDTH_XS + 'px',
      sm: BREAKPOINT_MIN_WIDTH_SM + 'px',
      md: BREAKPOINT_MIN_WIDTH_MD + 'px',
      lg: BREAKPOINT_MIN_WIDTH_LG + 'px',
      xl: BREAKPOINT_MIN_WIDTH_XL + 'px',
    },

    //省略
  },
  rules: [
    //省略

    //各数値変換設定を一括指定
    //margin
    ...setProperty('m', ['margin']),
    ...setProperty('mt', ['margin-top']),
    ...setProperty('mr', ['margin-right']),
    ...setProperty('mb', ['margin-bottom']),
    ...setProperty('ml', ['margin-left']),
    ...setProperty('mx', ['margin-inline']),
    ...setProperty('my', ['margin-block']),

    //padding
    ...setProperty('p', ['padding']),
    ...setProperty('pt', ['padding-top']),
    ...setProperty('pr', ['padding-right']),
    ...setProperty('pb', ['padding-bottom']),
    ...setProperty('pl', ['padding-left']),
    ...setProperty('px', ['padding-inline']),
    ...setProperty('py', ['padding-block']),

    //font-size
    ...setProperty('text', ['font-size']),

    //gap
    ...setProperty('gap', ['gap']),
    ...setProperty('gap-x', ['column-gap']),
    ...setProperty('gap-y', ['row-gap']),

    //height
    ...setProperty('h', ['height']),
    ...setProperty('min-h', ['min-height']),
    ...setProperty('max-h', ['max-height']),

    //width
    ...setProperty('w', ['width']),
    ...setProperty('min-w', ['min-width']),
    ...setProperty('max-w', ['max-width']),

    //border-width
    ...setProperty('border', ['border-width']),
    ...setProperty('border-t', ['border-top-width']),
    ...setProperty('border-r', ['border-right-width']),
    ...setProperty('border-b', ['border-bottom-width']),
    ...setProperty('border-l', ['border-left-width']),

    //border-radius
    ...setProperty('rounded', ['border-radius']),
    ...setProperty('rounded-t', ['border-top-left-radius', 'border-top-right-radius']),
    ...setProperty('rounded-r', ['border-top-right-radius', 'border-bottom-right-radius']),
    ...setProperty('rounded-b', ['border-bottom-right-radius', 'border-bottom-left-radius']),
    ...setProperty('rounded-l', ['border-top-left-radius', 'border-bottom-left-radius']),
    ...setProperty('rounded-tl', ['border-top-left-radius']),
    ...setProperty('rounded-tr', ['border-top-right-radius']),
    ...setProperty('rounded-br', ['border-bottom-right-radius']),
    ...setProperty('rounded-bl', ['border-bottom-left-radius']),

    //top
    ...setProperty('top', ['top']),
    //right
    ...setProperty('right', ['right']),
    //bottom
    ...setProperty('bottom', ['bottom']),
    //left
    ...setProperty('left', ['left']),
    //inset
    ...setProperty('inset', ['inset']),

    //translate Y ※固定で入れたい値がある場合は第3引数にオブジェクトを追加する
    ...setProperty('translate-y', ['--un-translate-y'], {
      transform: 'translateX(var(--un-translate-x)) translateY(var(--un-translate-y)) translateZ(var(--un-translate-z)) rotate(var(--un-rotate)) rotateX(var(--un-rotate-x)) rotateY(var(--un-rotate-y)) rotateZ(var(--un-rotate-z)) skewX(var(--un-skew-x)) skewY(var(--un-skew-y)) scaleX(var(--un-scale-x)) scaleY(var(--un-scale-y)) scaleZ(var(--un-scale-z))'
    }),
    //translate X ※固定で入れたい値がある場合は第3引数にオブジェクトを追加する
    ...setProperty('translate-x', ['--un-translate-x'], {
      transform: 'translateX(var(--un-translate-x)) translateY(var(--un-translate-y)) translateZ(var(--un-translate-z)) rotate(var(--un-rotate)) rotateX(var(--un-rotate-x)) rotateY(var(--un-rotate-y)) rotateZ(var(--un-rotate-z)) skewX(var(--un-skew-x)) skewY(var(--un-skew-y)) scaleX(var(--un-scale-x)) scaleY(var(--un-scale-y)) scaleZ(var(--un-scale-z))'
    }),
  ]
};
export default defineConfig(config);

shortcuts で複数のclass設定を1つにまとめる

uno.config.ts 内で複数のclass設定を1つにまとめたclassが生成できます。

Tailwindに例えるとcss側に定義していた@applyが近いかもしれません。

uno.config.ts

//省略
const config = {
  //省略
  theme: { //省略
  },
  shortcuts: {
    'btn': 'py-2 px-4 font-semibold rounded-lg shadow-md',
    'red': 'text-red-100',
  },
  rules: [ //省略
  ]
};
export default defineConfig(config);

未検証のため公式ドキュメントそのままですが、rulesと同じように正規表現を用いてより汎用的なclass生成も可能なようです。

uno.config.ts

//省略
const config = {
  //省略
  theme: { //省略
  },
    // 正規表現の場合は配列構造に変更する
  shortcuts: [
    {
      btn: 'py-2 px-4 font-semibold rounded-lg shadow-md',
    },
    [/^btn-(.*)$/, ([, c]) => `bg-${c}-400 text-${c}-100 py-2 px-4 rounded-lg`],
  ],
  rules: [ //省略
  ]
};
export default defineConfig(config);

出力結果

.btn-green {
  padding-top: 0.5rem;
  padding-bottom: 0.5rem;
  padding-left: 1rem;
  padding-right: 1rem;
  --un-bg-opacity: 1;
  background-color: rgb(74 222 128 / var(--un-bg-opacity));
  border-radius: 0.5rem;
  --un-text-opacity: 1;
  color: rgb(220 252 231 / var(--un-text-opacity));
}
.btn-red {
  padding-top: 0.5rem;
  padding-bottom: 0.5rem;
  padding-left: 1rem;
  padding-right: 1rem;
  --un-bg-opacity: 1;
  background-color: rgb(248 113 113 / var(--un-bg-opacity));
  border-radius: 0.5rem;
  --un-text-opacity: 1;
  color: rgb(254 226 226 / var(--un-text-opacity));
}

shortcutsについて詳しくは以下をご参照ください。

デフォルトプリセット(@unocss/preset-uno)のオプション

基本的な使い方なら変更せず初期設定のままでも特に問題なく動作するはずです。

重要なユーティリティのみ格納した小規模なプリセット(@unocss/preset-mini)と同じオプション設定が可能で、presets内で実行しているpresetUno()の引数にオブジェクトの形で渡すことで設定できます。

以下のサンプルに設定している値が全て初期値(何も渡さないpresetUno()と同じ意味)です。必要に応じて編集してください。

uno.config.ts

import { defineConfig, presetUno } from 'unocss';

const config = {
  content: {
    filesystem: [
      'src/**/*.{html,js,ts,jsx,tsx,vue,svelte,astro}',
    ],
  },
  presets: [
    presetUno({
      //ダークモードのオプション(class | media | DarkModeSelectors)
      dark: 'class',
      //.groupの代わりに[group=""]として擬似セレクタを生成する
      attributifyPseudo: false,
      //自動生成されるCSS変数の接頭辞
      variablePrefix: 'un-',
      //ユーティリティclassの接頭辞
      prefix: undefined,
      //自動生成されるCSS変数を削除するか
      //(falseにすると、transform系など一部動作しないclassがあります)
      preflight: true
    }),
  ],
  //省略
};
export default defineConfig(config);

オプションの詳細は以下のOptionsの項をご参照ください。

SCSSファイル側でUnoCSSを活用する

TailwindのようにSCSSファイル側でもUnoCSSの機能を利用することができます。

@apply(class名でstyleを生成)

style.scss

.custom-div {
  @apply text-center my-0 font-medium;
}

@screen(メディアクエリの呼び出し)

theme.breakpointsで設定したブレイクポイントをSCSS側でも引用利用できます。

@screenのみ、@screen lt-、@screen at- の3種類用意されています。

style.scss

body {
  /* uno.config.ts で定義したブレークポイントを参照 */

  /* theme.breakpoints.mdを参照(min-width) */
  @screen md {
    color: red;
  }
  /* 
    出力結果
    @media (min-width: 782px) {
      body { color: red; }
    }
  */

  /* theme.breakpoints.mdを参照(max-width) */
  @screen lt-md {
  }
  /* 
    出力結果
    @media (max-width: 781.9px) {
      body { color: red; }
    }
  */


  /* theme.breakpoints.md と theme.breakpoints.lgを参照(mdの範囲のみにstyleを当てる) */
  @screen at-md {
  }
  /* 
    出力結果
    @media (min-width: 782px) and (max-width: 1023.9px) {
      body { color: red; }
    }
  */
}

theme()(themeで設定した各値を参照する)

theme()関数を用いることでuno.config.tsのthemeで設定したベース設定を呼び出すことができます。

style.scss

$font_family: theme('fontFamily.sans');

body {
  color: theme('colors.black');
  font-family: $font_family;
}
p {
 transition: opacity theme('duration') theme('easing');
}

class名に追加するブレイクポイントの接頭辞

直上のSCSSファイル側でUnoCSSを活用する項で触れたlt、atや、Tailwind CSSで利用できるmax-やダイナミックブレイクポイントなどが活用できます。

theme.breakpointsで md: 782px, lg: 1024px と定義している例

<!-- @media (min-width: 782px) {} -->
<div class="md:mt-1"></div>

<!-- @media (max-width: 781.9px) {} -->
<div class="lt-md:mt-1"></div>
<div class="max-md:mt-1"></div>

<!-- @media (min-width: 782px) and (max-width: 1023.9px) {} -->
<div class="at-md:mt-1"></div>

<!-- @media (min-width: 1300px) {} -->
<div class="min-[1300px]-md:mt-1"></div>

<!-- @media (max-width: 1300px) {} -->
<div class="max-[1300px]-md:mt-1"></div>

その他未検証のため全て互換性があるかまでは把握していませんが、基本的なTailwind CSSのテクニックは利用できると思われます。細かい書き方の詳細は以下などをご参照ください。

【任意】hover設定をメディアクエリに格納する

Tailwind CSSには hoverOnlyWhenSupported というオプションがあり、有効化すると:hoverのstyleをホバー設定のメディアクエリに格納してくれる機能がありましたが、UnoCSSにはこれに相当するオプションがありません。

記事執筆時点では実験的な機能として、hoverのclass設定時に先頭に@をつけるとメディアを付けてくれる機能がありますが、group-hoverは対象外で実用レベルには至っていません。
そもそも@を付けるという書き方自体もベースとなるTailwind CSSの記法から外れているので、これを導入するコスト自体も少々高いと言えます。

@の使用例(@group-hover: は実装されていないので動作しない)

<!-- 
  @hoverと書くと:hover設定が以下のメディアクエリ内に格納される
  @media (hover: hover) and (pointer: fine) {}
-->
<button class="@hover:text-red"></button>

<button class="group">
  <!-- ※group-hoverには実装されていないので、以下の記述は動作しない -->
  <span class="@group-hover:text-red"></span>
</button>

UnoCSSの機能には頼らずPostCSSのプラグインで対応

postcss-media-hover-any-hover というPostCSSのプラグインを用いることでこの問題に対処できます。

Vite環境にインストール

Viteをインストールしている環境と同じ場所にインストールします。

npm install -D postcss-media-hover-any-hover

postcss.config.cjsへ呼び出しコードを追記

呼び出しコードまで記載できたら対応完了です。

postcss.config.cjs

module.exports = {
  plugins: {
    '@unocss/postcss': {},
    autoprefixer: {},
    'postcss-media-hover-any-hover': {},
  }
};

これで :hover のsyleが設定されているものは全て @media (any-hover: hover) {} に格納されるようになるため、Tailwind CSSと同じ使用感のままhoverのclass設定を利用できるようになります。

プラグイン有効化前

a:hover {
  text-decoration: underline;
}

プラグイン有効化後

@media (any-hover: hover) {
  a:hover {
    text-decoration: underline;
  }
}

hoverのメディアクエリをany-hoverにする妥当性については以下をご参照ください。

【任意】Reset CSSの追加

普段使っているReset CSSがある場合はstyle.scssでそれを読み込めば良いと思いますので任意項目としました。

デフォルトではReset CSSは含まれていないため、UnoCSSでReset CSSも挿入したい場合は @unocss/reset を追加インストールします。

npm install -D @unocss/reset

含まれているReset CSS

パッケージの中には複数のReset CSSが内包されているので、利用したいものをstyle.scss側で呼び出します。

Normalize.css (ソースコード

style.scss

@use '@unocss/reset/normalize.css';
@unocss;

sanitize.css(ソースコード

style.scss

@use '@unocss/reset/sanitize/sanitize.css';
@use '@unocss/reset/sanitize/assets.css';
@unocss;

Eric Meyer(ソースコード

style.scss

@use '@unocss/reset/eric-meyer.css';
@unocss;

Tailwind(ソースコード

style.scss

@use '@unocss/reset/tailwind.css';
@unocss;

このReset CSSは、Tawind CSSのPreflight設定に基づいた静的バージョンとして提供されているもので、テーマからスタイルを継承しませんが、デフォルトのborder-color設定のみ以下のコードを追記することで初期値を変更可能なようです。

この上書きについては公式ドキュメント内では言及されていませんが、GitHubに上がっている @unocss/reset/tailwind.css のreadme.md(/reset/tailwind.md) 内で言及されていました。

style.scss

@use '@unocss/reset/tailwind.css';
:root {
  --un-default-border-color: #e5e7eb;
}

/* 
  uno.config.ts の theme で設定している場合は
  themeから引用するとより汎用的かもしれません。
*/
:root {
  --un-default-border-color: theme('colors.black');
}

@unocss;

Tailwind compat(ソースコード

style.scss

@use '@unocss/reset/tailwind-compat.css';
@unocss;

Tailwind compatは、border-color設定の初期値上書きを含め基本的には直上の@unocss/reset/tailwind.css と同じですが、他のUI フレームワークとの競合を避けるためにボタンの背景色のオーバーライド設定のみ削除しているようです。(該当issue

reset/tailwind.css と reset/tailwind-compat.css の違い

/* @unocss/reset/tailwind.css */
button,
[type='button'],
[type='reset'],
[type='submit'] {
  -webkit-appearance: button; /* 1 */
  background-color: transparent; /* 2 */
  background-image: none; /* 2 */
}
/*
  ↓
 @unocss/reset/tailwind-compat.css
    background-colorのリセットが削除されている
*/
button,
[type='button'],
[type='reset'],
[type='submit'] {
  -webkit-appearance: button; /* 1 */
  /*background-color: transparent; !* 2 *!*/
  background-image: none; /* 2 */
}

その他 @unocss/reset の詳細は以下をご参照ください。

設定ファイルのまとめ

必要に応じて適宜調整してください。

postcss.config.cjs

module.exports = {
  plugins: {
    '@unocss/postcss': {},
    autoprefixer: {},
    'postcss-media-hover-any-hover': {},
  }
};

uno.config.ts

/*
  UnoCSS
    https://unocss.dev/guide/
*/
import { defineConfig } from 'unocss';
import presetUno from '@unocss/preset-uno';

//ブレイクポイント(min-width)の設定
const BREAKPOINT_MIN_WIDTH_XS = 375;
const BREAKPOINT_MIN_WIDTH_SM = 640;
const BREAKPOINT_MIN_WIDTH_MD = 782; //782pxはWordPress(ブロックエディタ)のブレイクポイント
const BREAKPOINT_MIN_WIDTH_LG = 1024;
const BREAKPOINT_MIN_WIDTH_XL = 1440;

// 1remあたりのピクセル値(通常16pxを仮定)(clampやrem変換の計算で用いる)
const REM_BASE = 16;

// clamp計算に用いるビューポートの最小値と最大値の基準設定
const VIEWPORT_MIN_WIDTH = BREAKPOINT_MIN_WIDTH_MD;
const VIEWPORT_MAX_WIDTH = BREAKPOINT_MIN_WIDTH_XL;
/*
 * clamp関数の計算を行う関数
 * @param {string} minValue - 描画する最小値
 * @param {string} maxValue - 描画する最大値
 * @return {string} - 計算結果
 */
function calculateClamp(minValue: string, maxValue: string): string {
  const minVal = Number(minValue);
  const maxVal = Number(maxValue);
  const minValueRem = (minVal / REM_BASE).toFixed(3); // remに変換
  const maxValueRem = (maxVal / REM_BASE).toFixed(3); // remに変換

  const slope = (maxVal - minVal) / (VIEWPORT_MAX_WIDTH - VIEWPORT_MIN_WIDTH);
  const yIntercept = minVal - slope * VIEWPORT_MIN_WIDTH;
  const slopeVw = (slope * 100).toFixed(3); // vwに変換

  const yInterceptRem = (yIntercept / REM_BASE).toFixed(3); // remに変換

  return `clamp(${minValueRem}rem, ${yInterceptRem}rem + ${slopeVw}vw, ${maxValueRem}rem)`;
}

/*
 * 小数点切り捨て関数
 * @param {number} number - 計算する値
 * @param {number} digit - 小数点第何位まで残すか
 * @return {number} - 計算結果
 */
function truncate_decimal(number: number, digit: number = 3): number {
  return Math.floor(number * Math.pow(10, digit)) / Math.pow(10, digit);
}

/*
 * vw計算を行う関数
 * @param {string} number - 計算する値
 * @param {number} base - 基準となる値
 * @return {string} - 計算結果vw
 */
function calculateVw(number: string, base: number): string {
  const result = (Number(number) / base) * 100;
  return `${truncate_decimal(result)}vw`;
}

/*
 * プロパティ名と値を取得する関数
 * @param {array} propertyNames - プロパティ名の配列
 * @param {string} value - 設定する値
 * @param {object} initialValue - 固定で設定するプロパティルール
 * @return {object} - プロパティルールを内包したオブジェクト
 */
function getPropertyNames(propertyNames: string[], value: string, initialValue: Record<string, string>): Record<string, string> {
  return propertyNames.reduce(
    (acc, propertyName) => {
      acc[propertyName] = value;
      return acc;
    },
    { ...initialValue }
  );
}

/*
 * プロパティルールを一括設定する関数
 * @param {string} prefix - class名の接頭辞
 * @param {string} propertyNames - 値を設定するプロパティ名の配列
 * @param {object} initialValue - 【任意】固定で設定するプロパティルール
 * @return {array} - プロパティルールを内包した配列
 */
function setProperty(prefix: string, propertyNames: string[], initialValue: Record<string, string> = {}): Array<[RegExp, (match: RegExpMatchArray) => Record<string, string>]> {
  return [
    //px設定 -[設定値] 例) mt-10
    [new RegExp(`^${prefix}-(\\d+)$`), ([, d]) => getPropertyNames(propertyNames, `${d}px`)],

    //rem変換(px to rem) -[設定値]ptr 例) mt-20ptr
    [new RegExp(`^${prefix}-(\\d+)ptr$`), ([, d]) => getPropertyNames(propertyNames, `${Number(d) / REM_BASE}rem`)],

    //em変換 -[設定値]em 例) mt-1.5em
    [new RegExp(`^${prefix}-(\\d+(\\.\\d+)?)em$`), ([, d]) => getPropertyNames(propertyNames, `${d}em`)],
    //em変換 -[設定値]/[基準値]em 例) mt-16/20em
    [
      new RegExp(`^${prefix}-(\\d+)/(\\d+)em$`),
      ([, d1, d2]) => {
        const result = Number(d1) / Number(d2);
        return getPropertyNames(propertyNames, `${truncate_decimal(result)}em`);
      }
    ],

    //%変換 -[設定値]per 例) mt-1.5per
    [new RegExp(`^${prefix}-(\\d+(\\.\\d+)?)per$`), ([, d]) => getPropertyNames(propertyNames, `${d}%`)],
    //%変換 -[分子]/[分母]per 例) mt-10/100per
    [
      new RegExp(`^${prefix}-(\\d+)/(\\d+)per$`),
      ([, d1, d2]) => {
        const result = (Number(d1) / Number(d2)) * 100;
        return getPropertyNames(propertyNames, `${truncate_decimal(result)}%`);
      }
    ],

    //vw変換(px to vw)(基準:BREAKPOINT_MIN_WIDTH_XS) -[設定値]ptvw 例) mt-20ptvw
    [
      new RegExp(`^${prefix}-(\\d+)ptvw$`),
      ([, d]) => {
        return getPropertyNames(propertyNames, calculateVw(d, BREAKPOINT_MIN_WIDTH_XS));
      }
    ],

    //vw変換(px to vw)(基準:BREAKPOINT_MIN_WIDTH_SM) -[設定値]ptvw-sm 例) mt-20ptvw-sm
    [
      new RegExp(`^${prefix}-(\\d+)ptvw-sm$`),
      ([, d]) => {
        return getPropertyNames(propertyNames, calculateVw(d, BREAKPOINT_MIN_WIDTH_SM));
      }
    ],

    //vw変換(px to vw)(基準:BREAKPOINT_MIN_WIDTH_MD) -[設定値]ptvw-md 例) mt-20ptvw-md
    [
      new RegExp(`^${prefix}-(\\d+)ptvw-md$`),
      ([, d]) => {
        return getPropertyNames(propertyNames, calculateVw(d, BREAKPOINT_MIN_WIDTH_MD));
      }
    ],

    //vw変換(px to vw)(基準:BREAKPOINT_MIN_WIDTH_LG) -[設定値]ptvw-lg 例) mt-20ptvw-lg
    [
      new RegExp(`^${prefix}-(\\d+)ptvw-lg$`),
      ([, d]) => {
        return getPropertyNames(propertyNames, calculateVw(d, BREAKPOINT_MIN_WIDTH_LG));
      }
    ],

    //vw変換(px to vw)(基準:BREAKPOINT_MIN_WIDTH_XL) -[設定値]ptvw-xl 例) mt-20ptvw-xl
    [
      new RegExp(`^${prefix}-(\\d+)ptvw-xl$`),
      ([, d]) => {
        return getPropertyNames(propertyNames, calculateVw(d, BREAKPOINT_MIN_WIDTH_XL));
      }
    ],

    //clamp変換 -clamp-[最小]-[最大] 例) mt-clamp-10-20
    [
      new RegExp(`^${prefix}-clamp-(\\d+)-(\\d+)$`),
      ([, d1, d2]) => {
        return getPropertyNames(propertyNames, `${calculateClamp(d1, d2)}`);
      }
    ]
  ];
}

const config = {
  content: {
    filesystem: [
      //監視対象ファイル設定
      'src/**/*.{html,js,ts,jsx,tsx,vue,svelte,astro}'
    ]
  },

  presets: [
    presetUno({
      //ダークモードのオプション(class | media | DarkModeSelectors)
      dark: 'class',
      //.groupの代わりに[group=""]として擬似セレクタを生成する
      attributifyPseudo: false,
      //自動生成されるCSS変数の接頭辞
      variablePrefix: 'un-',
      //ユーティリティclassの接頭辞
      prefix: undefined,
      //自動生成されるCSS変数を削除するか
      //(falseにすると、transform系など一部動作しないclassがあります)
      preflight: true
    }),
  ],

  /*
    ベース設定の上書き
      themeで設定できるプロパティ一覧
      https://github.com/unocss/unocss/blob/main/packages/preset-mini/src/_theme/types.ts#L15
  */
  theme: {
    breakpoints: {
      xs: BREAKPOINT_MIN_WIDTH_XS + 'px',
      sm: BREAKPOINT_MIN_WIDTH_SM + 'px',
      md: BREAKPOINT_MIN_WIDTH_MD + 'px',
      lg: BREAKPOINT_MIN_WIDTH_LG + 'px',
      xl: BREAKPOINT_MIN_WIDTH_XL + 'px',
    },
    colors: {
      black: {
        DEFAULT: '#333'
      },
      white: {
        DEFAULT: '#fff'
      },
     'gray': {
        DEFAULT: '#ccc',
     //Tailwind CSSと同じようにthemeで独自の設定追加も可能
        dark: '#888',
        'hoge-fuga': '#666',
      },
    },
    fontFamily: {
      //半角空ける必要がある場合はCSS上と同じようにダブルクオーテーションで囲む
      'sans': '"Hiragino Kaku Gothic ProN", sans-serif',
    },
    //transitionのイージング設定(Tailwind CSSでは transitionTimingFunction)
    easing: {
      DEFAULT: 'cubic-bezier(.16,1,.3,1)'
    },
    //transitionのアニメーション時間設定(Tailwind CSSでは transitionDuration)
    duration: {
      DEFAULT: '0.8s'
    },

    animation: {
      keyframes: {
        'custom-anime': `{
          /* タイムラインをここに記述 */
          0% {
            opacity: 1;
          }
          100% {
            opacity: 0;
          }
        }`,
        'custom-anime02': `{
          /* タイムラインをここに記述 */
          0% {
            opacity: 1;
          }
          100% {
            opacity: 0;
          }
        }`,
      },
      //animation-duration
      durations: {
        'custom-anime': '0.8s'
      },
      //animation-timing-function
      timingFns: {
        'custom-anime': 'cubic-bezier(.16,1,.3,1)'
      },
      //animation-delay
      counts: {
        'custom-anime': 0
      },
      //animation設定を自分で定義する
      properties: {
        'custom-anime02': {
          //ここに書いたanimation設定がそのまま出力される
          animation: '3s ease-in 1s 2 reverse both paused custom-anime02'
        }
      },
    },
  },

  //ショートカット記法
  shortcuts: [
    {
      btn: 'py-2 px-4 font-semibold rounded-lg shadow-md',
    },
    [/^btn-(.*)$/, ([, c]) => `bg-${c}-400 text-${c}-100 py-2 px-4 rounded-lg`],
  ],

  //カスタムプロパティ
  rules: [
    [
      'flex-center',
      {
        display: 'flex',
        'justify-content': 'center',
        'align-items': 'center'
      }
    ],

    [
      /^hover-opacity$/,
      function* ([], { symbols }) {
     //1件目の設定がhover-opacityに直接設定される
        yield {
          transition: 'opacity 0.8s cubic-bezier(.16,1,.3,1)',
          //設定したいプロパティが複数ある場合は続けて書く
          display: 'block',
        };
        //2件目以降は増やしたい設定の数だけ yield を追加していく
        yield {
          //${selector}に.hover-opacity が入るので任意のセレクタを追記する
          [symbols.selector]: (selector) => `${selector}:hover`,
          //設定したいプロパティ
          opacity: 0.7,
        };
        yield {
          [symbols.selector]: (selector) => `${selector}::before`,
          content: '""',
          //設定したいプロパティが複数ある場合は続けて書く
          display: 'block',
        };
      }
    ],

    //line-height 例) leading-1 => line-height: 1 leading-1.5 => line-height: 1.5
    [/^leading-(\d+(\.\d+)?)$/, ([, d]) => ({ 'line-height': d })],

    //letter-spacing 例) tracking-100 => letter-spacing: 0.1em
    [/^tracking-(\d+)$/, ([, d]) => ({ 'letter-spacing': `${Number(d) / 1000}em` })],

    //各数値変換設定を一括指定
    //margin
    ...setProperty('m', ['margin']),
    ...setProperty('mt', ['margin-top']),
    ...setProperty('mr', ['margin-right']),
    ...setProperty('mb', ['margin-bottom']),
    ...setProperty('ml', ['margin-left']),
    ...setProperty('mx', ['margin-inline']),
    ...setProperty('my', ['margin-block']),

    //padding
    ...setProperty('p', ['padding']),
    ...setProperty('pt', ['padding-top']),
    ...setProperty('pr', ['padding-right']),
    ...setProperty('pb', ['padding-bottom']),
    ...setProperty('pl', ['padding-left']),
    ...setProperty('px', ['padding-inline']),
    ...setProperty('py', ['padding-block']),

    //font-size
    ...setProperty('text', ['font-size']),

    //gap
    ...setProperty('gap', ['gap']),
    ...setProperty('gap-x', ['column-gap']),
    ...setProperty('gap-y', ['row-gap']),

    //height
    ...setProperty('h', ['height']),
    ...setProperty('min-h', ['min-height']),
    ...setProperty('max-h', ['max-height']),

    //width
    ...setProperty('w', ['width']),
    ...setProperty('min-w', ['min-width']),
    ...setProperty('max-w', ['max-width']),

    //border-width
    ...setProperty('border', ['border-width']),
    ...setProperty('border-t', ['border-top-width']),
    ...setProperty('border-r', ['border-right-width']),
    ...setProperty('border-b', ['border-bottom-width']),
    ...setProperty('border-l', ['border-left-width']),

    //border-radius
    ...setProperty('rounded', ['border-radius']),
    ...setProperty('rounded-t', ['border-top-left-radius', 'border-top-right-radius']),
    ...setProperty('rounded-r', ['border-top-right-radius', 'border-bottom-right-radius']),
    ...setProperty('rounded-b', ['border-bottom-right-radius', 'border-bottom-left-radius']),
    ...setProperty('rounded-l', ['border-top-left-radius', 'border-bottom-left-radius']),
    ...setProperty('rounded-tl', ['border-top-left-radius']),
    ...setProperty('rounded-tr', ['border-top-right-radius']),
    ...setProperty('rounded-br', ['border-bottom-right-radius']),
    ...setProperty('rounded-bl', ['border-bottom-left-radius']),

    //top
    ...setProperty('top', ['top']),
    //right
    ...setProperty('right', ['right']),
    //bottom
    ...setProperty('bottom', ['bottom']),
    //left
    ...setProperty('left', ['left']),
    //inset
    ...setProperty('inset', ['inset']),

    //translate Y ※固定で入れたい値がある場合は第3引数にオブジェクトを追加する
    ...setProperty('translate-y', ['--un-translate-y'], {
      transform: 'translateX(var(--un-translate-x)) translateY(var(--un-translate-y)) translateZ(var(--un-translate-z)) rotate(var(--un-rotate)) rotateX(var(--un-rotate-x)) rotateY(var(--un-rotate-y)) rotateZ(var(--un-rotate-z)) skewX(var(--un-skew-x)) skewY(var(--un-skew-y)) scaleX(var(--un-scale-x)) scaleY(var(--un-scale-y)) scaleZ(var(--un-scale-z))'
    }),
    //translate X ※固定で入れたい値がある場合は第3引数にオブジェクトを追加する
    ...setProperty('translate-x', ['--un-translate-x'], {
      transform: 'translateX(var(--un-translate-x)) translateY(var(--un-translate-y)) translateZ(var(--un-translate-z)) rotate(var(--un-rotate)) rotateX(var(--un-rotate-x)) rotateY(var(--un-rotate-y)) rotateZ(var(--un-rotate-z)) skewX(var(--un-skew-x)) skewY(var(--un-skew-y)) scaleX(var(--un-scale-x)) scaleY(var(--un-scale-y)) scaleZ(var(--un-scale-z))'
    }),
  ]
};

export default defineConfig(config);

関連リンク

参考リンク

目次 を閉じる