🌨

CSSからComponentを生成するMistcss触ってみた

2024/03/25に公開

はじめに

CSS から React Component を生成できる Mistcss というのがあったので触ってみた
CSS がかければ Component を作成できるため学習コストは低く、簡単に使用することができる
この記事では一通り環境構築の手順と、自分が使ってみた感想や気づきを記載する

環境構築

ここでは環境構築手順を記載する
ドキュメント見てやるよって方は飛ばして下さい
下記では少し公式ドキュメントとはフォルダ構成など変更して使ってます
react の環境構築は完了している前提で進めていきます

インストール

下記コマンドで Mistcss をインストール

npm install --save-dev mistcss

コンポーネント作成

Button Component を作成します
tailwind も使用できますが一旦割愛
※@apply でつらつらと反映させたい css を並べるだけなのでそんな変わりありません

Button.mist.css
@scope (.Button) {
  button:scope {
    /* Default style */
    font-size: 1rem;
    border-radius: 0.25rem;

    &[data-size='lg'] {
      font-size: 1.5rem;
    }

    &[data-size='sm'] {
      font-size: 0.75rem;
    }

    &[data-danger] {
      background-color: red;
      color: white;
    }
  }
}

data-xxx の xxx が props で渡せるプロパティになる

下記コマンドを実行

npx mistcss ./src/components

components 配下に Button.mist.css.tsx が生成されれば OK
こんな感じのファイルが勝手に生成される
Button.mist.css で data-で始めたプロパティが props で渡せるようになっている
この type は自動生成されるため、型の安全性が高い

Button.mist.css.tsx
// Generated by MistCSS, do not modify
import './Button.mist.css';

type ButtonProps = {
  children?: React.ReactNode;
  size?: 'lg' | 'sm';
  danger?: boolean;
} & JSX.IntrinsicElements['button'];

export function Button({ children, size, danger, ...props }: ButtonProps) {
  return (
    <button {...props} className='Button' data-size={size} data-danger={danger}>
      {children}
    </button>
  );
}

App.tsx で読み込む

App.tsx
import { Button } from './components/Button.mist.css.tsx';

export default function App() {
  return (
    <main>
      {/* Use it like a normal React component */}
      <Button size='lg'>Submit</Button>
      <Button size='sm' danger>
        Delete
      </Button>
    </main>
  );
}
tsconfig.json
 "compilerOptions": {
    /* Bundler mode */
    "allowJs": true,
    "allowImportingTsExtensions": true,
 }

ここまでしてファイルを実行すると下記のような表示がされたら成功 🙆‍♂️
css が反映されてない方は Button.mist.css.tsx の className と Button.mist.css に書いてある css 名が同じか確認してみて下さい

以降は触った感想や気づきを記載していく

どんなコンポーネントが作れるか?

自動生成されたファイルを見ると型が下記のようになっている

type ButtonProps = {
  children?: React.ReactNode;
  size?: 'lg' | 'sm';
  danger?: boolean;
} & JSX.IntrinsicElements['button'];

なので JSX.IntrinsicElements 型の要素が対象であることがわかる
他には a,input,header,footer,dialog あたりは使うことがありそう

ちなみにこんな感じで参照してファイルは生成される
fuga の値が JSX.IntrinsicElements の値に当てはまらない場合、型エラーになる

hoge.mist.css
@scope (.hoge) {
  fuga:scope {
    ...;
  }
}
hoge.mist.css.tsx
// Generated by MistCSS, do not modify
import './hoge.mist.css'

type HogeProps = {
  children?: React.ReactNode
  size?: 'lg' | 'sm'
  danger?: boolean
} & JSX.IntrinsicElements['fuga']

export function Hoge({ children, size, danger, ...props }: HogeProps) {
  return (
    <fuga {...props} className="Hoge" data-size={size} data-danger={danger}>
      {children}
    </fuga>
  )
}

論理演算子を使用する

論理演算子を使用して適用する css を操作できる

/* props.foo === 'x' && props.bar === 'y' */
&[data-foo='x']&[data-bar='y'] {
  /* ... */
}

/* props.foo === 'x' || props.bar === 'y' */
&[data-foo='x'],
&[data-bar='y'] {
  /* ... */
}

以下では red と border が true の一番上の input だけ枠線が赤くなっているのが確認できる

Input.mist.css
@scope (.Input) {
  input:scope {
    /* Default style */
    font-size: 1rem;
    border-radius: 0.25rem;
    height: 2rem;
    &[data-size='lg'] {
      font-size: 1.5rem;
    }

    &[data-size='md'] {
      font-size: 1.25rem;
    }

    &[data-size='sm'] {
      font-size: 0.75rem;
    }

    &[data-red]&[data-border] {
      border: 2px solid red;
    }
  }
}
App.tsx
import { Input } from './components/Input.mist.css.tsx';

export default function App() {
  return (
    <main>
      <p>red & border</p>
      <Input size='lg' red border />
      <p>red</p>
      <Input size='md' red />
      <p>border</p>
      <Input size='sm' border />
    </main>
  );
}

組み合わせて使用する

コンポーネントを組み合わせて使用できる
下記ではボタンを押したらダイアログが閉じる

import { Dialog } from './components/Dialog.mist.css.tsx';
import { Button } from './components/Button.mist.css.tsx';
import { useState } from 'react';

export default function App() {
  return (
    <main>
      <DialogComponent />
    </main>
  );
}

export function DialogComponent() {
  const [showDialog, setShowDialog] = useState(true);

  return (
    <div>
      {showDialog && (
        <Dialog open>
          メッセージメッセージメッセージメッセージ
          <Button onClick={() => setShowDialog(false)} dialog>
            OK
          </Button>
        </Dialog>
      )}
    </div>
  );
}

いいなと感じた点

適用するスタイルが フラグ 1 つで切り替えられるため、css が煩雑にならない
下記のように大きさも、使い方もフラグ 1 つで切り替えられることで特定の場合にネストする必要もないし管理が楽にできる

@scope (.Button) {
  button:scope {
    /* Default style */
    font-size: 1rem;
    border-radius: 0.25rem;
    height: 3rem;

    &[data-size='lg'] {
      font-size: 1.5rem;
    }

    &[data-size='sm'] {
      font-size: 0.75rem;
    }

    &[data-danger] {
      background-color: red;
      color: white;
    }

    &[data-dialog] {
      width: 100%;
      color: white;
      margin-top: auto;
      font-size: 1.25rem;
      background: #000;
      margin-top: auto;
    }
  }
}

tsx にも className を直接書く必要がないのでスッキリして見える

import { Input } from './components/Input.mist.css.tsx';

export default function App() {
  return (
    <main>
      <p>red & border</p>
      <Input size='lg' red border />
      <p>red</p>
      <Input size='md' red />
      <p>border</p>
      <Input size='sm' border />
    </main>
  );
}

また、data-xxx を見ればプロジェクトの中でどんなシーンで使うか予測できるのも良い
例えば Button.mist.css の data-xxx をシーンごとにフラグ名を決めて管理すれば使う人もフラグ名を見ればどんなシーンがあるか予測できる

まとめ

mistcss は css から react コンポーネントを作成できるツール
css ができれば使えるので学習コストは低そう
作成できるコンポーネントは atoms 層のコンポーネント
ただ、roadmap を見る限り v1.0 で破壊的な変更がありそうなので一旦こんなのもあるんだなーくらいに留めておいて良さそう
また、別のフレームワークでも使えるようになるかもなので今後が期待できるツール 🚀

Discussion