ある日、リリース済みのアプリのスクリーンショットを並べて見ていて、妙な違和感を覚えました。同じアプリの中なのに、設定画面のボタンだけ角丸が一回り大きいのです。原因をたどると、その画面だけ前の週に Rork で一度作り直していました。生成のたびに、角丸の半径が 12 から 16 へ、主要色の青がほんの少し明るいほうへ、と静かにずれていたのです。
一画面なら手で直せます。けれど個人開発で複数のアプリを抱えていると、この「じわじわ」が積み重なって、いつの間にかブランドの輪郭がぼやけます。生成 AI は指示の外側を埋めるのが得意で、色や余白のような「言わなかった部分」を毎回それらしく補完します。その補完こそが、再生成のたびの揺れの正体でした。
なぜ生成物の見た目は毎回ずれるのか
Rork が生成する Expo アプリのスタイルは、多くの場合コンポーネントの中に直接数値で書き込まれます。borderRadius: 12、padding: 16、color: "#2563EB" のような形です。これらはどの画面にも散らばっていて、再生成のときに AI が文脈から「だいたいこのくらい」と推定し直します。指示に明示されていない数値は、毎回わずかに違う値で再構成されます。
つまり揺れの根本は、見た目の決定権が無数のコンポーネントに分散していることです。決定権が散らばっている限り、再生成は揺れます。直す方向は1つで、見た目の決定を1か所に集めることです。
トークンを単一情報源にまとめる
最初の一歩は、色・余白・角丸・タイポグラフィといった「デザインの語彙」を、1つのファイルに固定することです。このファイルだけは AI に再生成させず、人間が管理します。
// tokens.ts — このアプリの見た目を決める唯一の場所
// AI に再生成させない。変更は必ずここを手で編集する
export const tokens = {
color: {
brand: "#2563EB",
brandPressed: "#1D4ED8",
bg: "#FFFFFF",
text: "#0F172A",
textMuted: "#64748B",
danger: "#DC2626",
},
radius: { sm: 8, md: 12, lg: 16, pill: 999 },
space: { xs: 4, sm: 8, md: 12, lg: 16, xl: 24, xxl: 32 },
font: { body: 16, title: 22, caption: 13 },
} as const;
export type Tokens = typeof tokens;
コンポーネント側は、もう生の数値を持ちません。必ずトークンを経由します。
// PrimaryButton.tsx — 生の数値を一切書かない。すべてトークン参照
import { Pressable, Text, StyleSheet } from "react-native";
import { tokens } from "../tokens";
export function PrimaryButton({ label, onPress }: {
label: string; onPress: () => void;
}) {
return (
<Pressable onPress={onPress} style={styles.btn}>
<Text style={styles.label}>{label}</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
btn: {
backgroundColor: tokens.color.brand,
borderRadius: tokens.radius.md,
paddingVertical: tokens.space.md,
paddingHorizontal: tokens.space.lg,
alignItems: "center",
},
label: { color: "#FFFFFF", fontSize: tokens.font.body, fontWeight: "600" },
});
これで「青ボタンの角丸を 12 にする」という決定は、世界に1か所しか存在しなくなります。再生成が borderRadius を勝手に推定する余地が消えます。
生成 AI に毎回トークンを参照させる
ファイルを作っただけでは足りません。Rork に画面を生成させるとき、生のスタイル値を書かせない約束を、プロンプトの規約として固定します。私が使っている指示は、おおむね次の形です。
- スタイルは必ず
tokens.ts の値を参照すること。生の数値・カラーコードを直接書かない
- 新しい色や余白が必要になったら、コンポーネントに書かず、まず
tokens.ts に追加候補として提案する
- 既存のトークンで表現できる見た目は、新しい値を作らずに再利用する
この3点を毎回の生成指示の冒頭に置くだけで、生成物がトークンを参照する率が目に見えて上がりました。AI は禁止より「どこを見ればよいか」を示されたほうが素直に従います。
逸脱を CI で検出する
それでも、生成物に生の数値が紛れ込むことはあります。人間のレビューだけに頼ると見落とすので、機械で検出します。コンポーネント内に裸のカラーコードやマジックナンバーがあれば落とす、軽いスクリプトを置きます。
// check-tokens.mjs — components 配下に生スタイル値が混入していないか検査
import { readFileSync } from "node:fs";
import { globSync } from "glob";
const files = globSync("src/components/**/*.tsx");
const hexColor = /#[0-9a-fA-F]{3,8}\b/;
// tokens.ts 以外で許可しないパターン(白だけは例外運用も可)
let violations = 0;
for (const f of files) {
const lines = readFileSync(f, "utf8").split("\n");
lines.forEach((line, i) => {
if (line.includes("tokens.")) return; // トークン参照行は対象外
if (hexColor.test(line) && !line.includes("#FFFFFF")) {
console.log(`${f}:${i + 1} 生カラーコード: ${line.trim()}`);
violations++;
}
});
}
if (violations > 0) {
console.log(`\n❌ ${violations} 件の生スタイル値を検出。tokens.ts に寄せてください`);
process.exit(1);
}
console.log("✅ 生スタイル値なし");
このスクリプトを push 前に走らせると、再生成で入り込んだ生の #3B82F6 のような値が一覧で出ます。検出されたら、その値を tokens.ts の既存トークンに置き換えるか、新しいトークンとして正式に登録します。揺れを「直す」のではなく、揺れが入った瞬間に「気づく」仕組みを置くのが要点です。
複数アプリで一貫性を保つときの注意
6本のアプリを並行して運用していると、トークンを全アプリで完全に共有したくなります。けれど私の経験では、共有しすぎると今度は1本のブランド変更が他の5本に波及して、かえって動きが鈍くなりました。
実用的だったのは、構造だけ共有して値は各アプリが持つ方式です。radius や space のキー名と段階数(sm/md/lg…)は全アプリで揃え、実際の色や数値は各アプリの tokens.ts に閉じます。こうすると、新しい画面を別アプリへ移植するときにコンポーネントがそのまま動き、ブランドの色だけは各アプリで独立して育てられます。
本番で効いた数値と、私が避けた落とし穴
この仕組みを実際の運用に入れてから、効果を数値で実感した場面がいくつかありました。トークン化する前は、画面を1枚再生成するたびに見た目のレビューに平均で10分ほどかけていました。色や余白が前と同じかを目視で照合していたためです。トークンに寄せたあとは、その照合作業がほぼ消え、レビュー時間は2〜3分まで縮みました。1日に数画面を触る週は、この差が積み上がって体感で大きく変わります。
私自身がつまずいた落とし穴も共有しておきます。最初、トークンを導入したのに揺れが止まらず、半日ほど原因を追ったことがありました。本番ビルドで確認すると、生成された一部のコンポーネントが tokens.color.brand を import しつつ、その隣で "#2563EB" という同じ色を生のまま併記していたのです。見た目が同じなので動作上のエラーは出ず、レビューでも見逃していました。だからこそ、後述の CI による機械検出が要ります。人間の目は「同じ色」を異常だと感じません。
もう1つの落とし穴は、トークンを細かく刻みすぎたことです。余白を6段階で足りるところに10段階用意したら、生成 AI がどれを選ぶか迷い、かえって一貫性が落ちました。私は段階数を絞ることを推奨します。個人的には、余白は xs から xxl までの6段階、角丸は4段階に収めるのが、AI にとっても人間にとっても判断しやすい粒度でした。AdMob のバナーを差し込む画面でも、余白の段階が少ないほうがレイアウト崩れの予測が立てやすく、App Store のスクリーンショットを撮り直す回数も減りました。
判断の指針としては、新しいトークンを足したくなったら一拍置いて、既存の段階で代用できないかを先に考えるのが実用的です。トークンは増やすほど自由になりますが、自由が増えるほど再生成の揺れも戻ってきます。
次の一手
今あるアプリのコンポーネントから、まず色だけを tokens.ts に抜き出してみてください。色は揺れがいちばん目に見える要素で、効果を実感しやすいからです。そのうえで check-tokens.mjs を push 前に挟めば、次の再生成からは「気づかないうちにずれる」状態を抜け出せます。
地味な仕込みですが、長く運用するアプリほど効いてきます。同じ悩みを持つ方の役に立てば嬉しく思います。