framer-code-components-overrides について
Framerのコードコンポーネントやオーバーライドを扱うコーディングエージェント向けの、コミュニティ管理スキルです。Framerの標準機能を優先し、実装パターンやよくある問題をまとめています。
主な機能
- 01コードコンポーネントとオーバーライドのガイド
- 02標準機能を優先する判断指針
- 03Framer固有の問題と対処法
- 04セッションごとの更新と貢献手順
活用シーン
- 01Framerの機能にコードが必要か判断
- 02コンポーネントやオーバーライドの問題調査
- 03インストール済みスキルの更新
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ fredm00n/ framerlabs --skill framer-code-components-overridesインストール後、Claude Code で/framer-code-components-overridesを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/cms.mdfiber-handlers.mdhls-video.mdpatterns.mdproperty-controls.mdwebgl-shaders.md詳細
Framer コード開発
セクションタグ:[C] はコードコンポーネント、[O] はコードオーバーライド、[C/O] は両方に適用されます。
更新とコントリビューション
このスキルは公開リポジトリ fredm00n/framerlabs で管理され、コミュニティによって最新の状態に保たれています。インストール済みのコピーは古くなるため、以下の手順はセッションごとに一度実行します。
プロジェクトの指示行
プロジェクトのエージェント向け指示ファイル(CLAUDE.md、AGENTS.md、または同等のファイル)には、このスキル用の1行を記載できます。
framer-code-components-overrides: auto-update, contribute
auto-update を指定すると、新しいバージョンがある場合にエージェントがインストール済みのコピーを置き換えられます。contribute を指定すると、セッションで得た知見をプルリクエストとしてリポジトリに送ることを提案できます。どちらか一方だけでも機能します。この行がない場合、エージェントはインストール済みファイルを編集したり、プルリクエストを作成または提案したりしません。新しいバージョンがある場合に知らせるだけです。
まず新しいバージョンを確認する
以下の内容を利用する前に:
https://raw.githubusercontent.com/fredm00n/framerlabs/main/skills/framer-code-components-overrides/SKILL.mdを取得し、そのmetadata.versionとこのファイルのフロントマターに記載されたバージョンを比較します。- 同じバージョンの場合、または取得に失敗した場合(オフライン、ネットワークがブロックされているなど):このコピーを使い続けます。取得に失敗した場合は、更新を確認できなかったことを一度だけ伝えます。
- リモートのほうが新しい場合: このファイルの残りの内容ではなく、リモートの SKILL.md をこのセッションで使用し、参照ファイルもローカルのコピーではなく、同じ場所(
https://raw.githubusercontent.com/fredm00n/framerlabs/main/skills/framer-code-components-overrides/references/<file>.md)から読み込みます。そのうえで、プロジェクトの指示行に従います(NEW と OLD はそれぞれのバージョン番号です)。auto-updateがない場合: インストール済みのファイルはそのままにして、ユーザーに次のように伝えます。「framer-code-components-overrides の NEW が公開されており、お使いのコピーは OLD なので、今回は新しいバージョンを使用します。コピーを更新するには、npx skills add fredm00n/framerlabs --skill framer-code-components-overridesを実行するか、CLAUDE.md または AGENTS.md にframer-code-components-overrides: auto-updateを追加すると、自動的に最新の状態を保てます。このスキルはコミュニティによって管理されています。その行にcontributeを追加すると(framer-code-components-overrides: auto-update, contribute)、得られた知見をプルリクエストとして送ることを提案します。」auto-updateがある場合: リポジトリからskills/framer-code-components-overrides/フォルダー全体をダウンロードし(一時ディレクトリへの shallowgit cloneが最も簡単です)、インストール済みのフォルダーと一度に置き換えます。これにより、上流で削除されたファイルも削除されます。ダウンロードに失敗した場合は、インストール済みのコピーを変更しません。git リポジトリ内では、更新は通常の変更として扱います。保護されたブランチに直接コミットせず、現在の作業で使用するブランチまたは PR に、単独でコミットします。ユーザーには次のように伝えます。「framer-code-components-overrides を OLD から NEW に更新し、独立したコミットにしました。」指示行にcontributeがない場合は、次の文も追加します。「このスキルはコミュニティによって管理されています。CLAUDE.md または AGENTS.md の指示行にcontributeを追加すると、得られた知見をプルリクエストとして送ることを提案します。」
更新は、指定された正確なリポジトリとパスからのみ取得してください。
学んだことを上流に共有する
このスキルに関するプロジェクトの行に contribute が含まれている場合に限ります。
作業の最後に、その途中ではなく、このスキルに記載すべき学びがセッション中にあったか確認します。実際のビルドで検証済みで、解決にかなりの時間がかかった、または何かを壊した事実であり、すべての Framer ユーザーに当てはまり、このスキルにも Framer の公式ドキュメントにも記載されていないことが条件です。1つのプロジェクトだけに関する事実は該当しません。該当するものがなければ、何も言いません。
該当するものがある場合:
- 各知見を、初めて読む人にも分かるように書き直します。作業中のプロジェクトと照らし合わせ、そこに由来するものをすべて取り除きます。会社名、クライアント名、製品名、ドメイン、Framer プロジェクト ID、ブランチ名、ファイルパス、キー、トークン、スクリーンショットなどが対象です。知見を得た日付は "(2026-10-12)" の形式で残します。
fredm00n/framerlabsをクローンし、mainからブランチを作成して、skills/framer-code-components-overrides/を編集します。各知見を該当するトピックの下に記載し、似た内容を重複して追加するのではなく、既存の行を編集します。metadata.versionを更新し(修正ならパッチ、新しいセクションならマイナー)、ルートのREADME.mdの今日の日付の下に、平易な言葉で変更内容を1行追加します。- 同じ知見をすでに扱っているオープンなプルリクエストがないか確認します。該当するものがあれば、そちらに追加します。
- プルリクエストを作成する前に、差分をユーザーに見せて確認を求めます。公開リポジトリのプルリクエストは、マージされなくても、作成された時点で公開されます。
- ユーザーの承認を得たら、ブランチをプッシュし(リポジトリへの書き込み権限がないフォークにプッシュ)、
mainを対象にプルリクエストを作成して、リンクをユーザーに伝えます。コミットメッセージ、タイトル、説明は、スキルを使う人に向けて、変更内容と役立つ場面を説明するものにします。どのプロジェクトやセッションが発端かは書きません。
メンテナー向け: このスキルまたは参照ファイルを変更するたびに metadata.version を更新してください。更新しないと、インストール済みのコピーには変更が反映されません。
ネイティブ優先:コードは本当に必要ですか? [C/O]
ユーザーがコードコンポーネントまたはコードオーバーライドを明示的に求めている場合を除き、レイヤー、バリアント、エフェクト、CMS バインディングを使って Framer でネイティブに機能を構築します。コードは、それらで表現できないもの(WebGL、キャンバス、サードパーティの SDK、スクロール計算、DOM、ブラウザー API)に限って記述します。
この方針は、実績のある UI パターンで特に重要です。FAQ アコーディオン、タブ、お客様の声のスライダー、料金プランの切り替え、ナビゲーションのドロップダウン、モーダルは、どれも Framer でネイティブに構築でき、ネイティブ実装のほうが優れています。サイト所有者はコードなしで編集し、CMS にバインドし、テキストスタイルやカラー スタイルで見た目を変更でき、ブレークポイントに適応させ、Framer のアップグレード後もそのまま使えます。こうした機能をコードコンポーネントで実装するのは、動作や見た目に問題がなくても、過剰設計です。
ディスクロージャーまたはアコーディオンは、「折りたたみ」と「展開」の2つのバリアントを持つ1つのコンポーネントだけで構成し、ほかには何も加えません。項目全体を囲むラッパーコンポーネントは作らず、コンテンツを CMS から取得するために、コレクションリストを使って項目を繰り返します。ネイティブレイヤーですでに実現できるものを再現するプロパティ、オブジェクトの配列、フォント、ウェイト、カラーのコントロールは使いません。
ネイティブで実現できるか判断できない場合は、コードを書く前にユーザーに確認します。確認にかかるのはメッセージ1通ですが、誤った判断をすると、ビルド全体をやり直すことになります。
注意点(クイックリファレンス)
| 問題 | 原因 | 修正方法 |
|---|---|---|
| オーバーライドで変数テキストが見つからない | props.children だけを読み取っている | まず props.text を確認してください。変数にバインドされたテキストは children を経由しません |
| フォントスタイルが適用されない | フォントの各プロパティに個別にアクセスしている | フォントオブジェクト全体を展開してください:...props.font |
| ハイドレーションの不一致 | レンダリング中にブラウザー API を使用している | isClient の状態パターンを使用してください |
style でハイドレーションエラー #425 が発生し、> セレクターや引用符付き属性値を含む SSR CSS が適用されない | React 18 のサーバーレンダラーは <style>{css}</style> のテキストをエスケープするため、> は >、" は " として送信されます | <style dangerouslySetInnerHTML={{ __html: css }} /> をレンダリングしてください |
| 寸法が 0 のまま/SSR されたサイズが残る | window から読み取った初期状態がすでに実際の値と等しく、setState が何もしない | 状態を 0 で初期化し、エフェクト内で切り替えてください(ハイドレーションの安全性を参照) |
| 後の公開後にオーバーライドが要素を見つけられず、エラーもコードの変更もない | React の useId() で自身の要素を検索していた。ハイドレーション時にサーバーレンダリングと不一致になると、DOM にはサーバー側の id が残る | マウント後にブラウザーで id を作成するか、ref を使用してください(要素をリンクする IDを参照) |
子要素に width、offsetLeft、offsetParent がなく、その要素の scroll-snap-align も無視される | Framer がフレームの各子要素を display: contents の要素でラップしている | ボックスを持つ最初の子孫要素までたどり、getBoundingClientRect で測定してください(フレームの子要素の測定を参照) |
| ユーザーがトークンをバインドするとカラー値でクラッシュする | トークンの場合、ControlType.Color は {value: "#xxx"} を返し、静的な値の場合は文字列を返す | 使用前に tok(v) でラップを解除してください(カラー トークンを参照) |
| オーバーライドの props が undefined | プロパティコントロールを想定している | オーバーライドは addPropertyControls をサポートしていません |
| Framer のピッカーのドロップダウンにオーバーライドが表示されない | エクスポート値がファクトリー/HOC ジェネレーターの 呼び出し によって生成されている。Framer の静的スキャナーはリテラル形式のオーバーライドのエクスポートのみを認識する | 各オーバーライドをリテラル形式の export function withX(Component) または export const withX = (Component) => … として記述してください(オーバーライドはリテラル形式でエクスポートする必要があるを参照) |
| スクロールアニメーションが動作しない | コンテナーに overflow: scroll が設定されている | ビューポートで IntersectionObserver を使用してください(スクロール検出を参照) |
| ターゲット ID を設定するとスクロール/アニメーションが何も通知されず停止する | useScroll のターゲットが useState に格納され、最初のレンダー時の null が捕捉されている | 最新値を読み取るターゲットには useRef を使用してください(最新値を読み取る Refを参照) |
名前付き CMS レイヤーが findByFramerName で見つからない | レイヤーが動的コンポーネントのインスタンスであり、名前が data-framer-name にない | 動的コンポーネントを、期待する名前を持つプレーンなフレームでラップしてください |
| スロットに Collection List があるのにコードコンポーネントに空の状態が表示される | スロットの prop は要素が 1 つの配列であり、getCollectionData(array) はクエリを見つけられない | props.collectionList[0] を渡してください(references/cms.mdを参照) |
| CMS イベントの日付が一部の訪問者に対して誤った日になる | CMS の日付は UTC として記録された型付きの壁時計時刻であり、現地時間のゲッターによってずれる | getUTCDate() などを使って日付を読み取ってください(references/cms.mdを参照) |
| CMS アイテムの名前付きレイヤーがすべて同時に見つからなくなる | 繰り返される Collection List アイテム全体がコンポーネント化されている | アイテムはプレーンなフレームのままにしてください(繰り返しアイテムは決してコンポーネントにしないを参照) |
props.style の高さを書き換えるオーバーライドが公開ページで何もしない | Framer は生成 CSS クラスとしてサイズを出力し、ページルートにはインラインの min-height: 100vh が設定されている | ルートに対して !important を付け、インラインで値を設定してください(公開時のサイズ設定を参照) |
router.navigate() またはスクリプトによるリンクのクリック後、URL は変わるがページが読み込まれない | Framer はプログラムによるページ間ナビゲーションを、同期がずれたルート変更に変換する | 代わりにその場で処理してください(ページ間ナビゲーションを参照) |
| HLS 動画が永久に粗いままになる | HLS.js を使わずに Chrome で .m3u8 を使用している | HLS.js の動的インポートパターンを使用してください(HLS 動画ストリーミングを参照) |
| オーバーレイが「半押し」の状態で固まり、閉じるのに 2 回クリックする必要がある | 合成イベント(dispatchEvent)で Framer インタラクションをトリガーしている | fiber の走査で React ハンドラーを直接呼び出してください(references/fiber-handlers.mdを参照) |
| オーバーレイがコンテンツの下に隠れる | 親要素によるスタッキングコンテキスト | React Portal を使って document.body レベルにレンダリングしてください |
| シェーダーのアタッチエラー | コンパイル失敗による null シェーダー | attachShader() の前に createShader() の戻り値を確認してください |
TypeScript の Timeout エラー | NodeJS.Timeout 型を使っている | ブラウザー環境では代わりに number を使用してください |
| コンポーネントの表示名 | Framer UI でカスタム名が必要 | Component.displayName = "Name" |
| どのイージングカーブも同じように感じる | 初期距離を追跡していない | ターゲットが変化したときに initialDiff を追跡してください(references/patterns.mdを参照) |
| URL にバインドされたフィルターが、プログラムによる URL 書き込みに反応しない | replaceState/pushState は popstate を発火しない | 書き込み後に手動でイベントを発火させてください(URL への状態の書き込みを参照) |
| スライダーのドラッグでブラウザー履歴が大量に追加される/戻る操作が妨げられる | onChange のたびに pushState を実行している | 高頻度の書き込みには replaceState を使用してください |
| range 入力に青いネイティブの塗りつぶしが表示される/つまみのスタイルを変更できない | ネイティブの <input type="range"> の表示 | appearance:none を設定し、トラックを無効化して、エンジンごとにカスタムつまみを指定してください(ネイティブ range 入力のスタイル設定を参照) |
| オーバーライドが設定されたボタンが Enter と Space を無視する | onTap はポインタージェスチャーであり、キーボード操作や .click() では発火しない | インスタンスに onClick を渡してください(コンポーネントインスタンスを参照) |
オーバーライドで追加した DOM が消える、または React が insertBefore/removeChild エラーをスローする | オーバーライドが React の管理する子要素を置き換えた、または編集した | React のノードは置き換えず、隣に兄弟要素を挿入してください(React のノードの隣への DOM 挿入を参照) |
| 隠しフォームの値が空で届く | Framer がフォームを読み取った後に値が書き込まれた、または入力がコードによって挿入された | ネイティブの hidden input を使用し、キャプチャーフェーズの submit リスナーで値を入力してください(Framer フォームを参照) |
| 新しい CMS アイテムが再読み込み後にしか表示されない | CMS コンテンツは公開された各デプロイに組み込まれている | ページを再取得し、新しいカードを挿入してください(公開済み CMS コンテンツのその場での更新を参照) |
| 「非公開」の CMS フィールドが誰でも読み取れる | Framer はすべてのアイテムのすべてのフィールドを .framercms ファイルに公開する | 非公開データを CMS に保存しないでください(CMS データは公開されるを参照) |
目次
- ネイティブを第一に — そもそも機能にコードが必要かどうか
- 基礎 — コンポーネントとオーバーライドの違い、アノテーション、スターターテンプレート、複数ファイルのコンポーネント
- 作成 — プロパティコントロール、フォント、カラーのトークン
- レンダリングと SSR — ハイドレーション、キャンバスの検出、並行レンダリング、npm のインポート
- CMS — オーバーライド内のテキストのタイミング、繰り返し項目、コードコンポーネントでの CMS パターン、CMS が公開する内容
- オーバーライド — 特定のパターン — バリアントの制御、ファイバーのハンドラー、コンポーネントのインスタンス、Framer のフォーム
- DOM とパフォーマンス — スクロールの検出、ライブ読み取り用の ref、ポータル、公開時のサイズ設定、ページ間の移動、React の隣への挿入、CMS のライブ更新、一般的なパターン
- メディア — HLS 動画、WebGL
- デバッグ — 条件付きログ出力
基礎
コードコンポーネントとオーバーライド
コードコンポーネント [C]:キャンバスに追加するカスタム React コンポーネントです。addPropertyControls をサポートします。
コードオーバーライド [O]:既存のキャンバス要素をラップする高階コンポーネントです。addPropertyControls はサポートしません。
必須のアノテーション [C/O]
最低限、常に次を含めてください。
/**
* @framerDisableUnlink
* @framerIntrinsicWidth 100
* @framerIntrinsicHeight 100
*/
すべてのアノテーション:
@framerDisableUnlink— 変更時にリンク解除されないようにします@framerIntrinsicWidth/@framerIntrinsicHeight— デフォルトの寸法@framerSupportedLayoutWidth/@framerSupportedLayoutHeight—any、auto、fixed、any-prefer-fixed
ほとんどの Framer のコードファイルは、// @ts-nocheck から始められます。Framer のフックとプロパティの型は厳密ではなく、いくつかのパターン(条件付きフック、ファイバーの走査)は厳密な TypeScript ではエラーになります。
コードオーバーライドのパターン [O]
import type { ComponentType } from "react"
import { useState, useEffect } from "react"
/**
* @framerDisableUnlink
*/
export function withFeatureName(Component): ComponentType {
return (props) => {
// State and logic here
return <Component {...props} />
}
}
命名規則:必ず withFeatureName のプレフィックスを使用してください。
オーバーライドにはプロパティコントロールがなく、CMS への問い合わせもできません。CMS のコンテンツは、レンダリングされた時点でのみ読み取れます(CMS を参照)。設定はファイル先頭の定数ブロックに記述します。
オーバーライドはリテラル形式でエクスポートする必要があります(ピッカーによる検出) [O]
Framer のオーバーライドピッカーはファイルを静的にスキャンし、オーバーライドとして構文上認識できるエクスポート(リテラル形式の function 宣言、またはエクスポートに直接割り当てられたアロー関数)だけを一覧表示します。値が関数呼び出しの戻り値であるエクスポートはスキャナーに認識されず、名前で参照すれば動作する場合でも、オーバーライドのドロップダウンには表示されません。
ファクトリーを使って、よく似た複数のオーバーライドを DRY にまとめようとすると、特にこの問題が起きやすくなります。
// ❌ BROKEN — factory hides the overrides from the picker.
// Only literal functions in the file get listed; these three never appear.
const setVariant = (name) => (Component) => (props) => {
const [, setStore] = useStore()
return <Component {...props} onClick={() => setStore({ variant: name })} />
}
export const withSetVariant1 = setVariant("Variant 1") // not listed
export const withSetVariant2 = setVariant("Variant 2") // not listed
export const withSetVariant3 = setVariant("Variant 3") // not listed
// ✅ CORRECT — each override is a literal function; all are listed.
export function withSetVariant1(Component): ComponentType {
return (props) => {
const [, setStore] = useStore()
return <Component {...props} onClick={() => setStore({ variant: "Variant 1" })} />
}
}
どちらのリテラル形式も認識されます:export function withX(Component) {…} または export const withX = (Component) => (props) => …。ルール:ヘルパーを呼び出してオーバーライドを生成しないでください。冗長になっても、オーバーライドごとにリテラル形式を繰り返してください。(ストアを共有する 3 つのセッターと 1 つのリーダーの完全な例は、references/patterns.md → オーバーライド間での状態の共有 を参照してください。)
コードコンポーネントのパターン [C]
import { motion } from "framer-motion"
import { addPropertyControls, ControlType } from "framer"
/**
* @framerDisableUnlink
* @framerIntrinsicWidth 300
* @framerIntrinsicHeight 200
*/
export default function MyComponent(props) {
const { style } = props
return <motion.div style={{ ...style }}>{/* content */}</motion.div>
}
MyComponent.defaultProps = {
// Always define defaults
}
addPropertyControls(MyComponent, {
// Controls here
})
複数ファイルのコンポーネント [C]
複数のソースファイルに分割されたコンポーネントには、ソースファイルごとに Framer のコードファイルが 1 つ必要です。また、ファイル間のすべてのインポートには拡張子を含めます。./constants ではなく、./constants.tsx のように記述してください。
作成
プロパティコントロールのリファレンス [C]
コントロールの種類とパターンの全一覧は、references/property-controls.md を参照してください。
フォントの処理 [C/O]
フォントのプロパティに個別にアクセスしないでください。常にフォントオブジェクト全体をスプレッドしてください。
// ❌ BROKEN - Will not work
style={{
fontFamily: props.font.fontFamily,
fontSize: props.font.fontSize,
}}
// ✅ CORRECT - Spread entire object
style={{
...props.font,
}}
フォントコントロールの定義:
font: {
type: ControlType.Font,
controls: "extended",
defaultValue: {
fontFamily: "Inter",
fontWeight: 500,
fontSize: 16,
lineHeight: "1.5em",
},
}
カラートークン(ControlType.Color)[C]
ユーザーが静的な色を選択すると、ControlType.Color の値はプレーンな文字列として渡されますが、Framer のカラートークンにバインドすると、{ value: "#xxx" } オブジェクトとして渡されます。値を直接読み取るコンポーネントは、ユーザーがトークンをバインドした途端に動作しなくなります。
必ずラップを解除してください。
const tok = (v: any) =>
v && typeof v === "object" && "value" in v ? v.value : v
const bg = tok(props.background) // string in both cases
色のプロパティを解析、CSS 文字列、キャンバスのスタイルに使用する場合は、必ず tok() を使ってください。同じラッパー形式は、ほかのトークンにバインド可能なコントロール(サイズ、シャドウなど)にも現れることがあります。決めつける前に確認してください。
レンダリングと SSR
ハイドレーションの安全性 [C/O]
Framer はサーバー上で事前レンダリングを行います。SSR 中はブラウザー API を利用できません。
2 段階レンダリングパターン:
const [isClient, setIsClient] = useState(false)
useEffect(() => {
setIsClient(true)
}, [])
if (!isClient) {
return <Component {...props} /> // SSR-safe fallback
}
// Client-only logic here
レンダリング時に直接アクセスしないでください:
window、document、navigatorlocalStorage、sessionStoragewindow.innerWidth、window.innerHeight
これらの処理は useEffect 内で行い、useLayoutEffect は使わないでください。そうしないと、ハイドレーションの不一致や初回描画時のちらつきが発生します。
初期状態は SSR と一致させ、その後エフェクト内で切り替えてください:
// ❌ BROKEN — looks "smart" but isn't
const [vw, setVw] = useState(
typeof window !== "undefined" ? window.innerWidth : 0
)
useEffect(() => {
setVw(window.innerWidth) // no-op: state already equals window.innerWidth
}, [])
初期状態はクライアント上の実際の値とすでに一致しているため、setState は何もせず、SSR で出力された寸法(常に 0)がレンダリングされた DOM に永久に残ります。
// ✅ CORRECT — start at SSR-safe value, force a re-render via effect
const [vw, setVw] = useState(0)
const [vh, setVh] = useState(0)
useEffect(() => {
const onResize = () => {
setVw(window.innerWidth)
setVh(window.innerHeight)
}
onResize()
window.addEventListener("resize", onResize)
return () => window.removeEventListener("resize", onResize)
}, [])
// Hide the first paint (SSR'd at 0) until the effect has populated real values
<div style={{ opacity: vw > 0 ? 1 : 0 }} />
> 0 の不透明度ゲートとエフェクト内での切り替えを組み合わせると、初回描画時の表示を隠せます。そうしないと、更新時に 0/デフォルト値から実際のサイズへ変わる際にちらつきが見えます。
要素をリンクする ID [C/O]
React が生成する ID(useId())を使って要素をリンクしないでください。たとえば、document.querySelector('[data-x="' + useId() + '"]') で自身の要素を見つけるオーバーライドは避けてください。公開済みページのハイドレーション時にツリーがサーバーのレンダリングと異なると、DOM にはサーバー側の ID が残る一方、クライアントコードは独自の ID を探します。検索結果は null となり、エラーも発生しないまま、ある公開では動作していたコントロールが次の公開では機能しなくなります。
対象が ref を転送する場合は ref を使ってください。そうでない場合は、マウント後にブラウザー上で ID を作成し、ID が存在するようになってから属性をレンダリングしてください。
function useClientId() {
const [id, setId] = useState<string | null>(null)
useEffect(() => {
setId("x-" + Math.random().toString(36).slice(2, 10))
}, [])
return id
}
const id = useClientId()
<Component {...props} {...(id ? { "data-x": id } : {})} />
// every lookup returns early while id is null
公開またはコードの変更後は、編集した要素だけでなく、ページ上のすべてのインタラクティブ要素を再テストしてください。この種の不具合は、変更していないコードにも現れることがあります。
キャンバスとプレビューの判定 [C/O]
import { RenderTarget } from "framer"
const isOnCanvas = RenderTarget.current() === RenderTarget.canvas
// Show debug only in editor
{isOnCanvas && <DebugOverlay />}
次の場合に使用します:
- デバッグ用オーバーレイ
- エディターでの重いエフェクトの無効化
- プレビューの切り替え
並行レンダリング:状態更新を startTransition でラップする [C/O]
Framer は React の並行レンダラー上で動作します。イベントハンドラー内で複数の setter を使った更新(ステッパー、非同期チェーン、フォームフィールド)は、負荷が高いときにぎこちなくなることがあります。緊急性の低い更新はラップしてください。
import { startTransition } from "react"
const handleClick = () => {
startTransition(() => {
setQty((q) => ({ ...q, [id]: q[id] + 1 }))
setError(null)
})
}
const onSubmit = async () => {
startTransition(() => {
setLoading(true)
setError(null)
})
try {
const res = await fetch(...)
// ...
} catch (e) {
startTransition(() => {
setError(e.message)
setLoading(false)
})
}
}
ユーザー入力の setter(onChange → setValue)自体はラップしないでください。これは即座に反応する必要があります。
NPM パッケージのインポート [C/O]
標準のインポート(推奨):
import { Component } from "package-name"
Framer のキャッシュが更新されない場合は、CDN で特定のバージョンを指定します:
import { Component } from "https://esm.sh/[email protected]?external=react,react-dom"
React コンポーネントでは、必ず ?external=react,react-dom を含めてください。
CMS
オーバーライドでのコンテンツ取得タイミング [O]
CMS のテキストは、ハイドレーション後およそ 50~200 ms で props.text に非同期で渡されます。コンポーネントのプロパティにバインドされたテキストは、初回レンダリング時に同期的に渡されるため、遅延を考慮する必要はありません。
どちらにも使える信頼性の高いパターンは、resolvePlainText(props)(下記のオーバーライドでのテキストを参照)を使い、値が空でない場合にのみ表示する方法です。
const plainText = resolvePlainText(props)
// plainText is "" until content arrives → gate your animation on plainText.length > 0
任意の 100 ms の遅延は避けてください。読み込み時に要素がすでにビューポート内にある場合、競合状態が発生します。
Overrides 内のテキスト [O]
テキストは、設定方法に応じて2つの異なるソースから取得されます。
| ソース | 保存場所 | タイミング |
|---|---|---|
| 静的テキスト(Framer で入力) | props.children のネスト構造 | 初回レンダリング時に常に利用可能 |
| 変数にバインドされたテキスト(コンポーネントのプロパティ/CMS) | props.text(プレーンな文字列) | 変数の場合は初回レンダリング時に利用可能、CMS の場合は非同期 |
まず props.text を確認し、なければ children にフォールバックします。
import { isValidElement } from "react"
function extractParts(raw: any): any[] {
if (typeof raw === "string") return [raw]
if (isValidElement(raw)) return [raw]
if (Array.isArray(raw)) return raw.flatMap(extractParts)
return []
}
function toPlainText(parts: any[]): string {
return parts.map((p) => (typeof p === "string" ? p : "\n")).join("")
}
function resolvePlainText(props: any): string {
if (typeof props.text === "string" && props.text.length > 0) {
return props.text // variable-bound or CMS
}
const raw = props.children?.props?.children?.props?.children
return toPlainText(extractParts(raw)) // static text
}
テキストが props.children にしかないと思い込まないでください。 変数にバインドされたテキストは children の構造を経由しません。props.children にはプレースホルダーが含まれ、実際の値は props.text に入ります。children だけを読み取ると、変数テキストは見えません。
繰り返し項目をコンポーネントにしない [C]
Collection List の繰り返し項目全体をコンポーネントにしないでください。findByFramerName
ウォーカーは props.children を通じて React 要素ツリーを読み取りますが、コンポーネントインスタンスの内部ツリーはレンダリングされるまで存在しません。そのため、項目をコンポーネントでラップすると、名前付きレイヤーがすべて一度に隠れてしまいます(あるビルドでは背景、動画、移動するタイトルがすべて消え、項目が1つの背景レイヤーにまとまりました)。各名前付きフレームの内側にコンポーネントを配置するのは問題ありません。項目の最上位では、名前付きフレームをプレーンなフレームのままにしてください。
コードコンポーネント内の CMS [C]
コードコンポーネントは ControlType.ComponentInstance スロットを介して Framer CMS Collection List を受け取り、結果の React 要素ツリーをたどって各項目のコンテンツを抽出します。主なヘルパーは次のとおりです。
useQueryDataとgetCollectionDataで項目を実体化します。スロットのプロパティは配列なので、最初の要素を渡してくださいaddFieldsToSelectで、項目テンプレートにバインドされていないフィールドを読み取ります- CMS の日時は UTC で記録された現地時刻です。日付の読み取りには
getUTC*を使用します findByFramerNameで各項目のテンプレートから名前付きレイヤーを抽出します- 名前を検出する必要がある場合は、プレーンなフレームで動的コンポーネントをラップしてください
- ある CMS コンポーネントが別の CMS コンポーネントをラップする場合、
getPropertyControls(WrappedComponent)でコントロールを継承します
全体のパターン、ヘルパーコード、注意点については、references/cms.md を参照してください。
CMS データは公開されます [C/O]
公開済みページの HTML に含まれるのは、そのページがバインドしているフィールドだけですが、Framer はすべてのコレクションを .framercms データファイルとして framerusercontent.com/cms/... に公開し、すべての項目の全フィールドを保持します。追加読み込みのページネーションなど、実行時の読み取り処理でこれらのファイルが取得され、誰でもダウンロードできます。ページに表示されないフィールド(メールアドレスや内部メモなど)も公開状態であり、フィールドを削除した後も、以前のデプロイメントのファイルはオンラインに残ります。
- フォーム、プラグイン、サーバー API のいずれから送られたデータであっても、Framer CMS にプライベートデータを保存しないでください。
- CMS コンテンツの表示を制御する override は画面上で隠すだけです。データはファイルに残っています。
- プライベートデータやユーザーごとのデータは CMS ではなく、実行時に読み取る外部ストア(たとえば、行レベルセキュリティを備えた Supabase)に保存してください。
Overrides — 固有のパターン
バリアントの制御 [O]
バリアント名は props から読み取れません(ハッシュ化されている場合があります)。内部で管理してください。
export function withVariantControl(Component): ComponentType {
return (props) => {
const [currentVariant, setCurrentVariant] = useState("variant-1")
// Logic to change variant
setCurrentVariant("variant-2")
return <Component {...props} variant={currentVariant} />
}
}
Framer にアタッチされたハンドラーの呼び出し [O]
合成 DOM イベント(dispatchEvent)では Framer Motion のハンドラーが確実に呼び出されず、要素が押されたままの状態になることがあります。代わりに、DOM ノードからハンドラーを持つ fiber まで React fiber ツリーをたどり、ハンドラーを直接呼び出してください。
const onTap = findFiberHandler(wrapper, "onTap")
onTap?.({} as any, {} as any)
ヘルパー全体、デバッグ用スニペット、ディープリンクのユースケース、メンテナンス上のリスクについては、references/fiber-handlers.md を参照してください。
コンポーネントインスタンス:onClick、variant、スコープ付き CSS [O]
キャンバス上のコンポーネントインスタンスにオーバーライドを適用すると、コードから動作を制御できます。
export function withRefreshButton(Component): ComponentType {
return (props) => {
const [loading, setLoading] = useState(false)
return (
<Component
{...props}
variant={loading ? "Loading" : "Default"} // variant *name* from the canvas
onClick={() => run(setLoading)} // onClick, not onTap
aria-label="Refresh"
/>
)
}
}
onTapではなくonClickを使用してください。 どちらもインスタンスのルートに届きますが、onTapはポインタージェスチャーです。 フォーカスされた要素で Enter や Space を押した場合も、スクリプトから.click()を呼び出した場合も、イベントは発火しません。variantにはバリアント名を指定します。 これにより、コードで制御する状態(読み込み中、アクティブ)に、キャンバス上でデザインしたバリアントを再利用できます。- ループするアニメーションに連動する読み込み状態は、ループが一周したタイミングで終了させてください。 アイコンが1秒に1回転する場合、
max(1, ceil(elapsed / 1000))秒間 Loading バリアントを維持します。そうしないと、300 ms でリクエストが完了したときに、アイコンが回転の途中で元に戻ります。 - スタイルを制御できない組み込みコンポーネント(たとえば、ページネーションの「Load More」ボタン)は、
[data-framer-name="<instance name>"]を対象とする<style>を挿入することで、任意のオーバーライドからスタイルを変更できます。最初にキャンバス上でインスタンスに名前を付けてください。レイヤー名は公開後も維持されますが、生成された.framer-*クラスは維持されません。
Framer フォーム [O]
Webhook に送信するフォームを例に、ネイティブ Framer フォームの送信方法を説明します。
- ブラウザーはグローバルな
fetchを使ってapi.framer.com/forms/v1/forms/<id>/submitに送信し、プルーフ・オブ・ワークのヘッダーを追加します。フォームには、独自の非表示のハニーポット入力もレンダリングされます。 - Framer が送信内容を保存すると、Webhook やメールの送信先が実行される前に Success バリアントが表示されます。 ページでは Webhook が送信を受け付けたかどうかを把握できません。その情報が訪問者に必要な場合、Webhook が判定結果を公開する必要があります(オーバーライドがポーリングする簡単なステータスエンドポイントを用意します)。
- 送信が成功したことを検出するには、
window.fetchを一度ラップし、api.framer.com/forms/へのPOSTとres.okを照合します。リクエストボディには送信データが含まれるため、オーバーライドからフィールドの値を読み取れます。 - 計算値のフィールド(トークンや訪問者の IP アドレスなど)を追加する場合は、コードから挿入するのではなく、キャンバス上に実際の非表示入力(
formInputHiddenとformInputName)を配置し、フォームに対するキャプチャーフェーズのsubmitリスナーから値を設定します。このリスナーは、Framer のハンドラーがフォームを読み取る前に実行されます。
useEffect(() => {
const form = document.querySelector("form")
const input = form?.querySelector('input[name="ip"]') as HTMLInputElement | null
if (!form || !input) return
const fill = () => {
const set = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set
set?.call(input, valueRef.current) // native setter so React sees the change
}
form.addEventListener("submit", fill, true) // capture phase
return () => form.removeEventListener("submit", fill, true)
}, [])
DOM とパフォーマンス
スクロール検出の制約 [C/O]
Framer のスクロール検出には、ビューポートを基準とする IntersectionObserver が使われます。コンテナーに overflow: scroll を適用すると、この検出が機能しなくなります。
スクロールをトリガーとするアニメーションには、次を使用してください。
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && !hasEntered) {
setHasEntered(true)
}
})
},
{ threshold: 0.1 }
)
ライブ参照:useState ではなく useRef [C/O]
イベントごとに .current を読み取るフック(Framer Motion の useScroll、IntersectionObserver のターゲット、RAF ループなど)には、必ず useRef を渡してください。 ターゲットを useState に保存すると、最初のフック呼び出しで null が取得され、状態が切り替わっても再購読されません。
この問題は、たいてい一見すると 動作しているように見える ため、注意が必要です。useScroll は暗黙のうちにウィンドウのスクロールへフォールバックするため、id="..." を設定してターゲットを固定するまではページがアニメーションしているように見え、その後すべてが停止します。
// ❌ BROKEN — useScroll captures target: null on first render
const [scrollEl, setScrollEl] = useState<HTMLElement | null>(null)
useEffect(() => { setScrollEl(document.getElementById("section")) }, [])
const { scrollYProgress } = useScroll({ target: scrollEl })
// ✅ CORRECT — useScroll reads ref.current live on each event
const scrollRef = useRef<HTMLElement | null>(null)
useEffect(() => { scrollRef.current = document.getElementById("section") }, [])
const { scrollYProgress } = useScroll({ target: scrollRef })
これは、useScroll に限らず、イベントごとに参照ハンドルを通じて値を読み取る API に当てはまります。
フレームの子要素を計測する [O]
Framer は、フレームの各子要素(スタック内のコンポーネントインスタンスなど)を display: contents の要素でラップすることがあります。このラッパーにはボックスがないため、children[i] から幅、offsetLeft、offsetParent を取得できず、そこに設定した scroll-snap-align も無視されます。最初にボックスを持つ子孫までたどり、offsetLeft ではなく getBoundingClientRect() を使ってスクローラーを基準に計測してください。
Z-index のスタッキングコンテキストと React Portal [C/O]
問題: position: absolute を指定したコンポーネントは、親のスタッキングコンテキストを継承します。z-index: 9999 を指定しても、親の外にある要素より前面に表示することはできません。
解決策: React Portal を使って、document.body の階層にレンダリングします。
import { createPortal } from "react-dom"
export default function ComponentWithOverlay(props) {
const [showOverlay, setShowOverlay] = useState(false)
return (
<div style={{ position: "relative" }}>
{/* Main component content */}
{/* Overlay rendered outside parent hierarchy */}
{showOverlay && createPortal(
<div style={{
position: "fixed", // Fixed to viewport
inset: 0,
zIndex: 9999,
background: "rgba(0, 0, 0, 0.8)",
}}>
{/* Overlay content */}
</div>,
document.body
)}
</div>
)
}
主な違い:
position: "fixed"は親ではなく、ビューポートを基準に配置されます- Portal を使うと、コンポーネントの DOM 階層とスタッキングコンテキストから抜け出せます
- モーダル、ツールチップ、ポップオーバー、読み込みオーバーレイに使えます
Canvas と公開済みサイト: Portal は Canvas エディターと公開済みサイトの両方で機能します。RenderTarget のチェックは不要です。
公開済みページではオーバーライドからサイズを参照できない [O]
公開済みの Framer ページでは、2 種類のサイズ情報がオーバーライドの props に渡されません。
- フレームの高さはインラインスタイルではなく、生成された CSS クラス(
.framer-xxxxxxx { height: 100vh })として出力されるため、props.styleから読み取ったり書き換えたりできる高さはありません。代わりにインラインでheightを明示してください。インラインスタイルは生成されたクラスより優先されます。 - すべてのページルートの
<div data-framer-root>には、インラインのmin-height: 100vhが設定されています。これに優先させるには!importantが必要です。
スマートフォンでページを画面いっぱいの高さにする場合、どちらも重要です。100vh は大きいビューポートを指し、ブラウザーの URL バーが隠れた状態で測定されるため、どちらの指定も 100svh になるまでは、1 画面分のページでも数ピクセルスクロールします。最も簡単な修正方法は、サイト全体にカスタム CSS [data-framer-root] { min-height: 100svh !important } を適用し、さらにフレーム自体の高さも設定することです。両方をインラインで設定するオーバーライドは代替手段です(2026-08-19)。公開済みの要素を対象にするには、公開後も維持される [data-framer-name="…"] を使ってください。変化する生成済みの .framer-* クラスは使わないでください。
プログラムからページ間を移動できない [C/O]
公開済みの Framer サイトはシングルページアプリであり、別ページへのスクリプトによる移動はすべて、router.navigate()、アンカーに対する .click()、window.location.replace() のいずれであっても、同期が取れないルート変更として処理されます。URL は更新されますが、ページは読み込まれません。代わりに、その場で条件判定または連携を行ってください。localStorage トークンを書き込み、カスタムの window イベントを発火させ、現在のページのオーバーライドで応答させます。例外は、ページ全体の読み込み時に実行されるリダイレクトです。head スクリプト内、またはアプリの起動前に実行される場合、location.replace() は実際のページ遷移として機能します。
URL への状態の書き込み [C]
コンポーネントはクエリパラメーターを書き込むことで、Framer 標準の URL 連動フィルタリングを操作できます。読み取りとフィルタリングは Framer のフィルターが行います。コンポーネントはパラメーターを書き込み、同期を保つだけです。書き込み側には、直感に反するルールが 2 つあります。
- 頻繁に書き込む場合(スライダーのドラッグ、ライブフィルターなど)は、
pushStateではなくreplaceStateを使います。pushStateはイベントごとに履歴エントリーを追加し、戻るボタンで抜け出せなくなります。 history.replaceState/pushStateはpopstateを発火しません。 発火するのは、ブラウザー操作による移動(戻る/進む、URL の直接入力)のみです。書き込み後に自分でイベントを発火しないと、ページ内のほかの部分に変更が伝わりません。
const url = new URL(window.location.href)
url.searchParams.set(paramName, String(value))
window.history.replaceState({}, "", url.toString())
// replaceState is silent — fire popstate so URL-bound consumers re-sync
window.dispatchEvent(new PopStateEvent("popstate"))
コンポーネントは popstate もリッスンし、戻る/進む操作時にパラメーターを再読み込みして、自身の UI を更新します(ユーザーが編集できる値なので、範囲の制限や検証を行います)。SSR との整合性を保つため、初期状態には URL ではなく props を使い、URL の最初の読み取りは useEffect 内で行います。これはページ内のみの処理であり、ページ遷移は行わないため、ページ間移動に関する問題を回避できます。カスタムイベントによる状態バスと同じ考え方ですが、ブラウザー標準の popstate チャネルを使うため、Framer 独自の URL フィルターも追加の実装なしで変更を認識します。
ネイティブのレンジ入力のスタイル設定 [C]
Framer ではネイティブの <input type="range"> をスタイル設定できず、ブラウザーが独自の青いトラックとつまみを描画します。完全にスタイルを変更するには、次のようにします。
- 入力要素に
appearance: none(および-webkit-appearance: none)とbackground: transparentを設定します。 - 両方のエンジンでトラックを無効化します。
::-webkit-slider-runnable-trackと::-moz-range-trackを対象にします。 - 両方の
::-webkit-slider-thumbと::-moz-range-thumbでつまみを描画し直します。appearance:noneはネイティブのつまみも削除するため、エンジンごとに作り直します。 - 絶対位置指定の
<div>を下に配置して、表示するトラックと塗りつぶしを描画します。透明な入力要素をその上に重ねます。 <style>タグ内のセレクターは className(.rs-thumb)でスコープします。input[type=range]のように限定しないと、ページ上のすべてのレンジ入力に影響します。
デュアルつまみ(1つのトラックに入力要素を2つ重ねる場合): 重なった入力要素同士やトラックが互いを妨げないよう、各 入力要素 に pointer-events: none を設定し、つまみの疑似要素 のみに pointer-events: auto を設定します。これにより、つまみだけをドラッグできます。DOM ではなく、onChange ハンドラーで各つまみをもう一方の値 ± 1 ステップの範囲に制限し、交差しないようにします。
React が生成したノードの隣に DOM を挿入する [O]
オーバーライドで Framer が描画するコンテナ(たとえば Collection List)の内部に要素を追加する必要がある場合は、兄弟要素を追加し、React 自身のノードには手を加えないでください。React は自身が描画した子要素への参照を保持しており、後から移動、更新、削除します(ページネーションでページが追加される場合や、タブの切り替えでリストがアンマウントされる場合など)。子要素を置き換えたり、別の親要素に移したりすると、次の再調整で問題が発生します。一方、React が描画した子要素の 間 に配置した外部ノードは、React が自身のノードだけを操作するため、そのまま残ります。
reactNode.before(mine)/after(mine)で挿入し、独自の要素にdata-mine="<key>"を付けて、検索、スキップ、再配置ができるようにします。MutationObserverでコンテナを監視し、React が再マウントまたは追加を行ったときに独自のノードを再配置します。配置は冪等にしてください。コンテナにすでに独自のノードだけが表示されている場合は、何もしません。そうしないと、無関係な再レンダリングのたびにノードが削除・再挿入され、アニメーションが再開してしまいます。- 独自の変更を行った直後に
observer.takeRecords()を呼び出し、それらが再度処理されないようにします。 - React のノードを、React も管理しているインラインスタイルで非表示にしないでください。React がそのスタイルを書き換える可能性があります。何かを非表示にする必要がある場合は、属性と挿入したスタイルシートのルールを使用します。
公開済みの CMS コンテンツを再読み込みせずに更新する [O]
Framer サイトは静的なデプロイです。新しい CMS アイテムは、ページの次の公開バージョンでのみ存在し、実行中のページがその追加を知ることはありません。再読み込みせずに表示するには、次の手順を実行します。
- 現在のページを再度取得します(
fetch(location.pathname + "?x=" + Date.now(), { cache: "no-store" }))。Framer はページをmax-age=0で配信するため、最新のデプロイが返されます。 DOMParserで解析し、[data-framer-name="..."]を使ってリストの描画済みアイテムを読み取ります。サーバーでレンダリングされた HTML には Collection List の最初のページ(キャンバス上で指定された並べ替え順。URL のクエリにかかわらず)が含まれており、各日付フィールドには<time datetime>があるため、アイテムのキー設定と並べ替えができます。- ページの読み込み時の内容と比較し、新しいアイテムだけを挿入します(上記を参照)。
フォーム送信後、公開には 5~15 秒かかることがあります。時間制限を設けて数秒おきにポーリングし、待機中であることが訪問者に見える状態にします。
よく使われるパターン
共有状態、キーボード検出、一度だけ表示するロジック、スクロール効果、磁石のように吸い付くホバー、アニメーションのトリガー、モバイル最適化、Safari の SVG 修正、読み込み中のスクロールロック、lerp アニメーションによるイージングカーブについては、references/patterns.md を参照してください。
メディア
HLS ビデオストリーミング(.m3u8)[C]
Chrome/Firefox は HLS をネイティブではサポートしていません。単純な <video src="...m3u8"> では失敗するか、最低画質のまま再生されます(Safari はネイティブで処理します)。解決策:動的インポートで HLS.js を読み込み、サイレントフォールバックでネイティブビデオを使用します。
loadHls/attachHls の実装全体とクリーンアップに関する注意点については、references/hls-video.md を参照してください。
Framer での WebGL [C]
透明度の処理を含むシェーダーの実装パターンについては、references/webgl-shaders.md を参照してください。
デバッグ
デバッグログ [C/O]
本番ビルドでデータや不要な出力が漏れないよう、コンポーネント内のすべての console.log はモジュールレベルのブール値で制御してください。console.log を直接あちこちに追加しないでください。後から無効にする際に、すべて探し回ることになります。
const debugMode = false // flip to true when debugging this component
if (debugMode) console.log("Honeypot active, fields:", values)
ユーザー入力(フォームの値)、認証状態、サードパーティーのトークンを扱うコンポーネントでは特に重要です。そうしないと、これらが本番環境のコンソールログに出力されてしまいます。