メドピア開発者ブログ

集合知により医療を再発明しようと邁進しているヘルステックカンパニーのエンジニアブログです。読者に有用な情報発信ができるよう心がけたいので応援のほどよろしくお願いします。

最小手数で始めるTailwind CSS

パクチーパクパク小宮山です。
掲題通りTailwind CSSの始め方を最小手数で書いていきます。余談は一切ありません。

tl;dr

CSS管理は諦めてTailwind CSSを使おう。

Get Started

tailwindcss.com

ひたすら公式通りに進めます。例によってフロントエンドプロジェクトの環境構築はひたすら面倒なので、Tailwind CSS以外のツールチェインはなるべく使わない構成を目指します。

installします。

$ yarn init
$ yarn add tailwindcss

セットアップします。

$ yarn tailwindcss init

こういうファイルが作られました。

tailwind.config.js

module.exports = {
  purge: [],
  theme: {
    extend: {},
  },
  variants: {},
  plugins: [],
}

スタイルのエントリーポイントなるCSSファイルを作成します。ファイル名は任意です。このファイルをTailwind CSSが用意しているCLIでビルドすることで、実際にhtmlファイルで読み込むCSSファイルが出力されます。

tailwind.css

@tailwind base;

@tailwind components;

@tailwind utilities;

TODOリスト感のある素朴なHTMLファイルを用意します。この時点ではpublic/style.cssはまだ生成されていません。

見た目だけの実装なのでフォームも飾りです。

public/index.html

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="style.css">
    <title></title>
  </head>
  <body>
    <h2>New Todo</h2>
    <form>
      <input type="text" />
      <button>submit</button>
    </form>
    <h2>List Todo</h2>
    <ul>
      <li>
        <p>todo 1</p>
        <p>2020 05/12</p>
      </li>
      <li>
        <p>todo 2</p>
        <p>2020 05/12</p>
      </li>
      <li>
        <p>todo 3</p>
        <p>2020 05/12</p>
      </li>
    </ul>
  </body>
</html>

public/style.cssを生成するためのscriptを用意しておきます。

package.json

{
  "name": "minimum-tailwindcss",
  "version": "1.0.0",
  "scripts": {
    "build:css": "tailwindcss build tailwind.css -o public/style.css"
  },
  "dependencies": {
    "tailwindcss": "^1.4.6"
  }
}

実行します。

$ yarn build:css

お好きなwebサーバーを起動してpublic/index.htmlを開きます。package.jsonを汚したくなかったのでnpxでずるして最小手数の体裁を保ちます。

$ npx http-server ./public

スタイルが何もあたっていない状態のwebサイトが完成しました。Tailwind CSSにはnormalize.cssが含まれている(v1.4.6時点)ので、リセット系CSSを別で用意する必要はありません。

f:id:robokomy:20200518174836p:plain
ネイキッドウェブサイト

準備が整ったので早速Tailwind CSS流にスタイルを当てていきます。

  <body>
    <h2 class="mb-2 px-2 text-xl">New Todo</h2>
    <form class="mb-4 px-4">
      <input type="text" class="p-2 border" />
      <button class="ml-2 p-2 rounded text-white bg-blue-500">submit</button>
    </form>
    <h2 class="mb-2 px-2 text-xl">List Todo</h2>
    <ul class="py-2 px-4">
      <li class="p-2 border">
        <p class="border-b">todo 1</p>
        <p class="text-sm">2020 05/12</p>
      </li>
      <li class="mt-2 p-2 border">
        <p class="border-b">todo 2</p>
        <p class="text-sm">2020 05/12</p>
      </li>
      <li class="mt-2 p-2 border">
        <p class="border-b">todo 3</p>
        <p class="text-sm">2020 05/12</p>
      </li>
    </ul>
  </body>

大分それっぽくなりました。

f:id:robokomy:20200511191309p:plain
それっぽい見た目

使い方は見た目通りで、classがそれぞれ特定のCSS定義として用意されています。

例えばp-4ならpadding: 1rem;mt-2ならmargin-top: 0.5rem;といった感じです。インラインスタイルを簡略化したような使用感です。

デフォルトスタイル余談

tailwindcss.com

