インラインスタイルと Tailwind CSS と Tailwind CSS 入力補助ライブラリと Tailwind CSS in JS

2021/03/25
このエントリーをはてなブックマークに追加

この記事では Tailwind CSS 自体の紹介はしません。公式ページを参照してください。
https://tailwindcss.com/

Tailwind CSS とインラインスタイル

Tailwind CSS 関連の記事を目にする機会が増えました。私もプロダクトに導入してしばらく使っていますが、 CSS in JS だと 10 行だったスタイルが 1 行にまとまり、コードの見通しが非常に良くなりました。多くの方には受け入れられている印象ですが、中には「なにそれインラインスタイルと同じじゃんダメじゃん」みたいな反応も見られます。まずはこれに反論しておきたいです。


Linaria (CSS in JS) から Tailwind CSS へ

インラインスタイルの問題点

そもそもインラインスタイルにはどんな問題があるでしょう?

  1. Content-Security-Policy: style-src 'unsafe-inline';
  2. 擬似クラス(:focus, :first-child など)、擬似要素(::before, ::placeholder など)、メディアクエリ(@media)などが使えない
  3. 詳細度が無条件に高い
  4. 1 行にまとめると長く読みづらくなりがち
  5. デザインシステムから逸脱しやすい
  6. 「DRY 原則」違反: あちこちで同じ記述をすることになりやすい
  7. 「関心の分離」違反: マークアップ(構造)とスタイルが分離されない

Tailwind CSS は 1., 2., 3. を解決、 4., 5. を軽減してくれます。 6. は HTML 内では問題になりますが、スクリプト内ではスタイルのまとまりを変数に格納して使いまわせば問題になりません。実際にはスタイルを React などのコンポーネントに閉じ込めてしまって、使いまわすのはコンポーネント、ということが多いと思いますが。

特筆しておきたいのは 7. です。 HTML 構造とそのスタイルは、同じ目的(たとえば「ここに検索窓を置きたい」)のために一緒に作られて、要件が変われば同時に変更される可能性が高い、同じ関心事のまとまりです。 CSS だけでは見た目が調整しきれず <div> を追加することもあるでしょう。 display: grid を使って HTML のネストを浅くすることもあるでしょう。 HTML と CSS は切っても切れません。分離すべきは「あるコンポーネントの関心事と他のコンポーネントの関心事」であって、「あるコンポーネント内の HTML とスタイル」ではありません。

インラインスタイルのメリット

さて、インラインスタイルにメリットはあるでしょうか?

  1. スタイル変更の影響範囲が極小
  2. クラスの命名に悩まない
  3. マークアップの構造とスタイルを一緒に読める

カオスな CSS に出会ったことがある方なら 1. がどれほど大きなメリットかご理解いただけると思います。また、クラスの命名は日常のストレスであり、 1 度つけた名前を後から変えたくなることも多く、 2. も意外に重要です。 3. はインラインの意味そのものであり分かりやすいメリットですが、あえて例を出すなら、たとえばマージンや flex: auto など親子兄弟関係に影響のあるスタイルプロパティは構造と合わせて把握する必要があり、 CSS が別ファイルだと非常にメンテナンスしづらいことが分かると思います。

これらのメリットは Tailwind CSS でももちろん享受できます。

Tailwind CSS のスタイル制約

インラインスタイルとは異なり、 Tailwind CSS (2.0) はスタイルに制約を与えます。サイズや余白、色として指定できる値は限られており、候補値から選ぶことで視覚的に一貫した UI が構築しやすくなります。

この制約はデザインシステムとして働く重要な機能である一方、既存プロジェクトへの Tailwind CSS 導入には妨げになる可能性があります。既存プロジェクトで Tailwind CSS にないプロパティ値を多用していた場合、 Tailwind CSS に合わせてスタイルを変更するか、 tailwind.config.js で候補値を追加するか、 Tailwind CSS を使わずにスタイリングする必要が出てきます。

