Find a file
Danila Poyarkov 7ad6475e2b
feat: create, fill, and edit slots (#862)
* feat(app): add slot property controls and a shared picker

AppPicker is a searchable, grouped list that opens beside the properties
panel, built on Reka's popover and listbox so search keeps arrow-key
navigation. It has comfortable rows with a thumbnail and description and
compact rows for plain names, plus an optional footer action.

The slot property row shows whether an instance's slot is Default or
Modified, its item count, its limits with a checklist popover, Add
instances on AppPicker, and Reset slot and Delete contents. These are
presentational; wiring them to the editor follows. Strings are English
only until the locale files catch up.

* feat(app): open variable, style, and instance-swap choices in the shared picker

The variable binding picker, the shared style fields, and instance-swap
properties now open AppPicker: a titled panel beside the properties panel
with search that keeps arrow-key navigation, a check on the current
choice, and footer actions. Variable binding keeps its detach and
create-variable actions. Instance-swap choices list the property's
preferred components first; instanceSwapOptions keeps the preferred flag
it already computed. AppPickerField gives select-shaped fields the same
picker with a combobox trigger.

* feat(core): edit instance slot content

Only an instance's slots take layers now. slotScope classifies a parent as
free, a slot of an instance, or the locked rest of an instance. Moves,
reorders, layer-panel drops, paste, duplicate, and instance creation
claim an untouched slot first, as Figma does on the first edit, and
refuse the locked part; drops over it land in the instance's parent.
Claiming keeps the layers but unlinks them from the component and moves
the instance's overrides on nested instances onto those instances.

Reset slot, Delete contents, and Add instance are editor actions, each
one undo step that restores the instance's subtree. A canvas drop or
reorder that claims a slot undoes together with the move.

* feat(app): show and edit slot properties of the selected instance

The component properties section lists each slot of the selected instance
with its state, item count and limits, and adds instances, resets or clears
its content through the editor's slot actions. The slot model lives in the
Vue SDK as useSlotProperties.

* feat(app): outline slots on the canvas and mark them in the layers panel

Hovering or selecting a component, an instance, or a layer inside a slot
draws its slots with a dashed pink outline and tints empty ones. Slot
frames are selected and hovered in pink and show a dashed-square icon in
the layers panel.

* test(app): cover slot outlines with canvas snapshots

* feat(app): translate slot and picker strings

* docs: note slot editing and the shared picker

* refactor: share slot test and story setup

* refactor(app): name the picker's header prop heading

* feat(core): create, configure, and remove slots on main components

A frame of a main component becomes a slot through a SLOT property
named after it; other sibling layers are first wrapped in an auto
layout frame. Slot settings and removal are single undo steps.
Instances now follow the component's property bindings on sync, so a
slot created or removed on the component reaches existing instances.
Slot helpers move to a slots domain folder in Scene Graph and Core.

* feat(app): create and configure slots from the menu and properties panel

Create slot joins the canvas context menu for layers of a main
component. A Slots section on main components and their frames
renames slots, sets their description, layer limits, and preferred
components, and removes them.

* fix: keep slot claims in the same undo step and refuse wraps inside instances

Creating an instance and pasting HTML now batch the slot claim with the
edit. Undoing an added slot instance restores the previous selection.
Grouping or wrapping layers in the locked part of an instance is
refused, and a section that cannot move no longer claims a slot. The
picker clears its search however it closes, and its close and slot
actions labels are translated.

* feat(core): create and inspect slots through the plugin API

component.createSlot() adds a 100x100 frame named Slot, Slot 2, and so
on, bound to a new SLOT property, as live Figma does. Slot frames read
type SLOT, resetSlot() restores an instance slot's component content,
and limitViolations reports BELOW_MIN, ABOVE_MAX, and
HAS_NON_PREFERRED for instance slots. addComponentProperty and
editComponentProperty take a description and slotSettings, a cloned
slot is a plain frame, and componentPropertyReferences uses property
keys in both directions. The limit checks move to Scene Graph so the
Vue SDK and the plugin API share them.

* fix: keep nested slot content across swaps and read nested instance properties

A nested instance points at the instance it was cloned from, so its
component properties resolved to nothing. Properties now resolve through
those links to the main component, and a swap or variant switch parks
the slot content nested instances own and restores it into nested
instances of the same names, as live Figma does. Plugin appendChild and
insertChild claim the slot they add to and refuse the locked part of an
instance with Figma's error.

* test(core): record resetSlot on a main component slot; fix the Slots roadmap row

* fix(core): refuse deleting layers outside an instance's slots

As in Figma, delete and plugin remove() leave an instance's own layers
and its slot frames alone, while removing slot content claims the slot
in the same undo step as the delete.

* test(app): wait for the bulk rename dialog to close before the next shortcut

* feat(app): unify add, remove, and settings controls in the properties panel

A section's + adds an item and a row's - removes it everywhere: grid
tracks and variant properties now follow the fill and effect lists, and
the header + of a component set adds Property 1 ready to rename instead
of an inline form. Removing a variant is Delete, as for any layer.
Row settings share the sliders icon, the variables button opens with
its own icon, every icon button requires a label, and the instance
header buttons use sentence case. Create Slot joins the app menu.

* docs: note the unified properties panel controls

* feat(app): animate floating panels alike and share the severity icon

Popovers, menus, selects, comboboxes, and pickers now fade and grow in
from the side they open on and fade out on close, from one motion
preset that respects reduced motion; tooltips keep the tooltip preset.
SeverityIcon and its colours move from the design check to shared
feedback UI, and slot limits use them, so a broken limit reads like a
design-check warning.

* docs: note consistent popover and menu animation

* feat(app): give every floating panel one surface and close it at once

Popovers, menus, selects, comboboxes, pickers, presence cards, chat
history, and the issue tooltip share one rounded surface with a 1px
ring that outlines it in both themes; one-line tooltips keep a compact
shape with the same edge. The select theme's radius and elevation
options and local shadow overrides are gone. Panels still grow in from
their side but now close immediately: a fading modal menu kept blocking
the canvas and shortcuts until it unmounted.

* fix(app): keep a reopened context menu open and name option-drag undo Duplicate

Closing the canvas menu hands focus back to the canvas; when that
landed just after a quick reopen, the new menu closed as focus moved
outside it. Shortcuts no longer wait for a panel that is already
closing, and an option-drag duplicate that claims a slot is undone as
Duplicate rather than Move.

* test(app): wait for menus and popovers to close before the next key
2026-10-04 17:02:28 +00:00
.claude ci: keep AI disclosure out of co-author credits 2026-09-15 23:12:44 +03:00
.devcontainer refactor: rename @open-pencil/codegen to @open-pencil/emit (#822) 2026-10-04 11:22:20 +00:00
.github ci: run CI and the PR title check for the merge queue (#867) 2026-10-04 13:51:51 +04:00
.storybook fix(vue): start CanvasKit once CanvasSurface provides the canvas (#823) 2026-10-02 00:23:49 +04:00
.vscode
assets/brand build(tools): group tools by role and gate them like the rest of the repo (#791) 2026-09-30 05:00:31 +04:00
desktop feat: create, fill, and edit slots (#862) 2026-10-04 17:02:28 +00:00
lint build(tools): group tools by role and gate them like the rest of the repo (#791) 2026-09-30 05:00:31 +04:00
packages feat: create, fill, and edit slots (#862) 2026-10-04 17:02:28 +00:00
public feat: refresh branding with generated platform icons (#707) 2026-09-16 11:54:07 +03:00
skills/open-pencil feat: control documents, history, settings, and tools from the CLI and MCP (#871) 2026-10-04 16:02:36 +00:00
src feat: create, fill, and edit slots (#862) 2026-10-04 17:02:28 +00:00
tests feat: create, fill, and edit slots (#862) 2026-10-04 17:02:28 +00:00
tools feat: create, fill, and edit slots (#862) 2026-10-04 17:02:28 +00:00
vite feat(lint): suggest group-to-frame and hidden-layer fixes; keep canvas edits in code previews (#870) 2026-10-04 12:46:06 +00:00
.coderabbit.yaml chore: hide review status chatter and test generation prompts 2026-09-15 21:43:04 +03:00
.gitattributes Fix CJK text garbled when font unavailable (#89) 2026-03-10 16:58:57 +03:00
.gitignore build(tools): group tools by role and gate them like the rest of the repo (#791) 2026-09-30 05:00:31 +04:00
.gitleaks.toml chore(tools): add secret scanning gate 2026-07-01 15:24:06 +03:00
.lfsconfig ci: move Git LFS to provider-neutral gateway 2026-08-01 17:52:08 +03:00
.oxfmtrc.json style: tighten import grouping 2026-05-06 02:22:08 +03:00
AGENTS.md fix: validate parsed JSON at untrusted boundaries with Valibot (#855) 2026-10-04 17:01:50 +00:00
bun.lock feat: control documents, history, settings, and tools from the CLI and MCP (#871) 2026-10-04 16:02:36 +00:00
bunfig.toml refactor(mcp): align transport domain structure 2026-07-25 22:59:59 +03:00
CHANGELOG.md feat: create, fill, and edit slots (#862) 2026-10-04 17:02:28 +00:00
commitlint.config.ts build(tools): group tools by role and gate them like the rest of the repo (#791) 2026-09-30 05:00:31 +04:00
CONTRIBUTING.md docs: describe rebasing and merging stacked pull requests (#869) 2026-10-04 10:17:30 +00:00
index.html fix: explain unsupported browsers instead of a blank window (#745) 2026-09-22 14:40:59 +04:00
knip.json fix(text): finalize font readiness and label shaping (#593) 2026-08-30 14:33:32 +03:00
LICENSE docs: acknowledge OpenPencil contributors in license 2026-08-31 09:11:10 +03:00
oxlint.json fix: validate parsed JSON at untrusted boundaries with Valibot (#855) 2026-10-04 17:01:50 +00:00
package.json build: update dependencies (#873) 2026-10-04 12:48:24 +00:00
playwright.config.ts chore: merge master into live-editor-regressions 2026-09-16 00:14:31 +03:00
portless.json chore: add Portless development URLs 2026-08-20 08:15:54 +03:00
README.md docs: trim the README CLI section and align its examples (#885) 2026-10-04 16:38:28 +00:00
SECURITY.md docs: document private security reporting 2026-05-17 12:52:10 +03:00
steiger.config.ts build(tools): group tools by role and gate them like the rest of the repo (#791) 2026-09-30 05:00:31 +04:00
tsconfig.json refactor: rename @open-pencil/codegen to @open-pencil/emit (#822) 2026-10-04 11:22:20 +00:00
tsconfig.node.json ci: validate PR commits and streamline package verification 2026-09-15 20:51:33 +03:00
vite.config.ts feat: preview streamed JSX on the canvas (#692) 2026-10-01 10:52:36 +04:00
wdio.conf.ts test(native): cover the desktop MCP server lifecycle (#723) 2026-09-18 11:42:55 +03:00

OpenPencil

MIT license npm Discord GitHub Discussions

Open-source design editor. Opens .fig and .pen design files, includes built-in AI, and ships as a programmable toolkit with a headless Vue SDK for building custom editors.

Status: Active development. Usable today, with some rough edges as features evolve.

Try it online → · Download · Documentation · Roadmap · llms.txt

OpenPencil

Installation

macOS (Homebrew):

brew install --cask openpencil

Or download from the releases page, or use the web app — no install needed.

Requires macOS 13 or later with current Safari updates, Windows 10 or later, or Linux with WebKitGTK 2.40+; the web app needs Chrome 111, Edge 111, Firefox 128, or Safari 16.4 or later. See system requirements.

What it does

  • Opens .fig and .pen files — read and write native Figma files, open supported Pencil documents from the app or OS file browser, copy & paste nodes between apps
  • AI builds designs — describe what you want in chat, 100+ tools create and modify nodes. Connect OpenRouter, Anthropic, OpenAI, Google AI, DeepSeek, Z.ai, MiniMax, or compatible endpoints
  • Fully programmable — headless CLI, XPath queries, Figma Plugin API via eval, MCP server for AI agents, and desktop agent integrations for Claude Code, Codex, and Gemini CLI
  • Lint, convert, and extract tokens — inspect documents, lint naming/layout/accessibility, convert between supported formats, analyze colors/typography/spacing/clusters, and extract design tokens
  • Components and variants — create reusable components, group variants into component sets, insert local assets as instances, and switch variants from the inspector
  • Image vectorization — convert image layers into editable vector layers with Recraft or fal.ai
  • Design-to-code export — export selections as JSX/Tailwind, generate token outputs, and map designs into component-oriented code workflows
  • Vue SDK for custom editors — headless components and composables for embedding OpenPencil into other apps or building workflow-specific editing surfaces. Read the SDK docs →
  • Real-time collaboration — P2P via WebRTC, no server, no account. Cursors, presence, follow mode
  • Auto layout & CSS Grid — flex and grid layout via Yoga WASM, with gap, padding, alignment, track sizing
  • ~15 MB desktop app — Tauri v2 for macOS, Windows, Linux. Also runs in the browser as a PWA

CLI

npm install -g @open-pencil/cli
# or: bun add -g @open-pencil/cli

Inspect design files

Browse node trees, search by name or type, dig into properties — all without opening the editor:

openpencil tree design.fig
openpencil find design.pen --type TEXT
openpencil node design.fig --id 1:23
openpencil info design.fig
[0] [page] "Getting started" (0:46566)
  [0] [section] "" (0:46567)
    [0] [frame] "Body" (0:46568)
      [0] [frame] "Introduction" (0:46569)
        [0] [frame] "Introduction Card" (0:46570)
          [0] [frame] "Guidance" (0:46571)

Query with XPath

Find nodes by type, attributes, and structure:

openpencil query design.fig "//FRAME[@width < 300]"  # Frames under 300px
openpencil query design.fig "//SECTION//TEXT"        # Text inside sections

Export, convert, and import

Render to PNG, JPG, WEBP, SVG, PDF, PPTX, HTML, JSX, Storybook stories, or .fig, convert between document formats, and turn HTML/CSS into editable layers:

openpencil export design.fig                  # PNG
openpencil export design.fig -f jpg -s 2      # JPG at 2x
openpencil export design.fig -f tailwind-jsx  # Tailwind JSX
openpencil export design.fig -f storybook     # Storybook stories per component
openpencil convert design.pen design.fig      # Between document formats
openpencil import card.html --css card.css    # HTML/CSS → editable .fig

Lint and analyze

Catch naming, layout, and accessibility issues, and audit a design system's real palette, type scale, spacing, and repeated components:

openpencil lint design.fig --preset strict  # Naming, layout, accessibility
openpencil analyze colors design.fig        # Also typography, spacing, clusters
openpencil variables design.fig             # Variables and collections
#1d1b20  ██████████████████████████████ 17155×
#49454f  ██████████████████████████████ 9814×
#ffffff  ██████████████████████████████ 8620×
#6750a4  ██████████████████████████████ 3967×

Script with the Figma Plugin API

eval runs JavaScript against the document with Figma's Plugin API; -w writes the result back:

openpencil eval design.fig -c "figma.currentPage.selection.forEach(n => n.opacity = 0.5)" -w

Control the running app

Omit the file argument and the CLI works on the document open in the editor:

openpencil tree                                 # Inspect the live document
openpencil documents list                       # Also open, new, save, close, activate
openpencil tool call get_selection              # Run any MCP tool
openpencil undo                                 # Undo the newest automation change
openpencil settings set appearance.theme light  # Change editor settings

Every command supports --json. See the CLI reference for all commands and options.

AI & MCP

Built-in chat

Press ⌘J (CtrlJ on Windows and Linux) to open the AI assistant. It has 100+ tools that can create shapes, set fills and strokes, manage auto-layout, work with components and variables, run boolean operations, analyze design tokens, and export assets. Bring your own API key for OpenRouter, Anthropic, OpenAI, Google AI, DeepSeek, Z.ai, MiniMax, or compatible endpoints. No backend, no account.

Not every provider works in the browser, and not every model streams tool calls correctly. See BYOK provider & model compatibility for measured results — contributions welcome.

Coding agents (desktop)

Use Claude Code, Codex, or Gemini CLI directly in the chat panel. The agent connects to the editor's MCP server and uses all 100+ design tools. Requires the desktop app and the agent CLI installed locally.

Pi works too: install @open-pencil/harness globally, then add a Pi model profile in Settings → AI & agents.

Setup (Claude Code):

  1. Install the ACP adapter: npm install -g @agentclientprotocol/claude-agent-acp
  2. Add MCP permission to ~/.claude/settings.json:
    {
      "permissions": {
        "allow": ["mcp__open-pencil__*"]
      }
    }
    
  3. Open the desktop app → ⌘J → select Claude Code from the provider dropdown

MCP server

Connect Claude Code, Cursor, Windsurf, or any MCP client to inspect, modify, and export design documents headlessly. 100+ tools. Full docs →

Stdio (Claude Code, Cursor, Windsurf):

npm install -g @open-pencil/mcp
claude mcp add --scope user open-pencil -- openpencil-mcp

For other MCP clients:

{
  "mcpServers": {
    "open-pencil": {
      "command": "openpencil-mcp"
    }
  }
}

HTTP (scripts, CI):

openpencil-mcp-http   # Unix socket on macOS/Linux + http://127.0.0.1:7600/mcp

Set OPENPENCIL_MCP_ROOT to limit file access to one directory; it defaults to the current working directory.

AI agent skill

Teach your AI coding agent to use OpenPencil — inspect designs, export assets, analyze tokens, modify .fig files:

npx skills add open-pencil/open-pencil

Works with Claude Code, Cursor, Windsurf, Codex, and any agent that supports skills.

For documentation-aware agents, the docs site publishes llms.txt, llms-full.txt, and per-page Markdown files generated from the VitePress docs.

Collaboration

Share a link to co-edit in real time. No server, no account — peers connect directly via WebRTC.

  1. Click the share button in the top-right panel
  2. Share the generated link (app.openpencil.dev/share/<room-id>)
  3. Collaborators see your cursor, selection, and edits in real time
  4. Click a peer's avatar to follow their viewport

Why

Figma is a closed platform that actively fights programmatic access. Their MCP server is read-only. figma-use added full read/write automation via CDP — then Figma 126 killed CDP. Your design files are in a proprietary binary format that only their software can fully read. Your workflows break when they decide to ship a point release.

OpenPencil is the alternative: open source (MIT), reads .fig files natively, every operation is scriptable, and your data never leaves your machine.

See the roadmap for product direction and current Figma compatibility gaps.

Community

Contributing

bun install
bun run dev:portless  # Web editor at https://open-pencil.localhost
bun run tauri dev     # Desktop app (requires Rust)

CONTRIBUTING.md covers setup, quality gates, pull requests, and commits. AGENTS.md maps the repository and links the guide inside each package. Desktop builds need Rust and the Tauri v2 prerequisites; run bun run tauri build.

Tech stack

Layer Tech
Rendering Skia (CanvasKit WASM)
Layout Yoga WASM (flex + grid via fork)
UI Vue 3, Reka UI, Tailwind CSS 4
File format Kiwi binary + Zstd + ZIP
Collaboration Trystero (WebRTC P2P) + Yjs (CRDT)
Desktop Tauri v2
AI/MCP Vercel AI SDK (multi-provider BYOK), MCP SDK, Hono

Acknowledgments

Thanks to @sld0Ant (Anton Soldatov) for creating and maintaining the documentation site.

License

OpenPencil is licensed under the MIT License.

Copyright (c) 2026 Danila Poyarkov and OpenPencil contributors.