Tailwind CSSを使う上でまず最初に注意したほうがよいことは、line-heightのデフォルト値です。デフォルトでline-height: 1.5;htmlにあたっているので、全ての余白を自力で指定してピクセルパーフェクトを目指す場合は少し厄介です。

htmlに反映されていることもあり、気にせずスタイルを当てていって途中で変更したくなってしまうと相当な被害になることが予想されます(私です)。それっぽいline-heightを全体に当てておくか、パーフェクトを目指して全てを自力で当てるかの方針はなるべく早期フェーズでの選択がおすすめです。

Vendor Prefixes余談

ターゲットとするブラウザ次第では必要になるであろう、みんな大好きVendor Prefixesです。結論を言ってしまうとTailwind CSS自体にはVendor Prefixes的な対応は入っていません。

それを解決するのはもっとうまくやれる他のツールに任せているというのが公式スタンスです。ドキュメントでもAutoprefixerとの併用が紹介されています。

tailwindcss.com

この先はもうPostCSSの話題になってしまうので深入りはしませんが、Tailwind CSSをPostCSSのプラグインとして利用することも可能なので導入もそんなに手間ではありません。

ファイルサイズ問題

Tailwind CSSを活用する上で無視することのできない非常に重要な問題がファイルサイズです。

実際にtailwindcss buildを叩いてみた方なら、このような表示がされて既に嫌な予感を持っていたかもしれません。

   🚀 Building... tailwind.css

   ✅ Finished in 1.56 s
   📦 Size: 1.95MB
   💾 Saved to public/style.css

✨  Done in 4.11s.

「📦 Size: 1.95MB」です。これは相当に大容量です。normalize.cssが含まれているとはいえ、scriptもfontも含まれていないただのCSSファイルでこれは流石に無視できるサイズではありません。

ファイルサイズが肥大化する理由は明白で、p-4mt-2といったCSSのプロパティと数値の組み合わせが無数に存在するからです。さらにレスポンシブ対応でsm:p-4なんて指定も用意されているので、それら全てが含まれていると考えれば膨れるのも当然なわけです。

実はあのBootstrapにもこのようなutility的なclass群は存在しています。しかし用意されているものは必要最低限で、Tailwind CSSほどの汎用性も拡張性もありません。

内部事情までは知りませんが、おそらくファイルサイズの肥大化という問題は少なからず意識して絞っているのではないでしょうか。

getbootstrap.com

Bootstrapがおそらく敢えて避けているであろう、ファイルサイズがひたすら肥大化していくutility的なclass群という方向にTailwind CSSは振り切っているわけです。その方向に振り切る以上、便利さと引き換えにファイルサイズは諦めなければならない・・という時代もかつてはあったのかもしれません。しかし今は令和です。あれも欲しい、これも欲しいもっともっと欲しいを実現してくれる強力なツールが存在します。

PurgeCSSです。

PurgeCSS

purgecss.com

まただよ、またフロントエンド開発環境に登場人物が増えたよ即ブラウザバックしかけた方はちょっとだけ待ってください。なんとTailwind CSSは最近のリリースでPurgeCSSも内包するようになったので、設定ファイルを微修正するだけです。Tailwind CSS陣営としても、ファイルサイズ肥大化は重要な問題で、その解決法を明示する必要があると判断したのでしょう。

github.com

デフォルトの設定ファイルから、purge部分を少しだけ変更します。Tailwind CSSのclass表現を使っているファイルが全て含まれるようにパスを指定します。そうすることで、そのファイル内で現れていない不要なclassが全て削除されます。

tailwind.config.js

module.exports = {
  purge: ['./public/**/*.html'],
  theme: {
    extend: {},
  },
  variants: {},
  plugins: [],
}

パージ機能を有効にするにはNODE_ENV=productionの指定が必要です。早速実行してみます。

NODE_ENV=production yarn build:css
   🚀 Building... tailwind.css

   ✅ Finished in 1.29 s
   📦 Size: 12.08KB
   💾 Saved to public/style.css

✨  Done in 1.87s.

CSSのファイルサイズ肥大化は、人類にとって最早克服された問題だったのです。PurgeCSSが存在するからこそTailwind CSSのutility-firstという方針が成立すると言っても過言ではないかもしれません。これぞシナジーです。Cookie Clickerをやり込んだ皆さんなら、シナジーが如何に強力かつ重要なのかは身をもって体験しているはずです。