制約に縛られることなく Tailwind CSS の他のメリットを享受したい場合には、 Windi CSS という選択肢があります。

Windi CSS

Windi CSS は Tailwind CSS 2.0 互換の CSS フレームワークです。さらに "p-0.8" のような半端な値や "p-3px" "p-0.75em" のような単位付きの値、 "bg-hex-00ccff" のような具体的な色指定、さらには CSS カスタムプロパティの利用と、自由度の高い記述ができます。

そして Tailwind CSS 2.0 の Purge と比較すると爆速です。

Windi CSS の詳細については中の人の記事を参照してください。

Windi CSS 導入してみたよ

設定ファイルを作るでもなく、 windicss --init みたいなこともせず、単に windicss コマンドを実行すれば CSS が吐かれます。簡単。

$ npm i -D windicss
$ npx windicss 'src/**/*.(html|ts|tsx)' -o dist/utilities.css

VS Code 向け拡張機能 でコード補完も効きます。各種ビルドツール向けプラグインも用意されているのでビルド環境に合わせてご利用ください。

tailwindcss-jit

Tailwind CSS に倣って Windi CSS が作られたわけですが、今度は Tailwind CSS 側が Windi CSS に倣って tailwindcss-jit なるツールを作っています。

機能的には Windi CSS とほぼ同じかと思います。 Tailwind CSS の候補値以外の指定に "p-[3px]" のような [] 付き書式を用いる点が異なります。あとは PostCSS のプラグインとして動作する点も異なりますね(Windi CSS は単体で動作します)。

(Windi CSS のパクり感が強く、本家という安心感があることで Windi CSS が勝負しづらそうなのが気になりますが… 競って高め合ってもらえるならありがたいです。)


4/7 追記

Tailwind CSS v2.1 がリリースされ、早速 JIT 機能が本体に(プレビューとして)取り込まれました。上記リポジトリはすでにアーカイブされています。


Tailwind CSS 入力補助ライブラリ

ここからは Tailwind CSS の関連ツールを紹介します。

tailwindcss-classnames

tailwindcss-classnames

import classnames from 'tailwindcss-classnames';

const className = classnames('border-none', 'rounded-sm', { ['bg-gray-200']: true });

tailwindcss-classnames は classnames の引数の型を TypeScript のリテラル型で宣言したライブラリです。 Tailwind CSS で利用できないクラス名が使われていればトランスパイルエラーにしてくれます。便利。

そのままだとクラス名の結合は実行時になりますが、ビルド時に結合してしまいたければ babel-plugin-optimize-clsx が使えます(使えました)。

Typed Tailwind

import { Tw } from "./tw";

const Foo = () => (
  <p className={Tw().textBlue().fontBold().$()}>
    Bold, blue text
  </p>
);

Typed Tailwind はビルダーパターンで Tailwind CSS クラス名を生成するための TypeScript のクラスです。 TypeScript の型が付いており誤りを検出できます。 npm からインストールするのではなく、 https://typed.tw/ に自分の tailwind.config.js の中身を貼り付けて出てきたソースコードをコピペして使うみたいです。私は使いません。

Tailwind CSS in JS (TS)

Tailwind CSS を便利に使うべく、 JavaScript の世界に Tailwind CSS を持ち込もうとする動きがあります。

twind

import { tw } from 'twind'

const className = tw`h-screen bg-purple-400 flex items-center justify-center`

twind は実行時にタグ付きテンプレートリテラルで使われたクラス名から <style> 要素を生成するライブラリです。 SSR にも対応しています。
クラス名の記法は Tailwind CSS から拡張されており、 tw`sm:(border(2 black opacity-50 hover:dashed))` のような例を見ると DSL 感強めな印象です。

