5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

React、TypeScript、CVA、TailwindCSSを使ってポリモーフィックコンポーネントなボタンを作成する

Last updated at Posted at 2024-03-17

Webアプリケーションでは、スタイルが変わらずに異なる振る舞いを持つコンポーネントがよく見られます。

例えば、ボタンには基本的に<button>要素を利用してonClickを使用しますが、<a>要素としてhrefも持つことがあります。

その振る舞いがどうあれ、ボタンコンポーネントはデザインシステムのスタイルを保持すべきです。ここで「ポリモーフィックコンポーネント」と呼ばれるものが登場します。

この記事では、同じスタイルを保ちつつ振る舞いを変えることができるポリモーフィックボタンの作成方法について解説します。

polymorphic-component.jpeg

目的

目的は単純で、html要素を変更できるボタンコンポーネントを持つことです。

例えば、ボタンは<a>リンクや<button>ボタンのように振る舞うことができ、したがってそのルート要素の属性を持つことができます。

home.tsx
import React from 'react'
import { Button } from '@/ui/button'

export const Home = () => {
  return (
    <>
      <Button as={'a'} href={'/test'}>
        内部 リンク
      </Button>
      <Button
        as={'a'}
        href={'https://www.example.com'}
        rel="noreferrer"
        target="_blank"
      >
        外部 リンク
      </Button>
      <Button>ボタン</Button>
    </>
  )
}

似たようなスタイルのコンポーネントがありますが、DOMを調べてみると、ボタンコンポーネントのルートにある要素はまったく異なっています。

内部リンク・外部リンク・ボタン.png

利点

ポリモーフィックコンポーネントを作成することには3つの利点があります。

  • HTML構造の一貫性: <button><a>要素が互いに入れ子にならないので、クローラーはWebアプリケーションの構造を正しく解釈します
  • 柔軟性: <button><a>要素に加えて、コンポーネントは、例えばrole="button"を持つdivのような、どんな要素でも取ることができます
  • 堅牢性: Typescriptのおかげで、入力された要素に応じて、IDE(例: VSCode)は期待される属性を正しく解釈します(<a>にはhrefがありますが、<button>にはないため異常性を検知してくれます)

1. ボタンコンポーネントの作成

最初のステップは、下記のように作成することです。

button.tsx
type Props = React.ButtonHTMLAttributes<HTMLButtonElement>

export const Button = ({ ...props }: Props) => {
  return <button {...props} />
}

ここで、ボタンコンポーネントは、HTML属性のPropsに似た属性を期待します。
これは動的ではなく、ルート要素は<button>でなければなりません。

2. ボタンのスタイルを追加

次のステップは、デザインシステムに合わせてボタンにスタイルを追加することです。

TailwindCSStailwind-mergeで、Class Variance Authority (CVA)を使用します。
デザインシステムとClass Variance Authority (CVA)については、下記の参考文献を参照してください。

参考文献

button.tsx
import React from 'react'
import { twMerge } from 'tailwind-merge'
import { cva, type VariantProps } from 'class-variance-authority'

// スタイルを追加
const button = cva(['font-medium', 'py-2.5', 'px-3.5', 'rounded-md'], {
  variants: {
    intent: {
      primary: ['bg-blue-500', 'text-white'],
      secondary: ['bg-black', 'text-white'],
      ghost: ['text-blue-500'],
    },
  },
  defaultVariants: {
    intent: 'primary',
  },
})

type Props = React.ButtonHTMLAttributes<HTMLButtonElement> &
  VariantProps<typeof button>

export const Button = ({ className, intent, ...props }: Props) => (
  <button className={twMerge(button({ className, intent }))} {...props} />
)

これで、ボタンコンポーネントはPropsでインテントを受け取ることができるようになりました。あとはボタンのポリモーフィックな側面を管理するだけです。

3. as属性を変更して要素をパラメータとして受け取る

ここで、コンポーネントのルートで使用したい要素を指定できるようにする必要があります。

慣例では、as={要素(エレメント)}属性を使用します。
したがって、特定のHTML要素にデフォルトで存在するasを削除し、Elementタイプを待つ必要があります。

button.tsx
import React from 'react'
import { twMerge } from 'tailwind-merge'
import { cva, type VariantProps } from 'class-variance-authority'

// スタイルを追加
const button = cva(['font-medium', 'py-2.5', 'px-3.5', 'rounded-md'], {
  variants: {
    intent: {
      primary: ['bg-blue-500', 'text-white'],
      secondary: ['bg-black', 'text-white'],
      ghost: ['text-blue-500'],
    },
  },
  defaultVariants: {
    intent: 'primary',
  },
})

// デフォルトを削除
type Props = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'as'> & {
  as?: Element
} & VariantProps<typeof button>

export const Button = ({ className, intent, ...props }: Props) => (
  <button className={twMerge(button({ className, intent }))} {...props} />
)

4. 動的型の使用

最後に、ルート要素を動的に変更し、各要素に期待される型を高度に解釈することです。

デフォルトでは、私たちのコンポーネントはボタンなので、コンポーネントがインスタンス化されるときに asが指定されなければ、この要素を保持します。

button.tsx
import React from 'react'
import { twMerge } from 'tailwind-merge'
import { cva, type VariantProps } from 'class-variance-authority'

// デフォルトを削除することにより、要素に応じて動的に型を取得
type PolymorphicProps<Element extends React.ElementType, Props> = Props &
  Omit<React.ComponentProps<Element>, 'as'> & {
    as?: Element
  }

// スタイルを追加
const button = cva(['font-medium', 'py-2.5', 'px-3.5', 'rounded-md'], {
  variants: {
    intent: {
      primary: ['bg-blue-500', 'text-white'],
      secondary: ['bg-black', 'text-white'],
      ghost: ['text-blue-500'],
    },
  },
  defaultVariants: {
    intent: 'primary',
  },
})

// バリアント...異なるデータ型を単一の型として扱うことができるデータ構造
// Class Variance Authority (CVA)を利用してバリアントを取得する方法
type Props = VariantProps<typeof button>

// asが指定されていない場合のデフォルト要素を定義
const defaultElement = 'button'

// 適切に型付けされ、デザインされたButtonコンポーネント
export const Button = <
  Element extends React.ElementType = typeof defaultElement
>(
  props: PolymorphicProps<Element, Props>
) => {
  const { as: Component = defaultElement, className, intent, ...rest } = props
  return (
    <Component className={twMerge(button({ className, intent }))} {...rest} />
  )
}

まとめ

TailwindCSSを使用して、React(Typescript)でポリモーフィックコンポーネントなボタンを作成することは、スタイルの一貫性とHTML要素の選択の柔軟性を維持するための、綺麗な解決を提供します。

このアプローチにより、HTML構造はクリーンでクローラーに理解しやすいままとなり、アクセシビリティと UI/UXが向上します。

さらに、Typescriptを使用することで、ReactTypescriptを使用する多くの利点の恩恵を受けることができます。

ポリモーフィックなボタンとClass Variance Authority (CVA)によるスタイリングアプローチの組み合わせにより、保守性や再利用性が高く、柔軟で、拡張性が高いコンポーネントといえます。

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?