続けて拡張の話題に移りますが、そこでもPurgeCSSという存在が控えていることが非常に重要となります。

PurgeCSS余談: 禁忌事項

PurgeCSSを使う上で、いつか足元を撃ち抜くかもしれない禁忌事項が1つあります。それは、classを必ず完全な形で記述する」ことです。

例えばfont-sizeを動的に指定しようとして、こんな記述をしてしまうかもしれません。

fontSize = 'text-' + size; // size: 4 | 6 | 8

撃ち抜きました。完全に撃ち抜いて水中から氷の天井を見上げています。理由は単純で、PurgeCSSは正規表現によって、使われているclassを探します。つまり動的に生成されたclassは発見のしようがなく、無慈悲にproductionビルド時に削除されます。

多少遠回りになっても、classを完全一致な文字列としてファイル内に記述しなければいけません。例えばこのように。

fontSize = { 4: 'text-4', 6: 'text-6', 8: 'text-8' };

PurgeCSSの要請からこのような完全一致で書く必要があるわけですが、CSSセレクタをこのように完全一致で書くことを習慣つけるのはものすごくおすすめです。以前似たような話題で開発ブログも書きました。

tech.medpeer.co.jp

以前まではgrepがしにくいというやや個人的かもしれない理由だったんですが、今ではPurgeCSSの要請という強力な後ろ盾を得たのでバンバン推していきます。

拡張

デフォルトで用意されているclassでも不便はあまりないんですが、どうしてもそれだけでは足りないシーンというのもあります。

例えばTailwind CSSはremベースの指定が基本となっています。p-4ならpadding: 1rem;'、p-6なら1.5rem`といった具合です。

なんかそれっぽい感あってrem指定いいですよね。しかし残念ながら世の中そんな甘くなく、往々にしてpx単位ベタ打ちのピクセルパーフェクトを求められてしまうことだってあります。pxremに変換してなんとか表現するという努力も悪くないですが、なかなかに不毛な作業です。

そんなときはさくっとTailwind CSSを拡張してしまいましょう。

tailwind.config.js

module.exports = {
  purge: ['./public/**/*.html'],
  theme: {
    extend: {
      spacing: {
        // px単位
        ...[...Array(120)].reduce((m, _, i) => {
          m[`${i}px`] = `${i}px`
          return m
        }, {}),
      },
    },
  },
  variants: {},
  plugins: [],
}

豪快にp-1pxからp-120pxまで用意してみました。ピクセルパーフェクトし放題です。僅かばかりの良心で120pxにしましたが、必要な分だけ増やしてしまってください。

さて、こんなことをしたらファイルサイズがどんなことになるか想像はつくと思いますが、せっかくなのでそのままビルドしてみます。

   🚀 Building... tailwind.css

   ✅ Finished in 4.59 s
   📦 Size: 4.97MB
   💾 Saved to public/style.css

✨  Done in 6.11s.

やばいですねぇ、これはやばい。それではオチも何もなく結果も見えていますがPurgeCSSを通したビルドを行なってみます。

   🚀 Building... tailwind.css

   ✅ Finished in 4.42 s
   📦 Size: 11.79KB
   💾 Saved to public/style.css

✨  Done in 4.99s.

そういうことなんですね。Tailwind CSSの拡張は本来ならばファイルサイズ肥大とトレードオフで、神経すり減らしながら必要最小限になるよう調整しなければなりません。しかし今は令和です。我々の後ろにはPurgeCSSという対不要CSS最終防衛兵器が控えています。

ファイルサイズが2倍に膨れるようなこんな拡張を施しても、使わなかった分は全て削除されます。常に必要最小限の拡張が達成可能です。

レンダリング関数との組み合わせ

この頃流行りのライブラリと組み合わせてみます。最小手数と宣言してしまっているのでなるべく最小手数で使えそうなツールを探しました。探す手間は私が負ったので見逃してください。

サンプルコードが何の環境構築もなしに簡単に動いたので今回はPreactでいきたいと思います。まともに使った経験はないのでなんとなくで使っていきます。

preactjs.com