ソースコードを静的解析して CSS ファイルを吐くような仕組みはないようです。 README にはビルド時に prepass してランタイムを取り除けるっぽい記述がありますが、やり方は見つけられませんでした。できるとしても twind の仕組み的に tw`` を呼んでまわる形で、カバレッジが試されることになるんじゃないかと想像します(間違っていたらごめんなさい)。そもそも twind はビルド不要で動的にスタイルを生成できることに価値を置いているようですので、ビルドにこだわるなら twind が解決したいところとは課題が違うとも思います。

twin.macro

import tw from 'twin.macro'

const css = tw`text-sm md:text-lg`

const Input = tw.input`border hover:border-black`

const interactionStyles = () => (
  <div tw="hover:(text-black underline) focus:(text-blue-500 underline)" />
)

import _styled from "@emotion/styled";

const css = {
  "fontSize": "0.875rem",
  "lineHeight": "1.25rem",
  "@media (min-width: 768px)": {
    "fontSize": "1.125rem",
    "lineHeight": "1.75rem"
  }
};

const Input = _styled.input({
  "borderWidth": "1px",
  ":hover": {
    "--tw-border-opacity": "1",
    "borderColor": "rgba(0, 0, 0, var(--tw-border-opacity))"
  }
});

const interactionStyles = () => /*#__PURE__*/React.createElement("div", {
  css: {
    ":hover": {
      "--tw-text-opacity": "1",
      "color": "rgba(0, 0, 0, var(--tw-text-opacity))",
      "textDecoration": "underline"
    },
    ":focus": {
      "--tw-text-opacity": "1",
      "color": "rgba(59, 130, 246, var(--tw-text-opacity))",
      "textDecoration": "underline"
    }
  }
});

twin.macro は Babel のマクロ(babel-plugin-macros)です。タグ付きテンプレートリテラルをオブジェクト形式のスタイルに変換したり、 styled-components にしたり、 JSX の tw 属性に渡したクラス名から css 属性に渡すオブジェクトに変換したりと、他の CSS in JS ライブラリと組み合わせて利用する前提になっています。

xwind

import xw from "xwind";
// または
// import xw from 'xwind/lib/macro';

const bg = "bg-red-100";

const className1 = xw`text-green-200 ${bg}`;
const className2 = xw("text-green-200", bg);

xwind は Babel のプラグインまたはマクロとしてビルド時に動作します。タグ付きテンプレートリテラルまたは関数呼び出しで Tailwind CSS のクラスを記述しますが、特徴的なのは出力形式を選べることです。 twin.macro のようにオブジェクト形式に変換して CSS in JS で利用したり、 CSS 文字列に変換して別の CSS in JS で利用したり、あるいは(ほぼ)そのまま Tailwind CSS 2.0 互換のクラス名で出力しつつ外部 CSS を吐き出すこともできます(ほぼ、と書いたのは若干の拡張構文があるからで、拡張構文分は Tailwind CSS 2.0 互換クラス名に変換されます)。また、静的に解決できる式は解決してくれます。

twstyled

export const HeroHeading = (props) => (
  <h1
    tw="font-semibold text-3xl md:text-4xl lg:text-5xl not-italic"
    {...props}
  />
)

or

export const HeroHeading = (props) => (
  <h1 font-semibold- text-3xl- md--={['text-4xl']} lg--={['text-5xl']} not-italic->
    {...props}
  />
)

twstyled は Babel のプリセットとして提供されており、 JSX を拡張して tw 属性や Tailwind CSS のクラス名に該当する属性を記述できるようにしてくれるようです。
非常に興味深いのですが、私はうまく動かせませんでした(@twstyled/babel-preset@3.2.4 時点)。まず README に npm install --save twstyled @twstyled/babel-preset と書いてありますが npm に twstyled というパッケージがありません。 package.json の resolutions フィールドでパッケージを読み替えるような指示もあり(この時点でかなり嫌悪感ある)、 npm + npm-force-resolutions で試したのですが、もしかしたら yarn 前提かもしれません(yarn では試してません)。

