Back to blog
June 20, 2026·7 min read

What I learned building my first Figma plugin with AI

FigmaPluginClaude CodeIADesignOps
What I learned building my first Figma plugin with AI 1

This isn't an article about how AI built a plugin for me. It's actually the opposite: it's about how an apparently simple problem in Figma forced me to lay down rules, make product decisions, and understand the real environment where a tool has to work.

I've been using Figma for years now. And for years I've seen the same pattern repeat in large files: dozens, sometimes hundreds of frames, duplicate versions, screens that started as experiments and ended up mixed in with the main flow.

The problem isn't just disorder. It's the loss of trust. When a team doesn't know which screen is current, the file stops being a source of truth and becomes a kind of visual archaeology. Duplicate frames affect handoff, visual consistency, decision traceability, and team confidence in their own deliverables. Detecting them isn't just cleaning up Figma — it's reducing operational noise.

The manual solution is tedious: go frame by frame, compare visually, hope you don't miss anything. I knew there had to be a better way. I just hadn't built it yet.

First question: what does "duplicate" actually mean?

Before writing a line of code, I ran into the question that turned out to be the crux of the project: what does it mean for two artboards to be the same?

Is it visual duplication or structural duplication? Should layer names count — or is an artboard a duplicate even if someone renamed its internal layers? What about components — if two artboards use the same component but with different text overrides, are they duplicates? What about a frame that's identical in structure but at a different position on the canvas?

These aren't technical questions. They're design decisions. And I had to answer them before I could specify what I was building.

I settled on two modes: a strict comparison that includes internal layer names, and a visual one that ignores them and focuses purely on structure and visual properties. And the canvas position of the artboard — irrelevant to its content — had to be excluded from the comparison entirely.

That definition became the foundation of everything that followed.

The brief as a designer's tool

I decided to lean on Claude Code as a technical co-pilot to accelerate the implementation. But the starting point wasn't code — it was a brief.

Not a quick one-liner, but a real product document: what the plugin should do, how the comparison logic should work, which properties to include, what the output should look like, what edge cases to consider. The same kind of document I'd write before handing off to a development team.

In this case, the brief wasn't a prompt to "make code." It was a product specification: scope, rules, edge cases, and quality criteria. Using that document as a foundation, I was able to generate a first working version of the plugin. It wasn't perfect — it was a starting point. What came next was the interesting part.

The errors were lessons, not blockers

I expected the code to work on the first try. It didn't. But the failures were more instructive than smooth execution would have been.

The first collision was with Figma's plugin runtime: some modern JavaScript syntax simply isn't supported. Not a logic error — a context error. The code had to adapt to the real environment where it was going to run, not the ideal JavaScript you'd assume from a modern browser.

Then: `t.map is not a function`. Figma's API returns array-like collections that aren't real JavaScript arrays — they don't have `.map()`, `.filter()`, or `.forEach()`. The fix was converting everything to real arrays before operating on them.

Then: `cannot convert symbol to number`. This is Figma's `figma.mixed` — a Symbol value returned when a property has different values across children (like corner radii that vary per corner). Every numeric property needed a safety wrapper.

Each error revealed something real about how Figma works internally. By the time the plugin ran without crashing, I understood the API far better than I would have from reading documentation alone.

The comparison that wasn't comparing

The plugin ran. It showed results. Zero duplicates — even for frames I had manually duplicated to test it.

This was the most interesting problem to debug. The logic was working, but it was comparing the wrong thing.

I added a debug mode: for any two artboards with the same name but different hashes, find the first property that differs and show it. The output came back immediately: `root.x (4925 vs 4941)`.

The artboard's own X position on the canvas. I had been including it in the hash.

Of course two artboards at different positions on the canvas would produce different hashes — even if their content was identical. The canvas position of a frame is irrelevant to whether it's a duplicate. It's not part of the content. It's just where you put it. One of those decisions that seems obvious in hindsight and completely invisible until the data shows it to you.

One line of code: set the root artboard's X and Y to zero before hashing. After that, it worked.

Publishing isn't the same as running locally

I thought publishing would be straightforward. Submit, wait, done.

Figma rejected the first version.

The reason: I had added `"documentAccess": "dynamic-page"` to the manifest — which Figma requires for plugins that need to access pages beyond the currently active one. But with that flag, you can't access `page.children` synchronously. You have to `await page.loadAsync()` first. And you can't call `node.mainComponent` — you have to use `await node.getMainComponentAsync()`.

Every synchronous API I was using became asynchronous. Which meant the serialization function — which recursively walks every node in every artboard — had to become async, and every call that touched it had to be `await`ed. About an hour of refactoring. The second submission was approved.

There's a real gap between a plugin that runs in development and one that works in production under Figma's actual constraints. That gap only reveals itself when you try to cross it.

What I really learned

The technical problems were solvable. What surprised me was everything around them.

The most important lesson came before any code: before asking AI to build something, you have to know what you're building. And that isn't an AI problem. It's a design problem.

The brief forced me to make product decisions I would have skipped if I'd gone straight to code. The debug mode forced me to understand what the comparison logic was actually doing, not what I assumed it was doing. The Figma rejection forced me to understand the difference between a development environment and a production one.

None of those lessons came from reading documentation in advance. They came from running into specific problems in context — which is also, more or less, how design research works.

The plugin is published

Artboard list to Excel is live on the Figma Community. It detects *duplicate frames* using deep structural comparison — node types, positions relative to the artboard, fills, strokes, typography, auto layout properties, component references — and exports the results as an Excel file. It also finds frames inside Sections, which turned out to be a common case I hadn't considered until a test file had all its frames grouped inside one.

Is it perfect? No. Images are compared by their internal hash, not their visual content. Variables are compared by resolved value, not by variable ID. There are edge cases I haven't encountered yet.

But it solves a real problem, in a real context, for people who work with real files. And for a first plugin — also a reminder that designers can build tools, not just specify them — that's a good enough starting point.

Andrés Ballén

Andrés Ballén

UX Strategy & DesignOps Leader