😀

reactでtailwind variantsでシンプルレスポンシブなコンポーネント作成

2024/02/11に公開

概要

tailwind cssを使ってシンプルなレスポンシブに対応したコンポーネントを作成します。

期待値とするようなコンポーネントは以下の通りです。

<Button>click here</Button>
<Button size="lg" color="secondary">
  click here
</Button>

# md以上の時はlgのサイズが適用される。
<Button size={{ initial: 'sm', md: 'lg' }}>
click here
</Button>

tailwindでレスポンシブを実現させるには text-sm sm: text-mdという感じで記載する必要があります。しかし、コンポーネントを作成したときのprops値としてしばしばfontSize=mdのpropsを期待値として取る場合があります。これではレスポンシブに対応させるような形でtailwindで書くことが難しいです。なので、最初に書いた期待値を実現させるコンポーネントを作りたいです。基本的な構想はChakraUIのものを構想としています。これをtailwindでやろうとするとどうやるかなという考えです。

使うもの

使うものは tailwindtailwind-variantsの2つを利用してpropsの要件を満たそうと思います。
tailwind-variantsは以下です。

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

引数に応じて適用することができるstyleを変更させることができるライブラリです。レスポンシブにも簡単に適用させることができるのでお勧めです。

実装

tailwind-variants

responsiveで適用させるようにするには以下の設定が必要です

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

Buttonコンポーネントの実装

Buttonコンポーネントには適当なvariants,sizeをとるシンプルなコンポーネントです。なおかつそれらのprops値はレスポンシブの形で引数を取る必要があります。
以下の型とします。

export type ResponsiveProp<T> = {
  initial?: T
  sm?: T
  md?: T
  lg?: T
  xl?: T
}
export type Responsive<T> = T | ResponsiveProp<T>

次に、適当な形でpropsのデフォルト値を設定しつつ、適当なデザインを作成できる関数を作成します。なおこの書き方は tailwind-variantsを参照してください。

type Props = {
    color?: Responsive<'primary' | 'secondary'> 
    size?: Responsive<'sm' | 'md' | 'lg'>
}

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',
       },
     },
    defaultVariants: {
        size: 'md',
        color: 'primary'
      }
   },
   {
     responsiveVariants: ['sm', 'md', 'lg', 'xl'] // `true` to apply to all screen sizes
   })

このbutton関数とpropsの繋ぎこみを行います。繋ぎ込みにはレスポンシブに対応させたような辞書型にする必要があります(単一のstringだけを受け取る場合はレスポンシブな形にする必要はないですが、全体的に作成される形を合わせるようにします)。tailwind-variantsのレスポンシブの期待値が以下の通りなため形を合わせる必要があります。

button({
  color: {
    initial: 'primary',
    xs: 'secondary',
    sm: 'success',
    md: 'error'
  }
});

以下のように形を合わせる汎用関数を作成します。

import type { ResponsiveProp, VariantsResponsive } from '@/type/index'

export const normalizeResponsiveStyle = <T>(variants?: ResponsiveProp<T> | T, key: string): VariantsResponsive<T> => {
  if (!variants) {
    return {}
  }
  else if (typeof variants === 'string') {
    return { [key]: { initial: variants } }
  } else {
    return  { [key]: variants }
  }
}

ものすごく簡単な関数ですね。

総合的に完成させたコンポーネントが以下の通りです。

import { tv } from 'tailwind-variants'
import { normalizeResponsiveStyle } from '@/utils/style'
import type { Responsive } from '@/type'
type Props = {
    color?: Responsive<'primary' | 'secondary'>
    size?: Responsive<'sm' | 'md' | 'lg'>
}

export const Button = (props: Props) => {
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',
       },
     },
    defaultVariants: {
        size: 'md',
        color: 'primary'
      }
   },
   {
     responsiveVariants: ['sm', 'md', 'lg', 'xl'] // `true` to apply to all screen sizes
   }
   )
const buttonClass = useMemo(() => {
  return button({ ...normalizeResponsiveStyle(props.size, 'size'), ...normalizeResponsiveStyle(props.color, 'color') }, [props])
})

return (
  <button class={buttonClass}>
    <children />
  </button>
)
}

コンポーネントを使うときは冒頭の通り以下です。

<Button>click here</Button>
<Button size="lg" color="secondary">
  click here
</Button>

//生成されるのは以下
// <button class="font-medium rounded-full active:opacity-80 bg-blue-500 text-white md:px-4 md:py-3 md:text-lg text-sm">click here</button>
<Button size={{ initial: 'sm', md: 'lg' }}>
click here
</Button>

終わり

今回はreact風ですが他のフレームワークでも似たような感じで書くことができます。

Discussion