Getting Startedにて紹介されている最小手数っぽい方法でさきほどのTodoページを書き換えてみます。リスト部分のみです。

  <body class="p-2">
    <script type="module">
      import { h, render } from "https://unpkg.com/preact?module";

      const li = i => h(
        'li',
        { class: 'mt-2 p-2 border border-red-500' },
        [
          h('p', { class: 'border-b' }, `todo ${i}`),
          h('p', { class: 'text-sm' }, '2020 05/12'),
        ]
      )

      const app = h('div', null, [
        h('ul', { class: 'py-2 px-4 border border-red-500' }, [1, 2, 3].map(i => li(i)))
      ]);

      render(app, document.body);
    </script>
  </body>

jsxのセットアップをしだすと最小手数をはみ出しそうなのでh関数でゴリゴリと書きます。ReactしかりVueしかりElmしかり大体同じ使い心地です。結局のところTailwindCSSを使うときはclassの当て方にしか関心を持つ必要がないので、どんなツールを使おうが相性が悪くなることはないです。

f:id:robokomy:20200511200704p:plain
こんな感じ

レンダリングをscriptで制御できる利点の1つといえばリストをループでまとめて書けることです。ただループで回す欠点として、当然ですが全ての要素に同じclassが当たります。

'mt-2 p-2 border border-red-500'

そうするとこのように、端っこの要素に付けたくないmarginborderが付いてしまうことがよくあります。

f:id:robokomy:20200511200721p:plain
気になる隙間

まず浮かぶ解決策は普通にclassを付けてcssを当てていく方法です。

.item:first-child { margin-top: 0; }

しかしせっかくTailwindCSS使っているのだから、見通しをよくするためにも独自のclassは極力使いたくないという欲が出てきます。よし分かったcssを使いたくないなら、scriptで制御してしまえばよいではないか方針に切り替えます。

'p-2 border border-red-500' + (i === 0 ? '' : 'mt-2')

確かにこれで解決して世界は平和になったように見えるんですが、我々が求めているのは本当にこれだったのかという疑問が残ります。

そんなところで、TailwindCSSは新たな解を用意してくれています。

これを、

'mt-2 p-2 border border-red-500'

↓こうする。

'mt-2 p-2 border border-red-500 first:mt-0'

first:mt-0'というclassが増えました。見た通りです。これを付けると&:first-child { margin-top: 0; }と同様な効果があり、リストの先頭要素だけmargin-top0にすることができてしまいます。

f:id:robokomy:20200511201224p:plain
しゅっ

後出しですが注意点として、このfirst:mt-0という機能はデフォルト設定のままでは使えません。このようなprefixで制御するスタイル機能は複数あり、すべてを有効にすると相当なファイルサイズになってしまうからです。

tailwindcss.com

有効にするにはvariantsという設定を拡張します。firstlasthoverなど有用なものは揃っているので、気になったものはとりあえず有効にしちゃいましょう。使っていないものはどうせPurgeCSSで削除されます。

追加した設定はデフォルトのものとマージはされず、上書きされるので注意してください。

tailwind.config.js

module.exports = {
  purge: ['./public/**/*.html'],
  theme: {
    extend: {},
  },
  variants: {
    margin: ['responsive', 'first', 'last'],
  },
  plugins: [],
}

first:m-0first:m-1first:mt-0という風に用意されるclassが倍々で増えるので当然ですがcssファイルサイズも相当に膨れます。

   🚀 Building... tailwind.css

   ✅ Finished in 1.69 s
   📦 Size: 2.12MB
   💾 Saved to public/style.css

はい、PurgeCSSの出番です。

   🚀 Building... tailwind.css

   ✅ Finished in 1.44 s
   📦 Size: 11.79KB
   💾 Saved to public/style.css

レンダリング関数との組み合わせ: Elm余談

メドピアでElmは一切使われていないので完全に余談なんですが、Tailwind CSSはElmプロジェクトにものすごくおすすめです。

自分自身Elmは最近少し触っているくらいでそこまで詳しくはないという前置きをしておいて、script部分に関してはその徹底した関数型言語特性から非常に強力なんですが、スタイルに関しては重要視されていないのかあまりよい戦略が見つかりません。探せばなくもないんですが、模索中な段階だったりやたらとややこしかったりするものが多いです。

