【Tailwind CSS】Tailwind VariantsでTailwind CSSを次のレベルへ引き上げよう
はじめに
今回は、Tailwind CSS
を次のレベルにするためにTailwind Variants
によるスタイリングをしようという内容です。
Tailwind CSS
は書きやすいですし、かなり使い心地がいいですが、ある点についてのみ圧倒的な短所があります。
それが「クラス名長すぎ問題」です。
私はよく以下のようなことを感じたりします。
おそらく、Tailwind CSS
を使った人がある方なら一度は思ったことあるのではないでしょうか。
そんなTailwind CSS
の短所を解決できると感じたものがTailwind Variants
です。
Tailwind Variantsとは
以下は、ドキュメントから抜粋したTailwind Variants
の定義です。
Tailwind VariantsとはTailwind CSSの機能とファーストクラスのVariant APIを組み合わせたライブラリです
ここで登場したVariant APIという言葉ですが、これはStitches
というCSS in JS
ライブラリの影響を受けたもので、Tailwind Variants
はその考え方をTailwind CSS
に輸入したライブラリです。
Stitches
では以下のようにstyled()
を使用してvariants
を追加できます。
要するに、このvariants
の考え方をTailwind CSS
製にしたライブラリということです。
const Button = styled('button', {
// base styles
variants: {
variant: {
primary: {
// primary styles
},
secondary: {
// secondary styles
},
},
},
});
<Button>Button</Button>
<Button variant="primary">Primary button</Button>
これまたドキュメントからですが、以下のような特徴があるようです。
- slots・responsive variants・components compositionなどの豊富な機能
- TypeScriptにより完全に型付けされている
- 特定のフレームワークに依存しないユーティリティ性
それでは実際の実装を見て、利点を感じてみましょう。
Tailwind Variantsを使ってみよう
それでは、早速、プロジェクトにTailwind Variants
を導入しましょう。
Tailwind Variantsを導入
インストールに当たり、前提としてTailwind CSS
がインストールされいる必要があります。
Tailwind CSS
がインストールされていない場合、以下のドキュメントに従い、Tailwind CSS
をインストールしてください。
Tailwind CSS
が導入済み・インストールできた方は以下ドキュメントに従い、Tailwind Variants
をインストールしましょう。
yarn add tailwind-variants
インストールができたら、早速、実装してみましょう。
基本の使い方
以下は、ドキュメントのIntroduction
にあるbutton
をコンポーネント化したものです。
import { tv } from 'tailwind-variants'
const button = tv({
base: 'font-medium bg-blue-500 text-white rounded-full active:opacity-80',
variants: {
color: {
primary: 'bg-blue-500 text-white',
secondary: 'bg-purple-500 text-white',
},
size: {
sm: 'text-sm',
md: 'text-base',
lg: 'px-4 py-3 text-lg',
},
},
compoundVariants: [
{
size: ['sm', 'md'],
class: 'px-3 py-1',
},
],
defaultVariants: {
size: 'md',
color: 'primary',
},
})
export const Button = () => {
return (
<button
type="button"
className={button({ size: 'sm', color: 'secondary' })}
>
Click me
</button>
)
}
基本的には、tv()
を使用してクラス名を作成していきます。
それぞれのプロパティは以下のように機能します。
-
base
: 常に適用されるスタイルを定義 -
variants
: プロパティを宣言し、引数で該当するプロパティが渡された際に適用されるスタイルを定義(上記のコードの場合、sizeがsmでcolorがsecondaryなのでtext-sm
とbg-purple-500 text-white
が適用される) -
compoundVariants
: 複数のプロパティに対して、同じスタイルを適用させられる(上記のコードの場合、sizeがsmかmdのときにpx-3 py-1
が適用される) -
defaultVariants
: 引数に指定がない場合に適用されるスタイル
基本的には上記のようにクラス名を生成し、JSXで各タグのclassName
で呼び出して使います。
基本の実装だけでも、JSXが汚れていないのがわかりますし、スタイルが宣言的なので、コードの見通しがよく、一目でどんなスタイルが当てられるかがわかると思います。
propsでスタイルを当てる場合
ただ、基本の使い方だと、コンポーネントを親コンポーネントで呼び出したい場合、柔軟性に欠けます。
しかし、Tailwind Variants
はそのあたりもカバーしており、variants
をprops
として渡すための型(VariantProps
)も提供してくれます。
import React, { FC } from 'react'
import { VariantProps, tv } from 'tailwind-variants'
const button = tv({
base: 'font-medium bg-blue-500 text-white rounded-full active:opacity-80',
variants: {
color: {
primary: 'bg-blue-500 text-white',
secondary: 'bg-purple-500 text-white',
},
size: {
sm: 'text-sm',
md: 'text-base',
lg: 'px-4 py-3 text-lg',
},
},
compoundVariants: [
{
size: ['sm', 'md'],
class: 'px-3 py-1',
},
],
defaultVariants: {
size: 'md',
color: 'primary',
},
})
type ButtonProps = VariantProps<typeof button>
export const Button: FC<ButtonProps> = (props) => {
return (
<button
type="button"
className={button({ size: props.size, color: props.color })}
>
Click Me
</button>
)
}
上記のようにButtonコンポーネントのprops
に型付けすることで、親コンポーネントでprops
によるスタイルのしても可能となります。
当然、型付けされているので、親コンポーネントでprops
の値を指定する際に補完が効きますし、カーソルホバーすれば、どのような型が適用されているかもわかります。
import { Button } from './components/Button'
const Home = () => {
return (
<main className="h-screen w-screen">
<Button size="lg" color="secondary" />
</main>
)
}
export default Home
レスポンシブ対応
さらにレスポンシブにスタイルを適用することもできます。
以下のようにresponsiveVariants
を定義することで、画面サイズに合わせて、適用させるスタイルを変更することができます。
注意点として、公式ドキュメントではxs
から指定がありますが、initial
がxs
から考えるため、xs
は不要です。(型エラーが出ます。)
import React from 'react'
import { tv } from 'tailwind-variants'
const button = tv(
{
base: 'font-medium bg-blue-500 text-white rounded-full active:opacity-80',
variants: {
color: {
primary: 'bg-blue-500 hover:bg-blue-700',
secondary: 'bg-purple-500 hover:bg-purple-700',
success: 'bg-green-500 hover:bg-green-700',
error: 'bg-red-500 hover:bg-red-700',
},
},
},
{ responsiveVariants: ['sm', 'md', 'lg'] },
)
export const Button = () => {
return (
<button
type="button"
className={button({
color: {
initial: 'primary',
sm: 'secondary',
md: 'success',
lg: 'error',
},
})}
>
Click Me
</button>
)
}
ただし、responsiveVariants
を使用する際は、tailwind.config.ts
でwithTV()
を使用する必要があります。
+ import { withTV } from 'tailwind-variants/dist/transformer.js'
import type { Config } from 'tailwindcss'
const config = {
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))',
},
},
},
plugins: [],
} satisfies Config
+ export default withTV(config)
この設定を加えることでresponsiveVariants
を使用することができます。
もちろん、通常のTailwind CSS
のようにbase
やVariants
にsm:text-sm
やxl:text-lg
なども適用できるので、可読性を考慮してみた結果であったり、実装要件に合わせて使いわけることが可能です。
複数のコンポーネントに一度にスタイルを設定
通常のプロジェクトでは、1つ1つ定数としてtv()
を設定するのは、かなり手間です。Tailwind Variants
はそれも補う機能があり、その機能がslots
です。
slots
での実装はtv()
で定義することは変わらず、プロパティとしてslots
を定義し、slots
内にbase
はwrapper
など任意のプロパティ名を定義して、各種スタイルを記述します。
あとは、分割代入でcard()
のslots
のプロパティを宣言し、スタイルを当てたいコンポーネントのclassName
に適用させていきます。
import React from 'react'
import { tv } from 'tailwind-variants'
const card = tv({
slots: {
base: 'md:flex bg-slate-100 rounded-xl p-8 md:p-0 dark:bg-gray-900',
avatar:
'w-24 h-24 md:w-48 md:h-auto md:rounded-none rounded-full mx-auto drop-shadow-lg',
wrapper: 'flex-1 pt-6 md:p-8 text-center md:text-left space-y-4',
description: 'text-md font-medium',
infoWrapper: 'font-medium',
name: 'text-sm text-sky-500 dark:text-sky-400',
role: 'text-sm text-slate-700 dark:text-slate-500',
},
})
export const Card = () => {
const { base, avatar, wrapper, description, infoWrapper, name, role } = card()
return (
<figure className={base()}>
<img
className={avatar()}
src="/avatar.png"
alt=""
width="384"
height="512"
/>
<div className={wrapper()}>
<blockquote>
<p className={description()}>
“Tailwind variants allows you to reduce repeated code in your
project and make it more readable. They fixed the headache of
building a design system with TailwindCSS.”
</p>
</blockquote>
<figcaption className={infoWrapper()}>
<div className={name()}>Zoey Lang</div>
<div className={role()}>Full-stack developer, NextUI</div>
</figcaption>
</div>
</figure>
)
}
Tailwind Variantsのスタイルを上書き
実装をしていると、「この部分だけ、別のスタイルを当てて上書きしたい」なんていう場面が結構あります。
そんな場合にはTailwind Variants
のOverrides
機能で対応できます。
例えば、以下のケースだと、「このボタンだけ少し大きくしたい」というケースに対応しています。
import React from 'react'
import { tv } from 'tailwind-variants'
const button = tv({
base: 'font-semibold text-white py-1 px-3 rounded-full active:opacity-80',
variants: {
color: {
primary: 'bg-blue-500 hover:bg-blue-700',
secondary: 'bg-purple-500 hover:bg-purple-700',
success: 'bg-green-500 hover:bg-green-700',
error: 'bg-red-500 hover:bg-red-700',
},
},
defaultVariants: {
color: 'primary',
},
})
export const Button = () => {
return (
<button type="button" className={button({ class: 'py-2 px-4' })}>
Click Me
</button>
)
}
overrides
ではbutton()
の引数にオブジェクトでclass
かclassName
を渡すことでスタイルの上書きが可能です。
上記の場合、base
で指定されているpadding
をpy-2 px-4
に上書きしています。
スタイルを引き継いで上書き
overrides
と似ていますが、以下のようなケースに対応できます。
「ボタンが2つあり、1つ目のボタンと基本は同じスタイルだけど、一部分だけ違うスタイルを当てたい。ただし、overrides
のようにJSX内での呼び出しでなく、tv()
の宣言で実装したい」
そのような場合には、extends
を使用します。
import React from 'react'
import { tv } from 'tailwind-variants'
const baseButton = tv({
base: 'font-semibold, dark:text-white py-1 px-3 rounded-full active:opacity-80 bg-zinc-100 hover:bg-zinc-200 dark:bg-zinc-800 dark:hover:bg-zinc-800',
})
const buyButton = tv({
extend: baseButton,
base: 'text-sm text-white rounded-lg shadow-lg uppercase tracking-wider bg-blue-500 hover:bg-blue-600 shadow-blue-500/50 dark:bg-blue-500 dark:hover:bg-blue-600',
})
export const Button = () => {
return (
<div className="flex gap-3">
<button type="button" className={baseButton()}>
Button
</button>
<button type="button" className={buyButton()}>
Buy button
</button>
</div>
)
}
上記のようにすることでbaseButton
を引き継いでのスタイリングができます。
公式ドキュメントでは配列でスタイルを定義していますが、文字列でも配列でもどちらでも指定可能です。
最後にslots
はよく使うことになると思うので、実践的な内容としてVariants
との組み合わせやComoound slots
という機能について解説します。
【実践編】 Slotsの機能を使ってみよう
実践編では簡易的なヘッダーのコンポーネントを使用して解説します。
まずは、複数のslots
に同じスタイルを適用できるCompound slots
という機能について解説します。
Compound slots
復習になりますが、slots
は複数のコンポーネントに対するスタイルを定義できるものでしたよね?
そのため、複数のslots
を定義していると、「この2つのslotsには同じスタイルを定義したい」という場面に遭遇すると思います。
そこで有用なのがCompound slots
という機能です。
これを使用することで、一度に複数のslots
にスタイルを適用できるので、一つ一つのslots
に対して同じスタイルの記述をしなくて済みます。
私の実装と公式の例を合わせてみると、より理解できると思います。
import React from 'react'
import { tv } from 'tailwind-variants'
const header = tv(
{
slots: {
root: 'bg-red-300',
container: 'max-w-3xl mx-auto bg-red-400 flex justify-between',
logo: '',
actions: 'flex gap-4',
signUp: 'bg-green-500',
signIn: 'bg-blue-500',
},
compoundSlots: [
{
slots: ['signUp', 'signIn'],
class: 'py-1 px-4',
},
],
}
)
export const Header = () => {
const { root, container, logo, actions, signUp, signIn } = header()
return (
<header className={root()}>
<div className={container()}>
<div className={logo()}>ロゴ</div>
<div className={actions()}>
<button type="button" className={signUp()}>
新規登録
</button>
<button type="button" className={signIn()}>
ログイン
</button>
</div>
</div>
</header>
)
}
私の場合は、signUp
とsignIn
という2つのslots
に対して、py-1 px-4
というスタイルを定義しています。
また、compoundSlots
は配列で定義するため、重複するスタイルがある場合、次々に定義することができます。
例えば、以下のようにすることができます。
compoundSlots: [
{
slots: ['signUp', 'signIn'],
class: 'py-1 px-4',
},
{
slots: ['logo', 'signUp', 'signIn'],
class: 'hover:opacity-50 rounded-full text-lg font-bold'
},
],
この場合、logo
・singUp
・signIn
には、それぞれ以下のスタイルが適用されます。
- logo:
hover:opacity-50 rounded-full text-lg font-bold
- signUp:
bg-green-500 py-1 px-4 hover:opacity-50 rounded-full text-lg font-bold
- signIn:
bg-blue-500 py-1 px-4 hover:opacity-50 rounded-full text-lg font-bold
このようにCompound slots
を使うことで記述の冗長性を防ぐことができたり、リファクタリングがしやすくなります。
SlotsとVariantsの組み合わせ
最後に、この記事で最も実践的な内容になると思われる実装を記述します。
それがSlots
とVariants
を組み合わせたスタイリングの定義になります。
公式ドキュメントはuseState
などを用いた例を実装しており、こちらのほうが、より実践的ですが、少し複雑なため、私の方は簡素化して紹介できればと思います。
import React, { FC } from 'react'
import { VariantProps, tv } from 'tailwind-variants'
const header = tv(
{
slots: {
root: 'bg-red-300',
container: 'max-w-3xl mx-auto bg-red-400 flex justify-between',
logo: '',
actions: 'flex gap-4',
signUp: 'bg-green-500',
signIn: 'bg-blue-500',
},
variants: {
type: {
fluid: {
container: 'max-w-full',
},
},
hidden: {
true: {
actions: 'hidden',
},
false: {
actions: 'flex',
},
},
color: {
primary: {
signUp:
'bg-blue-500 hover:bg-blue-700 shadow-lg rounded-full text-white dark:bg-blue-900',
signIn:
'bg-blue-500 hover:bg-blue-700 shadow-lg rounded-full text-white dark:bg-blue-900',
},
secondary: {
signUp:
'bg-purple-500 hover:bg-purple-500 shadow-lg rounded-full text-white dark:bg-purple-900',
signIn:
'bg-purple-500 hover:bg-purple-500 shadow-lg rounded-full text-white dark:bg-purple-900',
},
},
},
compoundSlots: [
{
slots: ['signUp', 'signIn'],
class: 'py-1 px-4',
},
],
},
{ responsiveVariants: ['sm'] },
)
type HeaderVariants = VariantProps<typeof header>
export const Header: FC<HeaderVariants> = ({ type, color }) => {
const { root, container, logo, actions, signUp, signIn } = header({
type,
color,
hidden: {
initial: true,
sm: false,
},
})
return (
<header className={root()}>
<div className={container()}>
<div className={logo()}>ロゴ</div>
<div className={actions()}>
<button type="button" className={signUp()}>
新規登録
</button>
<button type="button" className={signIn()}>
ログイン
</button>
</div>
</div>
</header>
)
}
まずは、Tailwind Variants
の定義内容を解説します。
各slots
の定義の部分は簡単ですよね。
次にvariants
の部分ですが、それぞれ順に見ていきましょう。
type
にfluid
が指定されていれば、slots
のcontainer
のmax-w-3xl
がmax-w-full
に上書きされます。
その他も同様の方法でネストを追っていくと、どんなスタイルが当てられるかがわかります。
hidden
では、true
の際にactions
がhidden
になり、false
ではflex
となります。
また、レスポンシブ対応されているので、実装部分も見ると画面幅が小さいときにhidden.true
のスタイルが当てられることがわかります。
color
も同様に、primary
とsecondary
という値によってsignUp
・signIn
に当てられるスタイルが変わるということがわかります。
これを親で以下のように呼び出すと、props.color
にはsecondary
が指定されているため、signUp
・signIn
にはcolor.secondary
のスタイルが当てられます。
props.type
はundefined
となるので、container
はmax-w-3xl
のままでmax-w-full
に上書きされません。
import { Header } from './components/Header'
const Home = () => {
return (
<div>
<Header color="secondary" />
</div>
)
}
export default Home
このようにslots
を使いこなすことができれば、複雑なスタイルも宣言的に定義することができ、JSXをきれいに保つことがでるため、コンポーネントのロジックやStateなどの状態が負いやすくなります。
また、それだけでなく、Tailwind CSS
のクラスも宣言的な実装になるため、何にどのスタイルが当たっているかをtv
を見るだけでわかるようになります。
以上でTailwind Variants
の大体の機能の解説はできたかなと思います。
解説していない機能も少なからずありますので、全てを知りたい方は公式ドキュメントを読んでみてください。
おわりに
非常に便利なライブラリだと思っていただけたのではないでしょうか。
- tvの宣言部分を見れば適用されるスタイルがわかる
- Tailwind CSSのクラス・JSXの可読性が上がる = 保守性が上がる
- レスポンシブ対応も簡単
- Stateなどとの組み合わせても可読性がそこまで落ちない
それ以外にもメリットは個人個人で感じたものがあるかと思います。
私、個人としてはTailwind Variants
を今後も使って開発していきたいと思いましたし、Tailwind CSS
を導入しない・できない理由はクラス名が長くなるからの一点の短所がかなり大きい理由可と思っているので、それを補えるライブラリを使うことができれば、個人開発だけでなく、企業のプロジェクトにも導入のハードルを下げることができるのではないかと思ってもいます。
最後までお読みいただき、ありがとうございました!
参考文献
Discussion