📘

shadcn/ui を park-ui に置き換える

に公開

tailwindのお勉強がてら作成していたサイトを置き換えました。tailwindはすごく小さなサイトをサクッと作るだけなら全然アリだと思いますが、主に下記の理由からこのまま書き続けると負担になりそうに思い、置き換えに踏み切りました。

  • class名がひたすら横長で可読性悪い
  • 擬似要素がある場合、区切りがぱっと見でわからない
  • 独自の短縮表記がcssと乖離していて学習コストが高い

置き換えにあたって課題となる点はHeadlessUIのライブラリが変わること、CSSフレームワークが変わることの2点です。

headless ui css
shadcn/ui radix-ui tailwind
park-ui ark-ui panda-css

Headless UI

Headless UIライブラリが変更になる部分については、どちらも似たようなコンポーネント群が提供されているため致命的に困ることはありませんでした。構造やプロパティが変わるだけなので脳死で移植作業を行うのみです。
コンポーネントの種類はshadcn/uiの方が若干豊富だと思います。一部のshadcnにしかないUIを使っていた場合はコンポーネントを自作するか諦めるかで葛藤しそうです。

CSS framework

こちらの方が改修が大変だと思っていましたが、tailwind=>panda-cssへの変換であれば意外と単純でした。どちらも単にclassNameに値を入れるだけで成立する構造のため、tailwindを普通のcssに置換しつつひたすら転記するだけですみました。

// tailwind
className='grid ...'

// panda-css
className={css({display:grid; ... })}

pandaは上記のようなclassNameを使う書き方以外にもChakra-UIのようにプロパティを使ってスタイルを当てることもできます。

<Stack gap="2" />

個人的にはこちらの書き方の方が好みなのですが、移植してみた経験から言うと、cssに関係するものは全部classNameに押し込めてしまった方がメンテナンス性は高いな、という印象です。というのも後述する複雑なセレクター指定などはプロパティに生えていません。なので、それらを利用する時はclassName、そうでないものはプロパティへと書き分けると段々とごちゃついてきます。いっそ割り切って全部classNameに書く方が迷いが少ないです。

tailwindと比較するとオブジェクト表記になるため縦に縦に長くなります。tailwindはクラス名が横に長くてstyleが分かりにくくなりますが、pandaの場合は縦に長くてコンポーネントの構造が見通しにくくなります。どちらも一長一短で醜くなることには変わりなく、あとは好みの問題と言う感じでした。個人的にはcss modulesの方がコンポーネントの見た目がすっきりして好きですが、クラス名を考えるのが面倒&ファイル分けると見るのが億劫になるので積極的に採用しない。

以下はpandaに変換するにあたって若干迷ったものを備忘録としてメモしておきます。

container queries

※tailwindでコンテナクエリを使うには@tailwindcss/container-queriesをインストールする必要があります。

before.ts
const Container = () => (
  <div className="@container">
    <div className="grid grid-cols-[repeat(1,minmax(350px,1fr))] @[700px]:grid-cols-[repeat(2,minmax(350px,2fr))]">
      <div></div>
    </div>
  </div>
);
after.ts
const Container = () => (
  <div
    className={css({
      containerType: 'inline-size',
    })}
  >
    <div
      className={css({
        display: 'grid',
        gridTemplateColumns: '[repeat(1, minmax(350px, 1fr))]',
        '@container (min-width: 700px)': {
          gridTemplateColumns: '[repeat(2, minmax(350px, 2fr))]',
        },
      })}
    >
      <div></div>
    </div>
  </div>
);

クラス名のマージ

※tailwindはtailwind-mergeおよびclsxを利用します。

before.ts
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';

// utility関数を作成
export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

// こんな感じで使う
export const Sample = forwardRef<HTMLDivElement, SampleProps>(
  ({ className, ...props }, ref) => {
    return (
        <div
            className={cn('grid gap-2', className)}
            ref={ref}
            {...props}
        />
    );
  }
);
Sample.displayName = 'Sample';

panda-cssの場合、css関数がマージを担当します。
cx関数というのもありますが、こちらは単にクラス名を結合してくれるだけであり、重複除去等は行いません。

after.ts
// cx関数
import { css, cx } from 'styled-system/css'
 
const styles = css({borderWidth: '1px', borderRadius: '8px'})
 
const Card = ({ className, ...props }) => {
  const rootClassName = cx('group', styles, className)
  return <div className={rootClassName} {...props} />
}

// css関数
import { css } from 'styled-system/css';
import { SystemStyleObject } from 'styled-system/types';

export const Button = ({ css: cssProp = {} }: { css: SystemStyleObject }) => {
  const className = css(
    { display: 'flex', alignItems: 'center', color: 'black' },
    cssProp
  );
  return <button className={className}></button>;
};

複雑なselector

1例としてnotセレクターの場合

before.ts
className='[&:not(:first-child)]:mt-6'
after.ts
className={css({ '&:not(:first-child)': { marginTop: '[1em]' } })}

Discussion