おわりに

あるプロダクトで、開始当初は Linaria でスタイルを構築して、途中から Tailwind CSS を導入しました。可能なら Linaria への依存をなくしてビルドプロセスを減らしたいと思っていたのですが、どうしても Tailwind CSS の制約により一部 Linaria なコードが残っていました。これから Windi CSS を導入して Linaria 依存を断ち切ろうと画策しています。

新規プロダクトならむしろスタイルに制約を課したくなりますが、 Windi CSS や tailwindcss-jit ではかなり自由に記述できるようになったので、 tailwindcss-classnames などで縛るのもアリかなと思います。

Tailwind CSS in JS なツールをいくつか紹介しましたが、 tailwindcss-jit の登場でスタイル制約が取り払われることにより、またこれから情勢が変わるんじゃないかと想像します。

私は Awesome Tailwind CSS を全部なめたわけではないので、他にもおもしろいツールがあるかもしれません。

null でした。

その他の記事

Other Articles

2022/06/03
拡張子に Web アプリを関連付ける File Handling API の使い方

2022/03/22
<selectmenu> タグできる子; <select> に代わるカスタマイズ可能なドロップダウンリスト

2022/03/02
Java 15 のテキストブロックを横目に C# 11 の生文字列リテラルを眺めて ECMAScript String dedent プロポーザルを想う

2021/10/13
Angularによる開発をできるだけ型安全にするためのKabukuでの取り組み

2021/09/30
さようなら、Node.js

2021/09/30
Union 型を含むオブジェクト型を代入するときに遭遇しうるTypeScript型チェックの制限について

2021/09/16
[ECMAScript] Pipe operator 論争まとめ – F# か Hack か両方か

2021/07/05
TypeScript v4.3 の機能を使って immutable ライブラリの型付けを頑張る

2021/06/25
Denoでwasmを動かすだけの話

2021/05/18
DOMMatrix: 2D / 3D 変形(アフィン変換)の行列を扱う DOM API

2021/03/29
GoのWASMがライブラリではなくアプリケーションであること

2021/03/26
Pythonプロジェクトの共通のひな形を作る

2021/03/23
Serverless NEGを使ってApp Engineにカスタムドメインをワイルドカードマッピング

2021/01/07
esbuild の機能が足りないならプラグインを自作すればいいじゃない

2020/08/26
TypeScriptで関数の部分型を理解しよう

2020/06/16
[Web フロントエンド] esbuild が爆速すぎて webpack / Rollup にはもう戻れない

2020/03/19
[Web フロントエンド] Elm に心折れ Mint に癒しを求める

2020/02/28
さようなら、TypeScript enum

2020/02/14
受付のLooking Glassに加えたひと工夫

2020/01/28
カブクエンジニア開発合宿に行ってきました 2020冬

2020/01/30
Renovateで依存ライブラリをリノベーションしよう 〜 Bitbucket編 〜

2019/12/27
Cloud Tasks でも deferred ライブラリが使いたい

2019/12/25
*, ::before, ::after { flex: none; }

2019/12/21
Top-level awaitとDual Package Hazard

2019/12/20
Three.jsからWebGLまで行きて帰りし物語

2019/12/18
Three.jsに入門+手を検出してAR.jsと組み合わせてみた

2019/12/04
WebXR AR Paint その2

2019/11/06
GraphQLの入門書を翻訳しました

2019/09/20
Kabuku Connect 即時見積機能のバックエンド開発

2019/08/14
Maker Faire Tokyo 2019でARゲームを出展しました

2019/07/25
夏休みだョ!WebAssembly Proposal全員集合!!

2019/07/08
鵜呑みにしないで! —— 書籍『クリーンアーキテクチャ』所感 ≪null 篇≫

2019/07/03
W3C Workshop on Web Games参加レポート

2019/06/28
TypeScriptでObject.assign()に正しい型をつける

