Open7

CSSを持たない Headless な UI ライブラリと ChatGPT によるマークアップ生成を試してみる

mizchimizchi

いまさら気づいたけど Headless 系のUIライブラリが一番 AI と相性いいのではないか。

ロジックはプログラマで書けて自由度高いし、コンポーネントのネスト構造から意図を読み取れるだろうし、class 名は自由に書けるから意図を表明しやすい。 それをプロンプトとして ChatGPT or Codex にそのまま投げて書かせる、ができる。

というわけで vite + react + radix-ui + vanilla-extract で実験してみた。

プロンプト

あなたは凄腕のマークアップエンジニアです。

radix-ui は Headless UI ライブラリで、UIとしてのセマンティクスのみを持っています。

次のコードは React + radix-ui + vanilla-extract で書かれた React コンポーネントです。

// Popover.tsx
import * as Popover from '@radix-ui/react-popover';
import {red} from "./App.css";

export const Popover = () => (
  <Popover.Root>
    <Popover.Trigger>More info</Popover.Trigger>
    <Popover.Portal>
      <Popover.Content className={red}>
        Some more info…
        <Popover.Arrow />
      </Popover.Content>
    </Popover.Portal>
  </Popover.Root>
);

この ReactComponent に対して、vanilla-extract による Popover.css.tsx でいい感じのスタイルを当ててください。

以下は出力の例です。

import { style } from '@vanilla-extract/css';


export const Trigger = style({
  
});

export const Content = style({
  
});

export const Arrow = style({
  
});
mizchimizchi

Answer

はい、了解しました。vanilla-extractによるスタイルを適用するためのCSSモジュールを作成します。このCSSモジュールは、PopoverのTrigger、Content、およびArrowの各要素に対してスタイルを定義します。

この例では、一般的なスタイルを適用しますが、具体的なデザインの要件に基づいてこれらのスタイルをカスタマイズすることができます。

import { style } from '@vanilla-extract/css';

export const Trigger = style({
  backgroundColor: '#ddd',
  padding: '10px',
  border: 'none',
  borderRadius: '4px',
  cursor: 'pointer',
  ':hover': {
    backgroundColor: '#ccc',
  }
});

export const Content = style({
  backgroundColor: '#fff',
  border: '1px solid #ddd',
  borderRadius: '4px',
  padding: '20px',
  boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
});

export const Arrow = style({
  color: '#ddd',
});
mizchimizchi

もうちょっと主張を強くできませんか。Apple 風だと嬉しいです。
出力するコードは Popover.css.tsx のみで十分です。

Answer

もちろんです。Appleのデザイン感覚を取り入れ、以下にPopover.css.tsxのスタイルを更新しました。グラデーション、ラウンドコーナー、明るい色彩など、Apple風の主張の強いデザインを意識しました。

出力

mizchimizchi

感想

簡単な例しか試していないが、十分使いものになる気がする。Headless UI によるコンポーネント配置が、ある種のプロンプトとして機能しているように見える。

前に素のHTMLに class だけ当てた状態で、そのCSS を宣言させてみたことがあったが、それよりも精度が高い。今度はもうちょっと複雑な例も試してみようと思う。

おそらくテーミングも機能するだろう。プロンプトとして与えるとそれを汲んでくれるはず。

ただ、ほしい結果に誘導するには自分自身のデザインに対する語彙が足りない気がした。もう少し〜風の語彙を集めておく必要がありそう。

mizchimizchi

もう一例

次はForm要素です。これに対して同じくApple風で Form.css.tsx を生成してください。

import * as Form from '@radix-ui/react-form';
import {FormRoot, FormField, FormLabel, FormMessage, Input, Textarea, Button } from "./Form.css"

