Figma で詰めたカンプを Rork に渡し、生成された画面を実機で開いた瞬間、「合っている、けれど違う」と感じたことはないでしょうか。
私も同じ場面で手が止まりました。要素は全部ある。色も合っている。並び順も正しい。それなのに、Figma を隣に並べると明らかに別物に見えるのです。困ったのは、どこが違うのかを言葉にできなかったことでした。「なんか野暮ったい」以上の説明ができないまま、Fix Now に曖昧な指示を投げては微妙に外れた修正が返ってくる、という往復を何度か繰り返しました。個人開発だとデザインも実装も同じ手が行き来するので、この曖昧さはそのまま消えた時間として返ってきます。
やり方を変えたのは、目視をあきらめてからです。Figma の Dev Mode で各要素の数値を控え、生成コードの StyleSheet を横に開いて、一つずつ突き合わせました。地味な作業でしたが、そこでようやく輪郭が見えました。ズレはランダムに散らばっているのではなく、毎回ほとんど同じ5つのプロパティに集まっていたのです。
以下では、その5か所を具体的に挙げます。あわせて、Auto Layout を flexbox の語彙へ翻訳しておく表と、スクリーンショットに添えて渡すトークンの形も置いておきます。
目視をやめて、数値で突き合わせる
先に、進め方だけ共有させてください。
Figma と実機のスクリーンショットを見比べる方法は、実のところあまり機能しませんでした。人の目は「全体の印象」で判断してしまうため、原因のプロパティにたどり着けません。「余白が広い気がする」と思った箇所が、実際には行間の差だった、ということが何度もありました。
代わりに、対象を1コンポーネントに絞り、Figma 側の値と生成コードの値を数値のまま並べます。Dev Mode が使えるプランなら CSS 相当の値がそのまま読めますし、使えなくても右パネルの数値を書き写せば足ります。
この突き合わせを5〜6コンポーネント分やると、同じ種類の差が繰り返し現れます。そこから先は、個別の修正ではなく「その種類をまとめて直す」に切り替えられます。原因を1回特定すれば、以降のコンポーネントは同じ処方で片づく、という状態に持っていくのが目的です。
Auto Layout を、先に flexbox の語彙へ翻訳しておく
ズレの話に入る前に、土台になる対応関係を整理しておきます。Auto Layout と flexbox は考え方が近いので「だいたい同じ」で済ませてしまいがちですが、名前が違うぶん、プロンプトで指示するときに言葉が滑ります。
| Figma(Auto Layout) | React Native の style | つまずきやすい点 |
|---|---|---|
| 縦に並べる | flexDirection: 'column' | RN の既定値は column。Web の常識と逆 |
| 横に並べる | flexDirection: 'row' | — |
| アイテム間の間隔 | gap | margin で代用すると端の余白が二重になる |
| パディング(4辺個別) | paddingTop ほか | Figma の「上下」「左右」まとめ指定に引きずられやすい |
| 主軸の配置 | justifyContent | — |
| 交差軸の配置 | alignItems | — |
| Fill container(主軸方向) | flex: 1 | ここが最頻出のズレ。後述 |
| Fill container(交差軸方向) | alignSelf: 'stretch' | 同上。width: '100%' で代用すると padding とけんかする |
| Hug contents | サイズ指定なし | — |
| Fixed | width / height | — |
| 折り返し | flexWrap: 'wrap' | — |
gap は React Native 0.71 以降で使えます。それより古い環境では margin での代用になりますが、その場合は最後の子要素の余白を打ち消す処理が必要になるため、生成コードがここでズレやすくなります。プロジェクトのバージョンは先に確認しておくと安全です。
ズレが集まる5か所
1. Fill と Hug の取り違え
いちばん多かったのがこれでした。Figma で「Fill container」にしていた要素が、生成コードでは内容に応じた幅(Hug 相当)になっている、あるいはその逆です。
見た目には「なんとなく中央に寄っている」「右側に余白が余っている」程度にしか映らないため、原因として疑われにくいのが厄介なところです。
直すときは、主軸か交差軸かを区別してください。縦並びのコンテナの中で「横いっぱいに広げたい」なら交差軸なので alignSelf: 'stretch'、縦並びの中で「残りの高さを埋めたい」なら主軸なので flex: 1 です。ここを width: '100%' で済ませると、親の padding と合算されて横にはみ出します。
2. 行間(lineHeight)
Figma のテキストは行送りが「Auto」のままになっていることが多く、この場合の実際の値はフォントのメトリクスから決まります。一方 React Native の lineHeight は明示的な数値です。指定しなければフォント既定に従うため、両者は簡単に食い違います。
2〜3px の差でも、5行のテキストブロックなら10px以上の差になります。「余白が広い」と感じた箇所の正体は、たいていこれでした。
Figma 側で行送りをパーセント指定している場合は fontSize × 割合 で px に直します。Auto のままなら、Dev Mode で計算後の値を読み取って、その数値をコードに固定してしまうのが確実です。デザイン側も Auto をやめて数値指定に統一しておくと、次回以降の突き合わせが楽になります。
3. 字間(letterSpacing)
行間ほど目立ちませんが、見出しで効いてきます。Figma でパーセント指定されている字間は、React Native では px なので変換が要ります。fontSize × パーセント ÷ 100 です。
12px のフォントに 2% の字間なら 0.24px。小さく見えますが、日本語の見出しは文字数が多いぶん累積するので、行の折り返し位置が変わることがあります。折り返しが1文字ずれるだけで印象は変わります。
4. 線の内側・外側(stroke alignment)
Figma の Stroke には Inside / Center / Outside があります。React Native の borderWidth は常に内側です。
つまり Figma で Outside の 1px 罫線を引いた 100px のボックスは Figma 上では 102px を占めますが、React Native では 100px のまま、内側 1px が borderに食われます。カード同士を並べたときに、境界の詰まり方が少しずつ違って見える原因がこれです。
対処はシンプルで、デザイン側の Stroke を Inside に統一するのが最も事故が少ないと感じています。Outside をどうしても保ちたい場合は、コード側でボックスの寸法に線幅の2倍を足す必要があります。
5. 影(shadow)
ここは完全一致をあきらめる場所です。
Figma のドロップシャドウは X / Y / ぼかし / 広がり / 色と不透明度を持ちます。React Native の従来のシャドウは、iOS が shadowColor shadowOffset shadowRadius shadowOpacity、Android は elevation のみです。Android では色も方向も指定できません。さらに「広がり(spread)」に相当するプロパティが従来のシャドウには存在しません。
ぼかしの値もそのままは使えません。目安として shadowRadius は Figma のぼかしの半分程度から始め、実機で見ながら詰めるのが早いです。数式で一致させようとするより、近い値から調整したほうが結果的に速く終わりました。
新しめの React Native では boxShadow が扱えるようになっていますが、プロジェクトのバージョンとアーキテクチャ設定に依存します。導入前に自分の環境で動くか確かめてください。
スクリーンショットに、テキストで数値を添える
ここまで挙げた5か所には共通点があります。どれも、スクリーンショットからは読み取れない情報だということです。
画像は構造とレイアウトを伝えるのには向いていますが、行間が22pxなのか24pxなのかは画像から確定できません。だから Rork がそこを推測で埋め、推測が外れる。当然といえば当然でした。
なので、画像に加えてテキストで数値を渡すようにしました。Figma の Variables をそのまま持ち出せると理想的ですが、Variables の読み取り用 REST API は上位プランでしか使えません。使えない場合は、実際に使っている値だけを手で書き出せば十分です。10行ほどの作業です。
// tokens.ts — Figma の Variables から実際に使っている値だけ書き出したもの
// as const を付けておくと fontWeight などが string に広がらず、StyleSheet でそのまま使えます
export const t = {
color: {
surface: '#171B22',
border: '#232A34',
text: '#E6E9EF',
textMuted: '#9AA4B2',
accent: '#4F8CFF',
},
space: { xs: 4, sm: 8, md: 12, lg: 16, xl: 24 },
radius: { sm: 6, md: 12, pill: 999 },
type: {
// lineHeight / letterSpacing を px で明示するのがこのファイルの主目的
title: { fontSize: 20, lineHeight: 26, letterSpacing: -0.2, fontWeight: '600' },
body: { fontSize: 15, lineHeight: 22, letterSpacing: 0, fontWeight: '400' },
caption: { fontSize: 12, lineHeight: 16, letterSpacing: 0.2, fontWeight: '400' },
},
} as const;これをプロンプトに貼り、「色・余白・タイポグラフィはこのトークンを参照し、生の数値をコードに直接書かないでください」と一文添えます。それだけで、生成コードにハードコードされた #171B22 が並ぶ状態はかなり減りました。
生成結果を受け取ったあとは、Figma の指定をコメントで残しておくと、次に再生成したときの突き合わせが速くなります。
import { Platform, StyleSheet } from 'react-native';
import { t } from './tokens';
export const card = StyleSheet.create({
root: {
// Figma: Vertical / gap 12 / padding 16 / 横は Fill container
flexDirection: 'column',
gap: t.space.md,
padding: t.space.lg,
alignSelf: 'stretch', // width: '100%' にすると親の padding とけんかする
backgroundColor: t.color.surface,
borderRadius: t.radius.md,
// Figma: Stroke 1px / Inside(Outside のままだと外形が 2px 大きくなる)
borderWidth: 1,
borderColor: t.color.border,
// Figma: Drop shadow Y=4 / ぼかし 16 / 広がり 0 / #000 24%
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowRadius: 8, // ぼかし 16 の半分から始めて実機で調整
shadowOpacity: 0.24,
},
// Android は elevation のみ。色も方向も指定できないため近似で妥協する
android: { elevation: 4 },
}),
},
title: { ...t.type.title, color: t.color.text },
body: { ...t.type.body, color: t.color.textMuted },
});トークンを単一の情報源として扱う考え方そのものは、再生成のたびに色と余白がずれる問題をデザイントークンで止めるでより踏み込んで書いています。Tailwind の語彙で揃えたい場合はRorkアプリにNativeWindを導入する方法も参考になるはずです。
直す順番を固定する
差分が複数見つかったとき、目についたものから直すと手戻りが出ます。上流のプロパティを変えると、下流の見え方も動くからです。
私は次の順に固定しました。
- 構造 — 入れ子の関係と
flexDirection。ここが違うと以降の調整が全部やり直しになります - 寸法 — Fill / Hug、
gap、padding。1と2で見た目の大半は合います - 文字 —
lineHeight、letterSpacing、fontWeight - 装飾 — 角丸、罫線、影
3と4を先にやってしまうと、2の修正でレイアウトが動いたときに調整し直しになります。逆に1と2を固めてしまえば、3と4は独立して詰められます。
Fix Now に投げるときも、この単位で1回ずつ依頼したほうが結果が安定しました。「デザイン通りにしてください」ではなく「このコンポーネントの gap を 12、padding を 16 にしてください」と、直す層と数値を指定する形です。
合わせきらない部分をどう扱うか
正直なところ、Figma と実機を完全に一致させるのは現実的ではありません。私はこの前提を先に置いてから作業を始めるようにしています。
影は前述のとおり Android で近似にしかなりません。フォントのレンダリングも Figma とプラットフォームで微妙に異なりますし、端末のダイナミックタイプ設定を尊重すれば、そもそもカンプ通りの文字サイズにはなりません。App Store でアクセシビリティ設定への追従が見られる箇所でもありますし、そこを固定してしまうほうがアプリとしては不親切です。
だから、合わせる対象を先に決めておくようにしました。構造・寸法・文字の階層関係は合わせる。影の細部と、ユーザー設定で変わる部分は合わせない。この線引きをデザイン側と共有しておくと、レビューで「1px違う」という議論に時間を取られずに済みます。
Rork のような生成ツールを使うと、実装そのものにかかる時間は確実に減ります。ただ、減ったぶんの時間が「デザインとの差を詰める作業」に移っただけ、という感覚も同時にありました。その作業を効率よく終わらせるには、勘ではなく数値で話す必要がある、というのが今のところの結論です。
次にやってみること
まず、いちばん再利用されているコンポーネントを1つ選んでください。カードでもリスト行でも構いません。
そのコンポーネントについて Figma の値と生成コードの StyleSheet を並べ、この記事の5か所(Fill/Hug、行間、字間、線の内外、影)だけを確認します。おそらく2つか3つは当てはまるはずです。当てはまった項目を tokens.ts に書き出して、次の生成からプロンプトに添える。ここまでが最初の一周です。
一周終えると、二周目以降のコンポーネントは同じ処方で片づきます。私の場合、3コンポーネント目からは突き合わせ自体がほとんど不要になりました。
同じところで足踏みしている方の遠回りが少しでも短くなれば嬉しいです。