2019/06/25
カブクエンジニア開発合宿に行ってきました 2019夏

2019/06/21
Hola! KubeCon Europe 2019の参加レポート

2019/06/19
Clean Resume きれいな環境できれいな履歴書を作成する

2019/05/20
[Web フロントエンド] 状態更新ロジックをフレームワークから独立させる

2019/04/16
C++のenable_shared_from_thisを使う

2019/04/12
OpenAPI 3 ファーストな Web アプリケーション開発(Python で API 編)

2019/04/08
WebGLでレイマーチングを使ったCSGを実現する

2019/03/29
その1 Jetson TX2でk3s(枯山水)を動かしてみた

2019/04/02
『エンジニア採用最前線』に感化されて2週間でエンジニア主導の求人票更新フローを構築した話

2019/03/27
任意のブラウザ上でJestで書いたテストを実行する

2019/02/08
TypeScript で “radian” と “degree” を間違えないようにする

2019/02/05
Python3でGoogle Cloud ML Engineをローカルで動作する方法

2019/01/18
SIGGRAPH Asia 2018 参加レポート

2019/01/08
お正月だョ!ECMAScript Proposal全員集合!!

2019/01/08
カブクエンジニア開発合宿に行ってきました 2018秋

2018/12/25
OpenAPI 3 ファーストな Web アプリケーション開発(環境編)

2018/12/23
いまMLKitカスタムモデル(TF Lite)は使えるのか

2018/12/21
[IoT] Docker on JetsonでMQTTを使ってCloud IoT Coreと通信する

2018/12/11
TypeScriptで実現する型安全な多言語対応(Angularを例に)

2018/12/05
GASでCompute Engineの時間に応じた自動停止/起動ツールを作成する 〜GASで簡単に好きなGoogle APIを叩く方法〜

2018/12/02
single quotes な Black を vendoring して packaging

2018/11/14
3次元データに2次元データの深層学習の技術(Inception V3, ResNet)を適用

2018/11/04
Node Knockout 2018 に参戦しました

2018/10/24
SIGGRAPH 2018参加レポート-後編(VR/AR)

2018/10/11
Angular 4アプリケーションをAngular 6に移行する

2018/10/05
SIGGRAPH 2018参加レポート-特別編(VR@50)

2018/10/03
Three.jsでVRしたい

2018/10/02
SIGGRAPH 2018参加レポート-前編

2018/09/27
ズーム可能なSVGを実装する方法の解説

2018/09/25
Kerasを用いた複数入力モデル精度向上のためのTips

2018/09/21
競技プログラミングの勉強会を開催している話

2018/09/19
Ladder Netwoksによる半教師あり学習

2018/08/10
「Maker Faire Tokyo 2018」に出展しました

2018/08/02
Kerasを用いた複数時系列データを1つの深層学習モデルで学習させる方法

2018/07/26
Apollo GraphQLでWebサービスを開発してわかったこと

2018/07/19
【深層学習】時系列データに対する1次元畳み込み層の出力を可視化

2018/07/11
きたない requirements.txt から Pipenv への移行

2018/06/26
CSS Houdiniを味見する

2018/06/25
不確実性を考慮した時系列データ予測

2018/06/20
Google Colaboratory を自分のマシンで走らせる

2018/06/18
Go言語でWebAssembly

2018/06/15
カブクエンジニア開発合宿に行ってきました 2018春

2018/06/08
2018 年の tree shaking

2018/06/07
隠れマルコフモデル 入門

2018/05/30
DASKによる探索的データ分析(EDA)

2018/05/10
TensorFlowをソースからビルドする方法とその効果

2018/04/23
EGLとOpenGLを使用するコードのビルド方法〜libGLからlibOpenGLへ

2018/04/23
技術書典4にサークル参加してきました

2018/04/13
Python で Cura をバッチ実行するためには

