🍣

Tailwind Variantsに触れてみる

2023/06/03に公開

https://www.tailwind-variants.org/

Tailwind Variantsとは

Tailwind Variants a first-class variant API library for TailwindCSS.
引用元: https://www.tailwind-variants.org/docs/introduction

ドキュメントに書かれているように、Tailwind VariantsはTailwindCSSの機能とファーストクラスのVariant APIを組み合わせた技術です。TypeScriptベースで作成されているので型安全であり、特定のフレームワークに依存しないユーティリティライブラリとなっています。

ここで登場したVariant APIという言葉ですが、これはStitchesというCSS in JSライブラリの影響を受けたものであり、Tailwind Variantsはその考え方をTailwindCSSに輸入しています。

Variantsには「変異体」という意味があるようですが、Variant APIを用いることで、一つのコンポーネントに対し複数のバージョン(バリエーション)を容易に追加することができるようになります。

StitchesのVariant APIの例

Stitchesでは次のようにstyled()を使用してvariantsを追加できます。

const Button = styled('button', {
  // base styles

  variants: {
    variant: {
      primary: {
        // primary styles
      },
      secondary: {
        // secondary styles
      },
    },
  },
});

// Use it
<Button>Button</Button>
<Button variant="primary">Primary button</Button>

もしくはcss()を使って次のようにも記述できます。

const button = css({
  // base styles

  variants: {
    variant: {
      primary: {
        // primary styles
      },
      secondary: {
        // secondary styles
      },
    },
  },
});

// Use it
<div className={button()}>Button</div>
<div className={button({ variant: 'primary' })}>Primary button</div>

Tailwind Variantsに触れてみよう

TailwindCSSのセットアップ

TailwindCSSを前提としたライブラリなので、事前にTailwindCSSのセットアップが必要です。

インストール

TailwindCSSのセットアップが済んだらTailwind Variantsをインストールしましょう。

$ npm install tailwind-variants

基本のスタイル

Tailwind Variantsでは次のようにtv()を使用してクラス名の生成を行います。

import { tv } from 'tailwind-variants';

const button = tv({
  base: 'bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded',
});

// button()
// => "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
export const Button = () => {
  return <button className={button()}>Button</button>;
};

上記のように、第一引数に渡したオブジェクトのbaseキーが基本のスタイルとなります。

button()の戻り値としてbaseの文字列がそのまま返ってきているように見えますが、内部的にはtailwind-mergeで処理されているので、TailwindCSSにおけるクラス名の非効率な重複を避けられるようになっています。

たとえばこれは以下のような挙動になるということです。

const style = tv({
  base: 'bg-blue-500 bg-red-500',
});

style()
// => "bg-red-500"

ちなみにtwMergefalseにすることで、tailwind-mergeの挙動をオフにすることも可能です。

const style = tv({
  base: 'bg-blue-500 bg-red-500',
}, {
  twMerge: false
});

style()
// => "bg-blue-500 bg-red-500"

Variants

VariantsはTailwind Variantsというライブラリの名前からしても、重要な機能だと考えられます。Variantsを用いることで同一のコンポーネントに対し、複数のバージョンのスタイルを容易に追加することができます。

Variantsを追加するためには、variantsキーを使用します。以下の例ではcolorに対してprimarysecondaryの色を設定しています。どのvariantsを使用するかは、button({ color: 'primary' })のように引数で指定します。

const button = tv({
  base: 'text-white p-4',
  variants: {
    color: {
      primary: 'bg-blue-500',
      secondary: 'bg-red-500',
    },
  },
});

button({ color: 'primary' })
// => "text-white p-4 bg-blue-500"

button({ color: 'secondary' })
// => "text-white p-4 bg-red-500"

上記では色だけを設定していますが、複数のvariantsを設定することもできます。以下の例ではcolorsizeを設定してます。

