こんにちは。ビデオリサーチのキクチです。
今回はタイトルの通り、とある低予算プロジェクトで手探りながらAI駆動UI開発を導入してみた話を書いてみたいと思います。
はじめに
昨年末にとある Web アプリ開発プロジェクトにアサインされました。
しかし、プロジェクト予算の都合上、専任のデザイナーやフロントエンドエンジニアを配置できない状況でした。
手元にあるのはパワーポイントで作成された画面スケッチのみ。
図形とテキストだけで構成されたざっくりしたスケッチは数多くありましたが、そこからデザインを起こすには手間がかかる上、改修が入るたびに修正するのは非常に非効率でした。
そこで思い切って、AI デザインツールの「tldraw.makereal」を導入してみることにしました。
なぜ tldraw.makereal を選定したのか
導入にあたって特に重視したポイントは以下のとおりです。
- 軽量・シンプル:チームメンバー全員がサクッと使えること
- ローカルで完結:セキュリティ要件上、セルフホスト出来ることが望ましい
- コードとの連動が容易:Tailwind CSSと相性が良いこと
ローカルで実行可能
最も大きかったのは「ローカル環境で動かせる」ことです。
ランサムウェア等のセキュリティインシデントが連日報道される昨今では、企業において外部SaaSやクラウドツールの導入に際し、事前審査や承認プロセスが必要になることが一般的かと思います。その点、tldraw.makerealはローカルでのセットアップおよびセルフホストが可能なので、複雑な承認プロセスを簡略化でき、より迅速にプロジェクトへの導入を進められる点が魅力でした。
Tailwind CSS でデザインされるため実装移行がスムーズ
tldraw.makerealで生成される基本デザインはTailwind CSSで組まれていて、実装に流用しやすいクラス名やコンポーネント構造になっています。
パワーポイントのスケッチしかない状態から、「それっぽいUIプロトタイプ」を tldraw.makereal で作ってしまえば、エンジニアはそこから Tailwind CSSベースの実装に進むことができるため、開発工数の大幅な短縮になります。
さらに余計なデザイン崩れが起こりにくいので、後々の修正コストも抑えられました。
簡単なインタラクションの実装も可能
単純な画面デザインだけでなく、フォームやボタンを押したら別画面へ遷移する、といった軽めのインタラクションが実装可能です。
実際に動くプロトタイプとしてクライアントに見せられるので、「イメージがわきやすい」「要件の抜け漏れを減らせる」のは大きい利点でした。
その他のメリット
学習コストが低い:分かりやすいUIでフロントエンジニアやデザイナーだけでなく、マネージャーやディレクターも直感的に使える。
ローカル環境構築のステップ
ここからは、実際にローカル環境で tldraw.makereal を動かす手順を紹介します。
- tldraw.makereal のリポジトリをローカルにクローンします。
- ※READMEではローカルで動作させる場合、starter repoを推奨していますが、メンテナンスされていないのか、そのままでは動作しないようです。
必要なパッケージのインストール
npm install
で依存パッケージをインストールします。
サーバーの起動
npm run dev
でローカルサーバーを立ち上げれば、ブラウザで http://localhost:3000 にアクセスしてツールを利用できます。
起動直後に以下のように表示されるので、APIキーをセットして保存。モデルはgpt-4oを利用しました。