2018/04/04
ARCoreで3Dプリント風エフェクトを実現する〜呪文による積層造形映像制作の舞台裏〜

2018/04/02
深層学習を用いた時系列データにおける異常検知

2018/04/01
音声ユーザーインターフェースを用いた新方式積層造形装置の提案

2018/03/31
Container builderでコンテナイメージをBuildしてSlackで結果を受け取る開発スタイルが捗る

2018/03/23
ngUpgrade を使って AngularJS から Angular に移行

2018/03/14
Three.jsのパフォーマンスTips

2018/02/14
C++17の新機能を試す〜その1「3次元版hypot」

2018/01/17
時系列データにおける異常検知

2018/01/11
異常検知の基礎

2018/01/09
three.ar.jsを使ったスマホAR入門

2017/12/17
Python OpenAPIライブラリ bravado-core の発展的な使い方

2017/12/15
WebAssembly(wat)を手書きする

2017/12/14
AngularJS を Angular に移行: ng-annotate 相当の機能を TypeScrpt ファイルに適用

2017/12/08
Android Thingsで4足ロボットを作る ~ Android ThingsとPCA9685でサーボ制御)

2017/12/06
Raspberry PIとDialogflow & Google Cloud Platformを利用した、3Dプリンターボット(仮)の開発 (概要編)

2017/11/20
カブクエンジニア開発合宿に行ってきました 2017秋

2017/10/19
Android Thingsを使って3Dプリント戦車を作ろう ① ハードウェア準備編

2017/10/13
第2回 魁!! GPUクラスタ on GKE ~PodからGPUを使う編~

2017/10/05
第1回 魁!! GPUクラスタ on GKE ~GPUクラスタ構築編~

2017/09/13
「Maker Faire Tokyo 2017」に出展しました。

2017/09/11
PyConJP2017に参加しました

2017/09/08
bravado-coreによるOpenAPIを利用したPythonアプリケーション開発

2017/08/23
OpenAPIのご紹介

2017/08/18
EuroPython2017で2名登壇しました。

2017/07/26
3DプリンターでLチカ

2017/07/03
Three.js r86で何が変わったのか

2017/06/21
3次元データへの深層学習の適用

2017/06/01
カブクエンジニア開発合宿に行ってきました 2017春

2017/05/08
Three.js r85で何が変わったのか

2017/04/10
GCPのGPUインスタンスでレンダリングを高速化

2017/02/07
Three.js r84で何が変わったのか

2017/01/27
Google App EngineのFlexible EnvironmentにTmpfsを導入する

2016/12/21
Three.js r83で何が変わったのか

2016/12/02
Three.jsでのクリッピング平面の利用

2016/11/08
Three.js r82で何が変わったのか

2016/12/17
SIGGRAPH 2016 レポート

2016/11/02
カブクエンジニア開発合宿に行ってきました 2016秋

2016/10/28
PyConJP2016 行きました

2016/10/17
EuroPython2016で登壇しました

2016/10/13
Angular 2.0.0ファイナルへのアップグレード

2016/10/04
Three.js r81で何が変わったのか

2016/09/14
カブクのエンジニアインターンシッププログラムについての詩

2016/09/05
カブクのエンジニアインターンとして3ヶ月でやった事 〜高橋知成の場合〜

2016/08/30
Three.js r80で何が変わったのか

2016/07/15
Three.js r79で何が変わったのか

2016/06/02
Vulkanを試してみた

2016/05/20
MakerGoの作り方

2016/05/08
TensorFlow on DockerでGPUを使えるようにする方法

2016/04/27
Blenderの3DデータをMinecraftに送りこむ

2016/04/20
Tensorflowを使ったDeep LearningにおけるGPU性能調査

→
←

関連職種

Recruit

→
←

お客様のご要望に「Kabuku」はお応えいたします。
ぜひお気軽にご相談ください。

お電話でも受け付けております
03-6380-2750
営業時間:09:30~18:00
※土日祝は除く