Tailwind CSSで引数のあるMixinのような仕組みを作る方法

後日追記: このブログの内容を再考して「Tailwind CSSで引数のあるMixinのような仕組みを作る方法(改)」として書き直した。


Sassでは、引数の値に応じて宣言をクラスに注入できるMixinの機能がある。たとえば次のようにすれば、フォントサイズがビューポートの幅に応じて流動的に変化するように実装できる。

@function rem($px) {
  @return ($px / 16 * 1rem);
}

// https://www.smashingmagazine.com/2022/01/modern-fluid-typography-css-clamp/
@function fluid-size($min-size, $max-size, $min-width: 640, $max-width: 1280) {
  $v: (100 * ($max-size - $min-size)) / ($max-width - $min-width);
  $r: ($min-width * $max-size - $max-width * $min-size) / ($min-width - $max-width);

  @return clamp(#{rem($min-size)}, #{$v * 1vw} + #{rem($r)}, #{rem($max-size)});
}

@mixin fluid-text($min-size, $max-size, $min-width, $max-width) {
  font-size: fluid-size($min-size, $max-size, $min-width, $max-width);
}

h1 {
  @include fluid-text(32, 64);
}

Tailwind CSSを使う場合でも、独自のプラグインを実装することで似たような仕組みが実現できる。

Tailwind CSSによってデフォルトで提供されているクラス群は、内部的には、すべてプラグイン機構を使って実装されている。プラグインの設定によって、クラス名のプリフィックスを決めたり、受け取った値に応じてどのような宣言を適用するかの処理をしたりしている。

プラグインはユーザー側でも簡単に作成できるようになっている。tailwind.config.jsに次のようなコードを書いていくだけだ。

const plugin = require('tailwindcss/plugin');

module.exports = {
  plugins: [
    plugin(function ({ addUtilities, addComponents, e, prefix, config }) {
      // Add your custom styles here
    }),
  ],
};

上記のコード例は公式ドキュメントからの引用。APIについても公式ドキュメントで解説されているので、適宜参照されたし。

これを使って、冒頭で例示したMixinのようなものを実装してみる。次のようなクラス名を記述することで意図した機能が実現できるようにする。

<div class="fluid-text-[24px,32px]">...</div>

プラグインのAPIにあるmatchUtilities関数を使うと、特定のクラス名の後ろ側に付与された値に応じてユーティリティクラスを生成する仕組みができる。

const fluidText = plugin(function ({ matchUtilities }) {
  matchUtilities({
    'fluid-text': (value) => {
      const [minSize, maxSize] = value.split(',').map((v) => {
        const matched = /^(\d+)px$/.exec(v);
        if (!matched) {
          throw new Error(`"${v}" is not a valid value`);
        }
        return Number(matched[1]);
      });

      return {
        'font-size': getFluidSize(minSize, maxSize),
      };
    },
  });
});

[]で囲われた文字列はそのままプラグインに渡される。,で区切られた箇所も、配列に変換されたりするわけではなく、単純に45rem,2remという文字列を受け取ることになる。その文字列をプラグインの実装で分割する。便宜上,で区切っているだけであって、このルールはどこかで決まっているものではない。

そして、このプラグインを使えば、次のようなCSSが生成される。

.fluid-text-\[24px\2c 32px\] {
  font-size: clamp(1.5rem, 1.25vw + 1rem, 2rem);
}

tailwind.config.jsの全体のコードは次のようになる。

const plugin = require('tailwindcss/plugin');

const rem = (px) => `${px / 16}rem`;

// https://www.smashingmagazine.com/2022/01/modern-fluid-typography-css-clamp/
const getFluidSize = (minSize, maxSize, minWidth = 640, maxWidth = 1280) => {
  const v = (100 * (maxSize - minSize)) / (maxWidth - minWidth);
  const r = (minWidth * maxSize - maxWidth * minSize) / (minWidth - maxWidth);

  return `clamp(${rem(minSize)}, ${v}vw + ${rem(r)}, ${rem(maxSize)})`;
};

const fluidText = plugin(function ({ matchUtilities }) {
  matchUtilities({
    'fluid-text': (value) => {
      const [minSize, maxSize] = value.split(',').map((v) => {
        const matched = /^(\d+)px$/.exec(v);
        if (!matched) {
          throw new Error(`"${v}" is not a valid value`);
        }
        return Number(matched[1]);
      });

      return {
        'font-size': getFluidSize(minSize, maxSize),
      };
    },
  });
});

module.exports = {
  // ...
  plugins: [fluidText],
};

プラグインを使えば、ほかにもいろいろなことができる。少なくとも、Tailwind CSSがデフォルトで提供しているようなことはすべて実現できる。corePlugins.jsの実装を眺めてみてもらえればイメージしやすいと思う。公式プラグインの実装も参考になる。