🍵

【提案】TailwindCSSの新お作法、一旦これにしませんか?

2023/06/19に公開2

はじめに

最近なにかと話題のTailwindCSSですが、個人的には結構手に馴染むし、何よりもUI構造とスタイルをセットにできるところがかなり好きだったりします。

ただしTailwindCSSはその便利さゆえに自由奔放に書くことができてしまうため、無秩序なUIコンポーネントの定義ができてしまったりします。そういった状況を数々経験してきた方々は、あまりTailwindCSSをよく思わないかもしれません。

個人的には「TailwindCSSは、ルールを守って運用できれば楽しくUIコンポーネントの実装ができる」のかなと思ったりします。

今回は楽しいTailwindCSSを用いた開発を実現するために、守るべきお作法を共有できたらと思います。

個人的Tailwindcssお作法の四箇条

今回紹介したいTailwindCSSのお作法は、以下の四箇条です。

  1. classNameは見やすさ重視で改行すべし
  2. eslint + prettierで秩序を保つべし
  3. スタイルの上書き方法に注意すべし
  4. 動的な値は事前に設定すべし

ルールその壱、classNameは見やすさ重視で改行すべし

TailwindCSSの「ちょっと嫌だな」と感じる部分と言えば、やはりclassNameが信じられないくらい横長になるところですよね。やはり横長なclassNameは書いている側も読む側もとても辛いです。

例えば、こんな感じのUIコンポーネントのコードがあるとします。
この程度の長さのclassNameでも横スクロールが発生するだけで気持ちが落ち込んでしまいますよね...

sample.tsx
export function Header() {
  return (
    <div className="relative flex h-[80px] flex-col items-start justify-center border border-gray-100 p-3 shadow-sm lg:p-0">
      <HeaderContent></HeaderContent>
    </div>
  );
}

上記のようなケースでは、積極的な改行を心がけるとみんなハッピーになったります。

sample.tsx
export function Header() {
  return (
    <div
      className="
	relative flex h-80 flex-col
	items-start justify-center border
	border-gray-100 p-3
	shadow-sm lg:p-0
      "
    >
      <HeaderContent></HeaderContent>
    </div>
  );
}

どうでしょうか?これなら割と読めませんか?
少なくとも横スクロールによるストレスや煩わしさは軽減できたかなと思います。

ルールその弐、eslint + prettierで秩序を保つべし

当たり前と言えば当たり前かもしれませんが、eslint + prettierでコードの秩序を保つのはmustでやるべきかと思います。具体的には、prettier-plugin-tailwindcsseslint-plugin-tailwindcssのプラグインは必ず入れておきたいですね。

この辺のプラグインは、下記のようにclassNameの秩序をいい感じに保ってくれます。

export function Header() {
  return (
    <div
      className="
-      flex h-80
-      items-start justify-center border
-      border-gray-100 p-3 flex-col
-      shadow-sm lg:p-0 relative
+      relative flex
+      h-80 flex-col items-start
+      justify-center border border-gray-100
+      p-3 shadow-sm lg:p-0
    "
    >
      <HeaderContent></HeaderContent>
    </div>
  );
}

ルールその参、スタイルの上書き方法に注意すべし

ある程度しっかりとコンポーネント設計を行う場合、例えばAtomic designなどでガッツりコンポーネントの粒度で切り分けるようなケースでは、コンポーネントの共通化スタイルの上書きが日常茶飯事ですね。

TailwindCSSを頻繁に使ってる方々からしたら至極当たり前ですが、TailwindCSSでの下記のようなスタイルの上書きは御法度ですね。(というかこれは上書きできてません。)

export function Button({ className = "" }: { className?: string }) {
  return <button className={`relative ${className}`}>Button</button>
}

上記の例で、すでにrelativeのスタイルを渡しているにも関わらずabsoluteをコンポーネントの呼び出し側から渡してしまうと、classNameにはrelative absoluteという矛盾したスタイルが渡ってしまいます。

そこでTailwindCSSにおけるコンポーネントの共通化やスタイルの上書きに関して個人的なおすすめを紹介していきます。

tailwind-mergeを使ったやり方

tailwind-mergeを使って共有コンポーネントのスタイルを上書きする手法は個人的に結構重宝しているのでおすすめです。