postgres周りの設定
このままでもデザイン生成自体は可能ですが、ローカル環境でホストする場合は生成したデザインの保存が行えません。
デフォルトでは生成したデザインの保存にNeon Serverless Postgresの使用を想定しているようですが、これをローカルのpostgresに向けるようソースを少し修正する必要があります。
今回は例としてローカルにdockerでpostgresを構築し、向き先を変更する手順にしてみます。
ローカル用postgresの構築
docker run --name my-postgres -e POSTGRES_USER=myuser -e POSTGRES_PASSWORD=mypass -e POSTGRES_DB=mydb -p 5432:5432 -d postgres
echo "CREATE TABLE links (id SERIAL PRIMARY KEY, shape_id VARCHAR(255) UNIQUE NOT NULL, html TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);" > schema.sql
docker exec -i my-postgres psql -U myuser -d mydb < schema.sql
envの作成
echo "POSTGRES_URL=postgres://myuser:mypass@localhost:5432/mydb?sslmode=disable" > .env.local
pgドライバーのインストール
npm install pg
ソースの修正
以下の2ファイルを次のように修正し、neonではなくpgを利用するよう変更します。
- make-real/app/makereal.tldraw.link/[linkId]/page.tsx
import { Pool } from 'pg'
import { notFound } from 'next/navigation'
import { LinkComponent } from '../../components/LinkComponent'
export const dynamic = 'force-dynamic'
const pool = new Pool({
connectionString: process.env.POSTGRES_URL,
})
export default async function LinkPage({
params,
searchParams,
}: {
params: { linkId: string }
searchParams: { preview?: string }
}) {
const { linkId } = params
const isPreview = !!searchParams.preview
const result = await pool.query('SELECT html FROM links WHERE shape_id = $1', [linkId])
if (result.rows.length !== 1) notFound()
let html: string = result.rows[0].html
const SCRIPT_TO_INJECT_FOR_PREVIEW = `
// send the screenshot to the parent window
window.addEventListener('message', function(event) {
if (event.data.action === 'take-screenshot' && event.data.shapeid === "shape:${linkId}") {
html2canvas(document.body, {useCors: true, foreignObjectRendering: true, allowTaint: true}).then(function(canvas) {
const data = canvas.toDataURL('image/png');
window.parent.parent.postMessage({screenshot: data, shapeid: "shape:${linkId}"}, "*");
});
}
}, false);
// and prevent the user from pinch-zooming into the iframe
document.body.addEventListener('wheel', e => {
if (!e.ctrlKey) return;
e.preventDefault();
}, { passive: false })
`
if (isPreview) {
html = html.includes('</body>')
? html.replace(
'</body>',
`<script src="https://unpkg.com/html2canvas"></script><script>${SCRIPT_TO_INJECT_FOR_PREVIEW}</script></body>`
)
: html + `<script>${SCRIPT_TO_INJECT_FOR_PREVIEW}</script>`
}
return <LinkComponent linkId={linkId} isPreview={isPreview} html={html} />
}
- make-real/app/lib/uploadLink.tsx
'use server'
import { Pool } from 'pg'
const pool = new Pool({
connectionString: process.env.POSTGRES_URL,
})
export async function uploadLink(shapeId: string, html: string) {
if (typeof shapeId !== 'string' || !shapeId.startsWith('shape:')) {
throw new Error('shapeId must be a string starting with shape:')
}
if (typeof html !== 'string') {
throw new Error('html must be a string')
}
shapeId = shapeId.replace(/^shape:/, '')
const client = await pool.connect()
try {
await client.query(
'INSERT INTO links (shape_id, html) VALUES ($1, $2)',
[shapeId, html]
)
} catch (error) {
console.error('Error in uploadLink:', error)
throw error
} finally {
client.release()
}
}
問題なくコンパイルが成功すれば、以下のように出力されるはずです。
% npm run dev
> draw-a-ui@0.1.0 dev
> next dev
▲ Next.js 14.0.1
- Local: http://localhost:3000
- Environments: .env.local
⚠ Invalid next.config.js options detected:
⚠ Unrecognized key(s) in object: 'functions'
⚠ See more info here: https://nextjs.org/docs/messages/invalid-next-config
✓ Ready in 1626ms
✓ Compiled /middleware in 128ms (55 modules)
○ Compiling /makereal.tldraw.com/page ...
✓ Compiled /makereal.tldraw.com/page in 3.1s (2305 modules)
✓ Compiled in 1214ms (1182 modules)
○ Compiling /makereal.tldraw.link/[linkId]/page ...
✓ Compiled /makereal.tldraw.link/[linkId]/page in 1328ms (2309 modules)
✓ Compiled /makereal.tldraw.com/api/openai/route in 425ms (1198 modules)
✓ Compiled in 938ms (2323 modules)
使用出来るAIプロバイダとモデルについて
現状では、OpenAIとAnthropicに対応しており、使用出来るモデルはそれぞれ、
OpenAI
- gpt-4o
- gpt-4o-mini
- gpt-4-turbo
Anthropic
- claude-3-7-sonnet-20250219
- claude-3-7-sonnet-20250219 (thinking)
- claude-3-5-sonnet-20241022
- claude-3-5-sonnet-20240620
- claude-3-opus-20240229
- claude-3-sonnet-20240229
- claude-3-haiku-20240307
のようです。
ソースを見ると、app/lib/settings.tsx
で使用するモデルが定義されており、ここを修正すればgpt-4.5やo1、o3-mini-high等にも変更できそうでしたが、内部で使用しているVercel AI SDKが未対応のため呼び出せないようでした。
デザインの生成
今回はサンプルとして、swaggerのAPIリファレンス画面のスクリーンショットを元にデザインを生成してみます。
画面上にスクリーンショットをドラッグアンドドロップし、右上の Make Real
ボタンを押すとウニョウニョとデザインの生成が始まります。
生成されたデザイン
色味や細かいコンポーネントの配置などは調整の余地がありますが「それっぽい」デザインが出力されました。
一撃でこのレベルのデザインを生成してくれるのであれば、プロトタイプ作成の取っ掛かりとして十分では無いでしょうか。

