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:
{
"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 inpublic/. 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
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" }])
closePluginthrowsFramerPluginClosedErrorinternally — always ignore in catch blocksshowUIshould be called inuseLayoutEffectto avoid flicker
Properties
framer.mode— current mode string
Collection Access
framer.getActiveManagedCollection() // → Promise<ManagedCollection>
framer.getActiveCollection() // → Promise<Collection> (unmanaged)
framer.getManagedCollections() // → Promise<ManagedCollection[]>
framer.getCollections() // → Promise<Collection[]>
framer.createManagedCollection() // → Promise<ManagedCollection>
Canvas Methods (canvas mode)
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
interface ManagedCollection {
id: string
getItemIds(): Promise<string[]>
setItemOrder(ids: string[]): Promise<void>
getFields(): Promise<ManagedCollectionField[]>
setFields(fields: ManagedCollectionFieldInput[]): Promise<void>
addItems(items: ManagedCollectionItemInput[]): Promise<void> // upsert!
removeItems(ids: string[]): Promise<void>
setPluginData(key: string, value: string | null): Promise<void>
getPluginData(key: string): Promise<string | null>
}
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
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
interface ManagedCollectionItemInput {
id: string
slug: string
draft: boolean
fieldData: Record<string, FieldDataEntryInput>
}
Field Data Values — MUST specify type explicitly
{ 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: "<p>hello</p>", contentType: "html" }
{ type: "enum", value: "case-id" }
{ type: "collectionReference", value: "item-id" }
{ type: "multiCollectionReference", value: ["id1", "id2"] }
{ type: "array", value: [{ id: "1", fieldData: { ... } }] }
Permissions
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. Passnullto delete.localStorage: Sandboxed per-plugin origin. No size warnings.setPluginData()triggers "Invoking protected message type" toast (SDK bug).
Key Exports from "framer-plugin"
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 — Complete API signatures and type definitions
- patterns.md — Common plugin patterns extracted from 32 official examples
- pitfalls.md — Known gotchas, workarounds, and debugging tips
Key Rules
- Always check the project's
CLAUDE.mdfor project-specific overrides and decisions - CMS plugins should attempt silent sync in
syncManagedCollectionmode before showing UI addItems()is upsert — no need to check for existing items before adding- Field data values MUST include explicit
typeproperty:{ type: "string", value: "..." } - Use
localStoragefor sensitive/user-specific data,pluginDatafor shared sync state - Import
"framer-plugin/framer.css"for standard Framer plugin styling - Use
<div role="button">instead of<button>to avoid Framer's CSS overrides - Handle
FramerPluginClosedErrorin catch blocks — ignore it silently - Call
showUIinuseLayoutEffectto avoid flicker when resizing - Always check permissions with
framer.isAllowedTo()before sync operations