使い方は至ってシンプルで、twMerge()メソッドにベースのスタイルと上書きしたいスタイルを渡すだけです。

button.tsx
"use client";

import { twMerge } from "tailwind-merge";

type Props = {
  text: string;
  type?: "button" | "submit" | "reset";
  classNames?: string;
};

const baseStyle = "relative p-3 border"

export function NormalButton({
  text,
  type = "button",
  classNames = "",
}: Props) {
  return (
    <button type={type} className={twMerge(baseStyle, classNames)}>
      {text}
    </button>
  );
}

tailwind variantsを使ったやり方

Tailwind Variantsとは、従来のTailwindCSSの機能にVariant APIを組み合わせたもので、TailwindCSSのスタイルの拡張や上書きを容易にしてくれます。

どうやら内部的にはtailwind-mergeを使っているようですので、今後はtailwind-mergeではなくTailwind Variantsを使用する機会が増えるような気がします。

Tailwind Variantsを使用することで、いろんなスタイルパターンを用意し、コンポーネントの呼び出し側でのスタイルの選択・上書きが可能です。

以下はButtonの共通コンポーネントのサンプルコードです。

button.tsx
"use client";

import { tv } from "tailwind-variants";

const button = tv({
  base: "rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700",
  variants: {
    color: {
      primary: "bg-blue-500",
      secondary: "bg-red-500",
    },
    size: {
      small: "p-2 text-sm",
      base: "p-4 text-base",
      large: "p-6 text-lg",
    },
    disable: {
      true: "pointer-events-none opacity-20",
    },
  },
});

type Props = {
  text: string;
  isDisabled?: boolean;
};

export function BaseButton({ text, isDisabled = false }: Props) {
  return (
    <button className={button({ disable: isDisabled, size: base })}>
      {text}
    </button>
  );
}

Tailwind Variantsに関しては下記の記事が非常にわかりやすかったので、詳しくはそちらをご参照いただけますと幸いです。
https://zenn.dev/yend724/articles/20230603-wgnqrgmj8kymzpev

ルールその肆、動的な値は事前に設定すべし

TailwindCSSではp-4のようなクラス名のプリセットだけではなく、p-[21px]のような動的な値を使用したクラス名を使用することができます。

このような動的なスタイルは便利な一方で、開発におけるコードの治安悪化の一因になりえます。

少し設計の話に近くなってしまいますが、動的なスタイルを使うのであれば、あらかじめtailwind.config.jsで設定するべきです。

また先ほどの例のp-[21px]のようなことがしたいのであれば、classNameで登場する1 = 1pxにするのがおすすめです。TailwindCSSでclassNameを1 = 1pxにするのは下記の記事が非常にわかりやすいので、ご参考にしてみてください。
https://zenn.dev/amon/articles/c928d3a5faa955

以下のようにあらかじめ使用するカラーパレットやスペースのサイズを決めておくと良いと思います。
spacingは1200pxまではそのまま1 = 1pxで扱えるように変更しております。

tailwind.config.ts
import type { Config } from "tailwindcss";

const spacingObj = Object.fromEntries(
  [...Array(1201)].map((_, i) => i).map((num) => [num, `${num}px`])
);

export default {
  content: [
    "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {
      backgroundImage: {
        "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
        "gradient-conic":
          "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
      },
      colors: {
        main: "#4157D0",
      },
      spacing: {
        ...spacingObj,
      },
      maxWidth: {
        ...spacingObj,
      },
      maxHeight: {
        ...spacingObj,
      },
      borderRadius: {
        "4xl": "2rem",
      },
    },
  },
  plugins: [],
} satisfies Config;

参考:
https://tailwindcss.com/blog/tailwindcss-v3-3#esm-and-type-script-support

まとめ

今回紹介したお作法は、TailwindCSSを使用する上で最低限守りたいルールですので、もっと他に「こんなルールあるといいよ」みたいなお作法があればぜひコメントで共有していただけますと大変助かります!!!!

それでは、良いTailwindライフを!🥳


Discussion

ピン留めされたアイテム
amonamon

途中、記事を紹介いただきありがとうございます。
いくつか個人的に外せないルールがありますので、コメントしてみます。

ルールその参、スタイルの上書き方法 で思ったこと