<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Swagger Petstore - OpenAPI 3.0</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap"
rel="stylesheet"
/>
<style>
body {
font-family: 'Inter', sans-serif;
background-color: #f3f4f6;
}
::scrollbar {
width: 8px;
height: 8px;
}
::scrollbar-track {
background: #f3f4f6;
}
::scrollbar-thumb {
background-color: #cbd5e1;
border-radius: 4px;
border: 2px solid #f3f4f6;
}
button:hover,
select:hover {
filter: brightness(95%);
}
pre {
background: #1e293b;
color: #cbd5e1;
padding: 1rem;
border-radius: 0.25rem;
overflow-x: auto;
font-size: 0.875rem;
line-height: 1.25rem;
}
.keyword {
color: #93c5fd;
}
.string {
color: #fbbf24;
}
.number {
color: #f87171;
}
</style>
</head>
<body class="min-h-screen flex flex-col">
<header class="bg-white shadow-sm">
<div class="max-w-7xl mx-auto px-4 py-4 flex items-center justify-between">
<div class="flex items-center space-x-2">
<svg
class="w-8 h-8 text-blue-500"
fill="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm0 3a7 7 0 0 1 7 7H5a7 7 0 0 1 7-7zm0 14a7 7 0 0 1-7-7h14a7 7 0 0 1-7 7z"
/>
</svg>
<span class="text-xl font-bold text-gray-700">
Swagger Petstore
</span>
<span class="text-sm text-gray-500 italic">
- OpenAPI 3.0 (1.0.12 OAS 3.0)
</span>
</div>
<div class="flex items-center space-x-2">
<div class="flex items-center space-x-1">
<label for="servers" class="text-sm text-gray-500">Servers:</label>
<select
id="servers"
class="border border-gray-300 text-sm rounded px-2 py-1 focus:outline-none"
>
<option value="https://petstore3.swagger.io/api/v3">
https://petstore3.swagger.io/api/v3
</option>
<option value="https://another.server.example.com">
https://another.server.example.com
</option>
</select>
</div>
<button
class="bg-blue-500 text-white text-sm font-medium rounded px-4 py-2"
>
Authorize
</button>
</div>
</div>
</header>
<main class="flex-1 max-w-7xl mx-auto w-full px-4 py-6">
<section class="mb-8 bg-white p-6 rounded shadow-sm">
<p class="text-gray-700 mb-4">
This is a sample Pet Store Server based on the OpenAPI 3.0 specification.
You can find out more about Swagger at
<a class="text-blue-600 underline" href="https://swagger.io"
>https://swagger.io</a
>.
</p>
<p class="text-gray-700 mb-4">
In the third iteration of the pet store, we've switched to the design-first
approach! You can help us improve the API whether it's by making changes
to the definition itself or to the code. That way, with time, we can improve
the API in general, and expose some of the new features in OAS3.
</p>
<ul class="list-disc list-inside text-gray-700">
<li>
<a class="text-blue-600 underline"
href="https://github.com/swagger-api/swagger-petstore"
>The Pet Store repository</a
>
</li>
<li>
<a class="text-blue-600 underline"
href="https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml"
>The source API definition for the Pet Store</a
>
</li>
</ul>
<div class="flex items-center space-x-4 mt-4">
<a
class="text-sm text-blue-600 hover:underline"
href="#"
>Terms of service</a
>
<a
class="text-sm text-blue-600 hover:underline"
href="#"
>Contact the developer</a
>
<a
class="text-sm text-blue-600 hover:underline"
href="#"
>Apache 2.0</a
>
<a
class="text-sm text-blue-600 hover:underline"
href="#"
>Find out more about Swagger</a
>
</div>
</section>
<section class="bg-white p-6 rounded shadow-sm">
<h2 class="text-xl font-bold text-gray-700 mb-2">
pet <span class="text-sm font-normal">Everything about your Pets</span>
</h2>
<div class="border-l-4 border-orange-500 pl-4 py-4 space-y-4">
<div class="flex items-center space-x-4">
<span
class="bg-orange-500 text-white text-xs font-semibold uppercase py-1 px-2 rounded"
>
PUT
</span>
<h3 class="text-lg font-semibold text-gray-700">
/pet
</h3>
<span class="text-gray-500 text-sm italic"
>Update an existing pet by Id</span
>
</div>
<div>
<button
id="tryItOutBtn"
class="bg-gray-100 border border-gray-300 text-sm px-3 py-1 rounded hover:bg-gray-200 transition"
>
Try it out
</button>
</div>
<div class="mt-4">
<h4 class="text-md font-semibold text-gray-700">Parameters</h4>
<p class="text-gray-500 text-sm ml-4">No parameters</p>
</div>
<div class="mt-4">
<h4 class="text-md font-semibold text-gray-700">Request body <span class="text-red-500">*</span></h4>
<p class="text-gray-500 text-sm ml-4">Update an existent pet in the store</p>
<div class="mt-2">
<p class="text-sm text-gray-700">Example Value | <span class="text-sm text-gray-500">Schema</span></p>
<pre class="mt-1" id="requestExample">
{
<span class="keyword">"id"</span>: <span class="number">10</span>,
<span class="keyword">"name"</span>: <span class="string">"doggie"</span>,
<span class="keyword">"category"</span>: {
<span class="keyword">"id"</span>: <span class="number">0</span>,
<span class="keyword">"name"</span>: <span class="string">"Dogs"</span>
},
<span class="keyword">"photoUrls"</span>: [
<span class="string">"string"</span>
],
<span class="keyword">"tags"</span>: [
{
<span class="keyword">"id"</span>: <span class="number">0</span>,
<span class="keyword">"name"</span>: <span class="string">"string"</span>
}
],
<span class="keyword">"status"</span>: <span class="string">"available"</span>
}
</pre>
</div>
<div class="mt-2 hidden" id="requestEditorContainer">
<textarea
id="requestEditor"
class="w-full border border-gray-300 rounded p-2 text-sm focus:outline-none"
rows="8"
>{
"id": 10,
"name": "doggie",
"category": {
"id": 0,
"name": "Dogs"
},
"photoUrls": [
"string"
],
"tags": [
{
"id": 0,
"name": "string"
}
],
"status": "available"
}</textarea>
<div class="mt-2">
<button
id="executeBtn"
class="bg-blue-500 text-white text-sm font-medium rounded px-4 py-2 mr-2"
>
Execute
</button>
<button
id="cancelBtn"
class="bg-white border border-gray-300 text-sm font-medium rounded px-4 py-2"
>
Cancel
</button>
</div>
</div>
</div>
<div class="mt-8">
<h4 class="text-md font-semibold text-gray-700">Responses</h4>
<div class="flex flex-col space-y-2 mt-4 ml-4">
<div class="flex flex-col sm:flex-row items-start sm:items-center sm:justify-between bg-gray-50 p-2 rounded border">
<div class="flex items-center space-x-2 mb-2 sm:mb-0">
<span
class="text-xs font-medium px-2 py-1 rounded bg-green-600 text-white"
>
200
</span>
<span class="text-sm text-gray-700">Successful operation</span>
</div>
<div class="flex items-center space-x-2">
<span class="text-sm text-gray-500 italic">Mime type:</span>
<select
class="border border-gray-300 text-sm rounded px-2 py-1"
>
<option value="application/json">application/json</option>
</select>
</div>
</div>
<div class="ml-6 mt-2">
<pre>
{
<span class="keyword">"id"</span>: <span class="number">10</span>,
<span class="keyword">"name"</span>: <span class="string">"doggie"</span>,
<span class="keyword">"category"</span>: {
<span class="keyword">"id"</span>: <span class="number">0</span>,
<span class="keyword">"name"</span>: <span class="string">"Dogs"</span>
},
<span class="keyword">"photoUrls"</span>: [
<span class="string">"string"</span>
],
<span class="keyword">"tags"</span>: [
{
<span class="keyword">"id"</span>: <span class="number">0</span>,
<span class="keyword">"name"</span>: <span class="string">"string"</span>
}
],
<span class="keyword">"status"</span>: <span class="string">"available"</span>
}
</pre>
</div>
<div class="flex items-center bg-gray-50 p-2 rounded border">
<span
class="text-xs font-medium px-2 py-1 rounded bg-red-600 text-white"
>
400
</span>
<span class="text-sm text-gray-700 ml-2">Invalid ID supplied</span>
</div>
<div class="flex items-center bg-gray-50 p-2 rounded border">
<span
class="text-xs font-medium px-2 py-1 rounded bg-red-600 text-white"
>
404
</span>
<span class="text-sm text-gray-700 ml-2">Pet not found</span>
</div>
<div class="flex items-center bg-gray-50 p-2 rounded border">
<span
class="text-xs font-medium px-2 py-1 rounded bg-red-600 text-white"
>
422
</span>
<span class="text-sm text-gray-700 ml-2">Validation exception</span>
</div>
<div class="flex flex-col sm:flex-row items-start sm:items-center sm:justify-between bg-gray-50 p-2 rounded border">
<div class="flex items-center space-x-2 mb-2 sm:mb-0">
<span
class="text-xs font-medium px-2 py-1 rounded bg-gray-500 text-white"
>
default
</span>
<span class="text-sm text-gray-700">Unexpected error</span>
</div>
<div class="flex items-center space-x-2">
<span class="text-sm text-gray-500 italic">Mime type:</span>
<select
class="border border-gray-300 text-sm rounded px-2 py-1"
>
<option value="application/json">application/json</option>
</select>
</div>
</div>
<div class="ml-6 mt-2">
<pre>
{
<span class="keyword">"code"</span>: <span class="string">"string"</span>,
<span class="keyword">"message"</span>: <span class="string">"string"</span>
}
</pre>
</div>
</div>
</div>
</div>
</section>
</main>
<footer class="bg-white mt-auto shadow-sm">
<div class="max-w-7xl mx-auto px-4 py-4 text-sm text-gray-500">
<p class="mb-0">
© 2025 Petstore API. All rights reserved.
</p>
</div>
</footer>
<script type="module">
const tryItOutBtn = document.getElementById('tryItOutBtn');
const requestEditorContainer = document.getElementById('requestEditorContainer');
const requestExample = document.getElementById('requestExample');
const executeBtn = document.getElementById('executeBtn');
const cancelBtn = document.getElementById('cancelBtn');
tryItOutBtn.addEventListener('click', () => {
tryItOutBtn.classList.add('hidden');
requestExample.classList.add('hidden');
requestEditorContainer.classList.remove('hidden');
});
cancelBtn.addEventListener('click', () => {
tryItOutBtn.classList.remove('hidden');
requestExample.classList.remove('hidden');
requestEditorContainer.classList.add('hidden');
});
executeBtn.addEventListener('click', () => {
const editedRequest = document.getElementById('requestEditor').value;
alert("Simulating PUT /pet with body:\n\n" + editedRequest);
});
</script>
</body>
</html>
Tips
今回はスクリーンショットからのデザイン生成例を示しましたが、tldrawはワイヤフレームの作成ツールなので、公式の動画にあるようにワイヤフレームの作成を行いつつmakerealでデザイン生成する、といった利用が本来の利用イメージに近いと思われます。
また、内部的には make-real/app/prompt.ts
で定義されたプロンプトを投げているようなので、ここを修正することでプロンプトチューニングも可能そうです。
プロンプト内に
- The HTML file should be self-contained and not reference any external resources except those listed below:
- Use tailwind (via \`cdn.tailwindcss.com\`) for styling.
- Use unpkg or skypack to import any required JavaScript dependencies.
- Use Google fonts to pull in any open source fonts you require.
- If you have any images, load them from Unsplash or use solid colored rectangles as placeholders.
- Create SVGs as needed for any icons.
との記載があるので、 Use tailwind (via \cdn.tailwindcss.com\) for styling.
の部分を別のCSSフレームワークなどに置き換えることで、tailwind以外にも対応できるのかもしれません。
まとめ
今回 tldraw.makerealを使ってみた結果、以下のようなメリットを感じました。
- 初期デザインから実装までのブレが少なくなる
- パワーポイント等の紙芝居ベースでも素早くビジュアル化できる
- セルフホスト出来るのでセキュリティ要件もクリアしやすい
- Tailwind CSS で実際のコードにつなぎやすい
- 簡単な画面遷移やインタラクションを事前に確認できる
プロジェクトでは、 初期にある程度形にしたモックやプロトタイプベースでステークホルダーと会話できるかが鍵になると考えています。
tldraw.makereal は「簡易UIデザイン」+「コードへの変換」のプロトタイプ作成の架け橋を効率的に担ってくれるため、実際にフロントエンドの実装工数を大幅に減らす一手になりました。
- 「デザインのラフスケッチをとりあえずコードにしやすい形でまとめたい」
- 「早くモックを作ってプロジェクト内に共有したい」
こんな課題を抱えている方に、tldraw.makereal は最適だと感じています。ぜひ一度試してみてはいかがでしょうか?