const MyForm = () => (
  <Form.Root className={FormRoot}>
    <Form.Field className={FormField} name="email">
      <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between' }}>
        <Form.Label className={FormLabel}>Email</Form.Label>
        <Form.Message className={FormMessage} match="valueMissing">
          Please enter your email
        </Form.Message>
        <Form.Message className={FormMessage} match="typeMismatch">
          Please provide a valid email
        </Form.Message>
      </div>
      <Form.Control asChild>
        <input className={Input} type="email" required />
      </Form.Control>
    </Form.Field>
    <Form.Field className={FormField} name="question">
      <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between' }}>
        <Form.Label className={FormLabel}>Question</Form.Label>
        <Form.Message className={FormMessage} match="valueMissing">
          Please enter a question
        </Form.Message>
      </div>
      <Form.Control asChild>
        <textarea className={Textarea} required />
      </Form.Control>
    </Form.Field>
    <Form.Submit asChild>
      <button className={Button} style={{ marginTop: 10 }}>
        Post question
      </button>
    </Form.Submit>
  </Form.Root>
);

export default MyForm;

ChatGPT

Apple風のデザインでForm要素をスタイリングするためのForm.css.tsxを以下に作成します。この例では、Appleのユーザーインターフェースで一般的に見られる、クリーンで明るいスタイルを適用しています。

// Form.css.tsx
import { style } from '@vanilla-extract/css';

export const FormRoot = style({
  display: 'flex',
  flexDirection: 'column',
  fontFamily: '-apple-system, BlinkMacSystemFont, sans-serif',
});

export const FormField = style({
  marginBottom: '20px',
});

export const FormLabel = style({
  fontWeight: 'bold',
  marginBottom: '5px',
});

export const FormMessage = style({
  color: '#ff3b30',
  fontSize: '12px',
  marginBottom: '5px',
});

export const Input = style({
  padding: '10px',
  borderRadius: '8px',
  border: '1px solid #e0e0e3',
  ':focus': {
    outline: 'none',
    borderColor: '#007aff',
  }
});

export const Textarea = style({
  padding: '10px',
  borderRadius: '8px',
  border: '1px solid #e0e0e3',
  resize: 'none',
  ':focus': {
    outline: 'none',
    borderColor: '#007aff',
  }
});

export const Button = style({
  backgroundColor: '#007aff',
  color: '#fff',
  padding: '10px',
  borderRadius: '8px',
  border: 'none',
  cursor: 'pointer',
  ':hover': {
    backgroundColor: '#0051a8',
  },
  ':focus': {
    outline: 'none',
  },
});

上記のスタイルは、inputフィールドやボタンに対してApple風のデザインを適用します。また、フォームメッセージは赤色で表示され、入力が必要なフィールドに対する注意を引きます。これらのスタイルは自由に調整やカスタマイズが可能です。

出力

mizchimizchi

Input を Button と同じく画面幅に対して引き延ばせませんか

ChatGPT

もちろんです。InputとTextareaのスタイルにwidth: '100%'を追加することで、それらを画面の幅に合わせて伸ばすことができます。

以下に更新されたForm.css.tsxを示します。

// Form.css.tsx
import { style } from '@vanilla-extract/css';

export const FormRoot = style({
  display: 'flex',
  flexDirection: 'column',
  fontFamily: '-apple-system, BlinkMacSystemFont, sans-serif',
});

export const FormField = style({
  marginBottom: '20px',
});

export const FormLabel = style({
  fontWeight: 'bold',
  marginBottom: '5px',
});

export const FormMessage = style({
  color: '#ff3b30',
  fontSize: '12px',
  marginBottom: '5px',
});

export const Input = style({
  padding: '10px',
  borderRadius: '8px',
  border: '1px solid #e0e0e3',
  width: '100%',
  ':focus': {
    outline: 'none',
    borderColor: '#007aff',
  }
});

export const Textarea = style({
  padding: '10px',
  borderRadius: '8px',
  border: '1px solid #e0e0e3',
  resize: 'none',
  width: '100%',
  ':focus': {
    outline: 'none',
    borderColor: '#007aff',
  }
});

export const Button = style({
  backgroundColor: '#007aff',
  color: '#fff',
  padding: '10px',
  borderRadius: '8px',
  border: 'none',
  cursor: 'pointer',
  ':hover': {
    backgroundColor: '#0051a8',
  },
  ':focus': {
    outline: 'none',
  },
});

出力

注釈: 惜しい。padding を考慮しないから突き抜けてしまっている。