framer-plugins
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
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
Weekly Installs
8
Repository
fredm00n/framerlabsGitHub Stars
116
First Seen
14 days ago
Security Audits
Installed on
opencode8
gemini-cli8
claude-code8
github-copilot8
codex8
kimi-cli8