ベストプラクティスじゃなくてもいいからとにかく手軽にスタイルを当てたいんだということで、結局素のCSSファイルをindex.htmlで読み込んだり、インラインスタイルを使っていくとう場面が結構あるのではないでしょうか。そして古き良きweb開発におけるスタイル管理苦難の旅路を追体験していくわけです。

そんなあなたにTailwind CSS。

div [ class "p-2 border text-blue-500" ] [ text "hello world!" ]

ただclassを提供するだけで特定フレームワークに依存しないので、もちろんElmとも相性ばっちりです。PurgeCSSは正規表現で使われているclass名を探しているだけなので.htmlでも.jsxでも.elmでもファイル形式は問題になりません。

インラインスタイルを超えて

Tailwind CSSの使い心地はインラインスタイルライクですが、そのポテンシャルはインラインスタイル特有の制約をものともしません。先ほども紹介した、first:mt-0というprefix付きの指定方法(variants)がそれです。

インラインスタイルの泣き所として、first:hover:といったセレクタや、レスポンシブのためのメディアクエリを使うことができません。まともなwebページを作る上でこれらの制約は致命的です。仕方ないから無理な部分だけCSSを別で作って対応したとしても、今度はインラインスタイルとCSSファイル内のスタイルが散らばって管理が面倒になっていきます。

一方でTailwind CSSはインラインスタイルライクではあっても中身はセレクタ指定のCSSなので、このような制約もうまく回避してくれています。

first:hover:は先の例で既に示しました。そしてレスポンシブも同じく、xs:p-1sm:p-2という風にprefixを付けるだけで分岐が可能です。

余談: そうはいっても万能ではなかった

残念ながら万能ではありません。実際に使ってみて、これは辛いなと感じたシーンもちょくちょくあります。

例えば親要素がhoverされたら子要素をdisplay: none;にしたいなんていう場面です。1つ1つのDOMに対してclassを当てていくという使い方になるので、親子であろうとDOMを跨いだスタイル制御をすることは現状だと厳しそうです。敗北した気分でしぶしぶCSSを書きましょう。

Tailwind CSS上級者の方々ならもしかしたら解決策を持っているかもしれません。求む情報発信。

実際に導入したNuxt.jsプロジェクトでのCSS比率余談

Tailwind CSS流のclassだけで実際どこまでスタイルを作れるのかは気になる点だと思います。どうしても素のCSSを書かなければならない場面があったとしても、そういう場面が多すぎたらTailwind CSSの導入はかえってスタイル定義の散逸を招いてしまうからです。

ということでNuxt.js利用の実際のプロダクトで集計してみました。.vueという拡張子のファイル153個に対して、<styleという文字列grepでヒットしたファイルが13個です。

その13個の内容はざっと見た限りこのようなものです。

  • html全体にかかるfont-familyなどの設定
  • <slot />で挿入した要素に対してのスタイル指定
  • <select>へのappearance: none;
  • アニメーション関係(transitionanimation@keyframes
  • DOM跨ぎのhover:制御

少なくないといえば少ないかもしれませんが、前向きに捉えればこれら以外はすべてTailwind CSS流儀でカバーできているわけです。まずまずな結果ではないでしょうか。

ちなみにそのプロダクトというのはこちらです。こっそりデバッグツールで覗いてみてもらうとTailwind CSSの雰囲気が分かるかもしれません。

spot-rmc.medpeer.jp

まとめ

近年のフロントエンド関連技術は激しく進化しまくっているものの、CSS関連の話題はどうしても置き去りにされやすいです。そうはいっても辛さは無視できないので様々な手法も考案されてきてはいますが、フレームワーク依存だったりまた新たな辛さが出てきたりとなかなか明る い未来は見えてきません。

そういう状況の中で、主観ベースですが、Tailwind CSSは過去最高に使い勝手が良かったです。インラインスタイルライクなのに制約が少なく拡張性が高い、そしてCSSファイルを管理する必要がほぼない。この特徴が非常に強力です。

CSS管理は諦めてTailwind CSSを使おう。現時点で私から提示できるCSS戦略のベストプラクティスです。


これは全く余談ではないんですがメドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html