--- name: framer-plugins description: > Framer Plugin SDK expert. Use when building, debugging, or modifying Framer plugins. Covers ManagedCollection API, CMS sync, plugin modes, UI patterns, permissions, data storage, and common pitfalls. user-invocable: true license: MIT metadata: author: fredm00n version: 1.0.0 --- # Framer Plugin Development Guide You are an expert on the Framer Plugin SDK. Use this reference when building, debugging, or modifying Framer plugins. Always check the project's CLAUDE.md for project-specific overrides. ## Quick Reference - **SDK package**: `framer-plugin` (v3.6+) - **Scaffolding**: `npm create framer-plugin@latest` - **Build**: Vite + `vite-plugin-framer` - **Base styles**: `import "framer-plugin/framer.css"` - **Core import**: `import { framer } from "framer-plugin"` - **Dev workflow**: `npm run dev` → Framer → Developer Tools → Development Plugin ## framer.json Every plugin needs a `framer.json` at the project root: ```json { "id": "6bbb4f", "name": "My Plugin", "modes": ["configureManagedCollection", "syncManagedCollection"], "icon": "/icon.svg" } ``` - `id` — unique hex identifier (auto-generated by scaffolding) - `modes` — array of supported modes (see below) - `icon` — 30×30 SVG/PNG in `public/`. SVGs need careful centering. ## Plugin Modes | Mode | Purpose | `framer.mode` value | |------|---------|---------------------| | `canvas` | General-purpose canvas access | `"canvas"` | | `configureManagedCollection` | CMS: first-time setup / field config | `"configureManagedCollection"` | | `syncManagedCollection` | CMS: re-sync existing collection | `"syncManagedCollection"` | | `image` | User picks an image | `"image"` | | `editImage` | Edit existing image | `"editImage"` | | `collection` | Access user-editable collections | `"collection"` | CMS plugins use both `configureManagedCollection` + `syncManagedCollection`. ## Core framer API ### UI Management ```typescript framer.showUI({ position?, width, height, minWidth?, minHeight?, maxWidth?, resizable? }) framer.hideUI() framer.closePlugin(message?, { variant: "success" | "error" | "info" }) // returns never framer.notify(message, { variant?, durationMs?, button?: { text, onClick } }) framer.setCloseWarning(message | false) // warn before closing during sync framer.setBackgroundMessage(message) // status while plugin runs hidden framer.setMenu([{ label, onAction, visible? }, { type: "separator" }]) ``` - `closePlugin` throws `FramerPluginClosedError` internally — always ignore in catch blocks - `showUI` should be called in `useLayoutEffect` to avoid flicker ### Properties - `framer.mode` — current mode string ### Collection Access ```typescript framer.getActiveManagedCollection() // → Promise framer.getActiveCollection() // → Promise (unmanaged) framer.getManagedCollections() // → Promise framer.getCollections() // → Promise framer.createManagedCollection() // → Promise ``` ### Canvas Methods (canvas mode) ```typescript framer.addImage({ image, name, altText }) framer.setImage({ image, name, altText }) framer.getImage() framer.addText(text) framer.addFrame() framer.addSVG(svg, name) // max 10kB framer.addComponentInstance({ url, attributes? }) framer.getSelection() framer.subscribeToSelection(callback) ``` ## ManagedCollection API ```typescript interface ManagedCollection { id: string getItemIds(): Promise setItemOrder(ids: string[]): Promise getFields(): Promise setFields(fields: ManagedCollectionFieldInput[]): Promise addItems(items: ManagedCollectionItemInput[]): Promise // upsert! removeItems(ids: string[]): Promise setPluginData(key: string, value: string | null): Promise getPluginData(key: string): Promise } ``` **Critical**: `addItems()` is an **upsert** — it adds new items and updates existing ones matched by `id`. ### Field Types ``` "boolean" | "color" | "number" | "string" | "formattedText" | "image" | "file" | "link" | "date" | "enum" | "collectionReference" | "multiCollectionReference" | "array" ``` ### Field Definition ```typescript interface ManagedCollectionFieldInput { id: string name: string type: CollectionFieldType userEditable?: boolean // default false for managed cases?: { id, name }[] // for "enum" collectionId?: string // for collection references fields?: ManagedCollectionFieldInput[] // for "array" (gallery) } ``` ### Item Structure ```typescript interface ManagedCollectionItemInput { id: string slug: string // Must be unique, max 64 characters draft: boolean fieldData: Record } ``` ### Field Data Values — MUST specify type explicitly ```typescript { type: "string", value: "hello" } { type: "number", value: 42 } { type: "boolean", value: true } { type: "date", value: "2024-01-01T00:00:00Z" } // ISO 8601 { type: "link", value: "https://example.com" } { type: "image", value: "https://img.url" | null } { type: "file", value: "https://file.url" | null } { type: "color", value: "#FF0000" | null } { type: "formattedText", value: "