自分の話ですが、外部からのスタイル上書きを(基本的に)許さない運用をしています。
Tailwind CSS 使う使わないに関係なく、UI コンポーネントの実装設計でよく出てくる問題です。

参考: 「コンポーネント設計 スタイル上書き」と Google 検索して出てきた上位の記事
https://qiita.com/mrskiro/items/1d8c4264be2b35a428b1

なるべく上書きしない方針だと、 tailwind-merge は機能過多の場合があります。
どこまでの上書きを許すかのルール決めを、それぞれのチームで話し合わされるとよさそうに思いました。

クラス名の連結

上書きをなるべく避けるルールにした場合、ただ単にクラス名を連結したいということがあります。
この場合 classnames, clsx というライブラリが便利です。

Button.tsx
// before
export function Button({ className = "" }: { className?: string }) {
  return <button className={`relative ${className}`}>Button</button>
}

// after
import { clsx } from 'clsx';
export function Button({ className }: { className?: string }) {
  return <button className={clsx('relative', className)}>Button</button>
}

String literal での連結は、スペースを入れ忘れてタイプミスしがちだったり、クラス名の見通しが悪くなります。
また className を空文字で初期化しない場合、relative undefined となることも。
tailwind-merge を使わない場合でも、どちらかの利用のルール決めがおすすめです。

バリエーション違いのスタイル定義

例えばボタンコンポーネントだと、大きさ (small, medium) や、色 (black, primary) などのバリエーション違いを用意することがあります。
props によってクラス名を付け替えるか、Data attributes を使うかは、ルールを決めておきたいところかなと思いました。

Button.tsx
type Props = {
  size?: 'small' | 'medium'
  color?: 'primary' | 'secondary'
}

// props によってクラス名を付け替える場合
export function Button({ size = 'medium', color = 'primary' }: Props) {
  return <button className={clsx(
    size === 'medium' ? 'text-md' : 'text-sm',
    color === 'primary' ? 'bg-skyblue text-white' : 'bg-white text-gray',
  )}>Button</button>
}

// Data attributes によって適用するスタイルを変える場合
export function Button({ size = 'medium', color = 'primary' }: Props) {
  return <button className="
    data-[size=medium]:text-md data-[size=small]:text-sm
    data-[color=primary]:bg-skyblue data-[color=primary]:text-white
    data-[color=secondary]:bg-white data-[color=secondary]:text-gray
  " data-size={size} data-color={color}>Button</button>
}
udyestudyest

「動的な値は事前に設定」について、記事内のユースケース(余白を1px毎に定義)でのやり方は個人的には推しません。
余白を 1200px まで 1px ずつあらかじめ定義しておくということは、色についても #000000 ~ #ffffff の16777216色分をあらかじめ定義しておくということと同義だと感じます。

Tailwind CSS を使用することで得られる恩恵の1つに、サイト全体のスタイルを統一することができる点もあるかと思います。
使用する色を制限して統一した方がサイト全体の調和が取れるのと同様に、使用する余白も統一されていた方が良いのです。
そこで自由気ままに 1px 区切りの余白を使用することを簡単に許してしまうと、簡単に余白ガッタガタの無法地帯になりかねません。

そういったルールの下でも、やはり定義されていないイレギュラーな余白を使用したいケースは出てきます。
その場合に mt-[6px] のような動的な書き方をすることで、「この余白はサイトのスタイルルールにそぐわないイレギュラーな余白だ」ということが見ただけで明確になります。
つまり、異分子を探す手がかりにもなります。

当然、開発ルールや環境によるとはもちろん思います。
上記で申し上げたことも、結局は私個人の見解ですし、全てのプロジェクト・開発に当てはまるとも思っていません。
CSS や Tailwind CSS をあまり理解していない人にも触らせる、class 内に [] の記号が入るのが気持ち悪い、そもそもスタイルガイドなんてないし気にしていない、個人開発だから好き勝手やらせてくれ、など色々な事情があると思います。
そういった場合は1pxずつあらかじめ定義しておいた方が都合が良いのだと理解はしているつもりです。
しかし、それを「最低限は守りたい・守るべきお作法」だとして提案するにはいささか粗暴がすぎるかなあ、と感じました。