const button = tv({
  base: 'text-white p-4',
  variants: {
    color: {
      primary: 'bg-blue-500',
      secondary: 'bg-red-500',
    },
    size: {
      small: 'text-sm p-2',
      base: 'text-base p-4',
      large: 'text-lg p-6',
    },
  },
});

button({ color: 'primary', size: 'small' })
// => "text-white bg-blue-500 text-sm p-2"

button({ color: 'secondary', size: 'large' })
// => "text-white bg-blue-500 text-base p-6"

ここでsizeに注目してみると、basep-4が上書きされていることがわかります。このようにvariantsに書いたクラスは基本スタイルを上書きします。

Booleanなvariants

たとえばdisableフラグのように、booleanな値を使用してvariantsを設定することもできます。

const button = tv({
  base: 'text-white p-4',
  variants: {
    color: {
      primary: 'bg-blue-500',
      secondary: 'bg-red-500',
    },
    // booleanなvariantsを設定
    disable: {
      true: 'pointer-events-none opacity-20',
    },
  },
});

button({ color: 'primary', disable: true });
// => "text-white p-4 bg-blue-500 pointer-events-none opacity-20"

デフォルト値の設定

variantsに対し、デフォルト値を設定したい場合はdefaultVariantsキーを使用します。

const button = tv({
  base: 'text-white',
  variants: {
    color: {
      primary: 'bg-blue-500',
      secondary: 'bg-red-500',
    },
    size: {
      small: 'text-sm p-2',
      base: 'text-base p-4',
      large: 'text-lg p-6',
    },
  },
  defaultVariants: {
    color: 'primary',
    size: 'base',
  },
});

button()
// => "text-white bg-blue-500 text-base p-4"
// defaultVariantsの値がデフォルトとなる
// base() は base({ color: 'primary', size: 'base' }) と同等になる

レスポンシブの記述

TailwindCSSでレスポンシブにおけるメディアクエリの記述を煩わしく思っている人もいるかもしれません。Tailwind Variantsでは、従来の方法に加え、次のようにTailwindCSSのブレイクポイントを扱うことができます。

const button = tv(
  {
    base: 'text-white',
    variants: {
      color: {
        primary: 'bg-blue-500',
        secondary: 'bg-red-500',
      },
      size: {
        small: 'text-sm p-2',
        base: 'text-base p-4',
        large: 'text-lg p-6',
      },
    },
  },
  {
    responsiveVariants: ['md', 'lg'],
  }
);

button({
  color: 'primary',
  size: {
    initial: 'small',
    md: 'base',
    lg: 'large',
  },
})
// => "text-white bg-blue-500 text-sm p-2 md:text-base md:p-4 lg:text-lg lg:p-6"
Responsive variantsを扱うための前準備

Tailwind Variantsでレスポンシブなvariantsを扱いたい場合、前準備としてtailwind.config.jsを変更する必要があります。

tailwind.config.js
const { withTV } = require('tailwind-variants/transformer')

/** @type {import('tailwindcss').Config} */
module.exports = withTV({
  content:  ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
})

詳しくは以下のドキュメントをご覧ください。
https://www.tailwind-variants.org/docs/getting-started#responsive-variants-optional

本記事は触れませんが他にも、別のvariantsに依存したvariantsを追加できるcompoundVariantsなどの機能もあります。

Slots

Slotsを使用すると、一度に分割された複数のコンポーネントに対するスタイルを作成できます。言葉だけではイメージしづらいと思うので、実際にコード例を見てみましょう。

import { tv } from 'tailwind-variants';

const card = tv({
  slots: {
    figure: 'w-48 p-4 border-2 border-solid bg-gray-100 text-center',
    avatar: 'inline-block w-full',
    caption: 'mt-2 text-gray-900 font-bold',
  },
});

const { figure, avatar, caption } = card();