hello

", contentType: "html" } { type: "enum", value: "case-id" } { type: "collectionReference", value: "item-id" } { type: "multiCollectionReference", value: ["id1", "id2"] } { type: "array", value: [{ id: "1", fieldData: { ... } }] } ``` ## Permissions ```typescript import { framer, useIsAllowedTo, type ProtectedMethod } from "framer-plugin" // Imperative check framer.isAllowedTo("ManagedCollection.addItems", "ManagedCollection.removeItems") // React hook (reactive) const canSync = useIsAllowedTo("ManagedCollection.addItems", "ManagedCollection.removeItems") // Standard CMS sync permissions const SYNC_METHODS = [ "ManagedCollection.setFields", "ManagedCollection.addItems", "ManagedCollection.removeItems", "ManagedCollection.setPluginData", ] as const satisfies ProtectedMethod[] ``` ## Data Storage Decision Tree | Need | Use | Why | |------|-----|-----| | API keys, auth tokens | `localStorage` | Per-user, no size warnings, not shared | | User preferences | `localStorage` | Per-user, synchronous | | Data source ID, last sync time | `collection.setPluginData()` | Shared across collaborators, tied to collection | | Project-level config | `framer.setPluginData()` | Shared, but 4kB total limit | - `pluginData`: 2kB per entry, 4kB total. Strings only. Pass `null` to delete. - `localStorage`: Sandboxed per-plugin origin. No size warnings. - `setPluginData()` triggers "Invoking protected message type" toast (SDK bug). ## Key Exports from "framer-plugin" ```typescript import { framer, useIsAllowedTo, FramerPluginClosedError } from "framer-plugin" import type { ManagedCollection, ManagedCollectionField, ManagedCollectionFieldInput, ManagedCollectionItemInput, FieldDataInput, FieldDataEntryInput, ProtectedMethod, Collection, CollectionItem } from "framer-plugin" import "framer-plugin/framer.css" ``` ## Supporting References For deeper information, see the companion files in this skill directory: - **[api-reference.md](references/api-reference.md)** — Complete API signatures and type definitions - **[patterns.md](references/patterns.md)** — Common plugin patterns extracted from 32 official examples - **[pitfalls.md](references/pitfalls.md)** — Known gotchas, workarounds, and debugging tips - **[marketplace.md](references/marketplace.md)** — Marketplace submission workflow, listing requirements, review process, plugin policies, and post-publication obligations ## Key Rules 1. Always check the project's `CLAUDE.md` for project-specific overrides and decisions 16. **Before building any new feature**, check [marketplace.md](references/marketplace.md) — the plugin must comply with Framer's policies (English UI, light+dark mode, no ads, USD-only pricing, IP ownership, etc.) or it will be rejected during the ~3-week review process 2. CMS plugins should attempt silent sync in `syncManagedCollection` mode before showing UI 3. `addItems()` is upsert — no need to check for existing items before adding 4. Field data values MUST include explicit `type` property: `{ type: "string", value: "..." }` 5. Use `localStorage` for sensitive/user-specific data, `pluginData` for shared sync state 6. Import `"framer-plugin/framer.css"` for standard Framer plugin styling 7. Use `
` instead of `