framer-plugins
Installation
SKILL.md
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 // Must be unique, max 64 characters
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
- marketplace.md — Marketplace submission workflow, listing requirements, review process, plugin policies, and post-publication obligations
Key Rules
- Always check the project's
CLAUDE.mdfor project-specific overrides and decisions - Before building any new feature, check 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
- 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 - Slugs must be unique and max 64 characters — append a unique ID suffix to title-based slugs
- Use
userEditable: trueon field definitions for fields users edit manually in the CMS - Never include user-editable fields in
fieldDataduring upsert — omitting them preserves user values - Never remove-all + re-add during sync — only remove items no longer in the source to preserve user data
ManagedCollectionhas nogetItems()— you can only read item IDs, not field data