export const Card = () => {
  return (
    <figure className={figure()}>
      <img
        className={avatar()}
        src="avatar.png"
        alt="Avator"
      />
      <figcaption className={caption()}>Caption</figcaption>
    </figure>
  );
};

/*
Result:
<figure class="'w-48 p-4 border-2 border-solid bg-gray-100 text-center">
  <img src="avatar.png" alt="Avator" class="inline-block w-full">
  <figcaption class="mt-2 text-gray-900 font-bold">Caption</figcaption>
</figure>
*/

また次のようにslotsvariantsを併用して使うこともできます。

const card = tv({
  slots: {
    figure: 'w-48 p-4 border-2 border-solid bg-gray-100 text-center',
    avatar: 'inline-block w-full',
    caption: 'mt-2 text-gray-900 font-bold',
  },
  variants: {
    color: {
      light: {
        figure: 'bg-white',
        caption: 'text-black',
      },
      dark: {
        figure: 'bg-black',
        caption: 'text-white',
      },
    },
  },
});

// variantsを指定
const { figure, avatar, caption } = card({ color: 'dark' });

figure()
// => "w-48 p-4 border-2 border-solid text-center bg-black"

avatar()
// => "inline-block w-full"

caption()
// => "mt-2 font-bold text-white"

本記事ではSlotsについて、これ以上言及はしませんが、Slotsの合成レスポンシブなVariantsとの併用も行うことができます。

スタイルの上書き

class / classNameキーでスタイルの上書きも簡単にできます。

import { tv } from 'tailwind-variants';

const button = tv({
  base: 'text-white p-4 bg-blue-500',
});

button({ class: 'bg-red-500' })
// => "text-white p-4 bg-red-500"

// もしくはclassNameでも同様
button({ className: 'bg-red-500' })
// => "text-white p-4 bg-red-500"

slotsを使った場合でも、同様にclass / classNameキーで上書きが可能です。

const card = tv({
  slots: {
    figure: 'w-48 p-4 border-2 border-solid bg-gray-100 text-center',
    avatar: 'inline-block w-full',
    caption: 'mt-2 text-gray-900 font-bold',
  },
});

const { figure } = card();

figure({ class: 'text-start' });
// => "w-48 p-4 border-2 border-solid bg-gray-100 text-start"
// text-centerがtext-startに上書きされる

スタイルの拡張

extendキーを用いてスタイルの拡張を行うこともできます。

const baseButton = tv({
  base: 'p-4 bg-gray-100',
});

// extendで拡張
const successButton = tv({
  extend: baseButton,
  base: 'bg-green-700 text-white',
});

successButton()
// => "p-4 bg-green-700 text-white"

もしくは以下のように関数の戻り値を結合する方法でも可能です。

const baseButton = tv({
  base: 'p-4 bg-gray-100',
});

const successButton = tv({
  base: [baseButton(), 'bg-green-700 text-white'],
});
successButton()
// => "p-4 bg-green-700 text-white"

スタイルの拡張はbaseに限った話ではなく、variantsslotsなどのその他のキーを拡張することも可能です。以下はbaseに加えvariantsを拡張した例です。

const baseButton = tv({
  base: 'p-4 bg-gray-100',
  variants: {
    size: {
      small: 'p-2 text-sm',
      large: 'p-6 text-lg',
    },
  },
});

const successButton = tv({
  extend: baseButton,
  base: 'bg-green-700 text-white',
  variants: {
    disable: {
      true: 'opacity-20 pointer-events-none',
    },
  },
});

successButton({ size: 'small', disable: true });
// => "bg-green-700 text-white opacity-20 pointer-events-none p-2 text-sm"

おわりに

今後ライブラリの開発が進むにつれて、Tailwind Variantsを検討する機会が増えそうと思い、本記事を書いてみました。本記事で紹介した内容はあくまでTailwind Variantsの機能の一部にしか過ぎないので、興味がある方はぜひ使ってみてください!

参考

https://www.tailwind-variants.org

Discussion