🌊

同じデザインを複数のHTMLタグに適用する

2025/01/06に公開

Webアプリなどを業務で開発する際に、UIのデザインをFigmaなどで作成し(Design System)、
それを元に実装することはよくあるかと思います。

本記事は、上記の場合に見た目は同じだけどHTMLタグは別にしたいユースケースに遭遇した場合に、
対応した内容を時系列で解説します。

実装全体は以下のPRです。
https://github.com/ki504178/nextjs_ts_codebase/pull/21

前提

Design Systemで定義されていたものとして、以下のようなパターンにより表示を切り替えられるボタンの共通コンポーネントでした。

初期

初期の想定では、button, aタグで利用される形のみだったため、以下のような構成で実装していました。(詳細はこちら

button
L button.tsx ← buttonタグのComponent
L link-button.tsx ← aタグのComponent
L buttonCommon.tsx ← 共通のデザインを適用するための共通処理

懸念点

  • Design Systemとしては1つのコンポーネントとして定義されてるが、実装ではタグごとに分かれてしまっている
  • 利用側で、タグによりコンポーネントを使い分けないといけないということを認知する必要がある
  • button, aタグ以外で同じデザインを利用したくなった場合にコンポーネントが増えてしまう

初期の懸念を解消

開発を進める中で、初期の懸念点の3点目に対応する必要が出てきたことと、
それ以外の懸念も含めて、利用側で任意にタグを指定できるようコンポーネントを改善しました。(詳細はこちら

button.tsx
import {
  type ComponentPropsWithoutRef,
  type ElementType,
  type ForwardedRef,
  forwardRef,
} from "react";
import getButtonClassName, { type ButtonColor } from "./buttonCommon";

type ThemeProps = {
  color?: ButtonColor;
};

type DistributiveOmit<T, K extends keyof T> = T extends unknown
  ? Omit<T, K>
  : never;

type As<T extends ElementType = ElementType> = { tag?: T };

type PolymorphicProps<E extends ElementType, P> = As<E> &
  P &
  DistributiveOmit<ComponentPropsWithoutRef<E>, keyof P | "tag">;

type Props<E extends ElementType = "button"> = PolymorphicProps<E, ThemeProps>;

export const Button = forwardRef(function button<
  E extends ElementType = "button",
>(
  {
    tag,
    color = "primary",
    children,
    disabled,
    ...props
  }: Props<E>,
  ref: ForwardedRef<ComponentPropsWithoutRef<E>["ref"]>,
) {
  const Tag = tag || "button";

  return (
    <Tag
      ref={ref}
      {...((!tag || tag === "button") && { type: "button", disabled })}
      {...props}
      className={getButtonClassName(color)}
    >
      {children}
    </Tag>
  );
});

問題点

  • そもそもここまで辿り着くのにだいぶ時間がかかった
  • そして時間をかけた結果、コードをパッと見て分かる通り、すぐに理解可能ではない複雑な型パズルで懸念点を解消した
    • その結果、将来的な保守性が重くなり、認知負荷も高くなった

完成形

そもそもの懸念点を見直してみると、コンポーネントで提供する必要がないと考えた。

その結果、Buttonコンポーネントが提供するのはデザインを適用したCSSを公開する形にし、問題点を解消する方針とした。(詳細はこちら

メリット

  • 問題点が解消されシンプルになる
  • Buttonデザインを一部上書きしたスタイリングもやりやすい

デメリット

  • コンポーネントで提供していた時にできていた、buttonタグのデフォルトtype="button"の指定ができなくなった
    • この指定は、formタグの中で利用していた場合などに、予期せずsubmit扱いとならないように制御していた。
    • Biomeを利用していたため、このルールによりLintでチェックできるようにし、同等の状態を担保できるようにした

まとめ

完成形を見て分かる通り色々考えた結果、本質的な解決方法はシンプルに収束するものなのかなと思いました。

この記事ではCSS Modulesをベースにしていますが、
(個人的に苦手な)Tailwind CSSでも@applyを利用することで同じことが実現できるかと思います。

Discussion