Jason Spidle

Principal product designer

How I built Sheets to Layers

A Figma plugin I built one weekend in between wrestling matches with my son.

Post

Sheets to Layers playground file cover, with a Google Sheet of season stats looping by hand-drawn arrows into a Figma table

It used to be that when the software you loved and used religiously broke, you'd have to figure out how to get by in life without it. Either that or its absence might inspire you to turn yourself into a software developer just to remake it. Certainly I considered it a few years ago when a dear tool of mine up and stopped working. Life prevented me from pursuing it then and the idea sat dormant until now. Because now, we've got coding agents.

The other thing I had on my side at this particular moment in time was a Figma prototype full of placeholder data just begging to be made to look real. Out with the fake names and fake prices and gray rectangles! But at what price? I know from experience that making such updates is the bane of my existence and that I'd do almost anything to avoid the work. I'd even build an entire Figma plugin myself to do the work for me.

The plugin that abandoned me was called Google Sheets Sync and it was made by Dave Williames. I love spreadsheets. I even have a hat that proves it. It's just so easy to bulk manipulate data in that form. The Google Sheets Sync plugin let you connect your Figma designs to Google Sheets columns and populate the text layers or the image layers or the background colors or whatever have you just by having that data in the rows of your sheet. But Dave didn't keep it updated and as Figma kept shipping, things in the plugin kept breaking until finally it just stopped working altogether.

This was the plugin I was meant to build and this was the moment it would happen. What better setup could there be? My choice was between spending all day manually updating layers with real-looking data or spending all day (it'll only be a day, right?) making a plugin so my Figma file could talk to a Google Sheet and then I'll only need to manually update all that data in a spreadsheet,

click a button, and blammo: fake data no more.

I'd been dabbling with Claude Code here and there. Mostly trying to figure out what it did and how to even use a TUI. I think also I spent a lot of time customizing Ghostty. It's the little things, you know? This seemed like a perfect opportunity to really put these coding agents to work. I had read enough about the most effective ways to prompt these agents to know that context is key. And that was the easy part. Writing up a storm about what I wanted software to do is a specialty of mine. So that's what I did.

Sheets to Layers with a Google Sheets link pasted in, the option to update the current page only selected, and Fetch and Fetch & Sync buttons
The plugin starts from a sheet's link and asks whether to update the whole document or only the current page.

I did all of this in fits and starts as I went about my weekend with a 3-year-old. I'd stand at my desk and write up a ticket for how the column labels should be parsed or about how we could use the values in a cell to swap a component or about how we might afford our heroic designer the ability to duplicate frames to match rows in a sheet. These tickets included acceptance criteria and examples and all the normal things you find in a ticket. The only real difference is that these were Markdown files instead of Trello cards.

My collection of tickets amassed, it was now time for the reckoning. I loosed Claude Code on the first couple tickets and waited to see what it would spit out on the other side. But actually, I didn't wait because I had to take the kiddo to the playground. But when I returned, I was pleasantly surprised. In fact, I was pleasantly blown away. Not only did it look like the thing I had pictured in my head, it also worked. I followed the instructions for installing the plugin in dev mode, fired it up, and there it was. I slapped my Google Sheet URL in there, clicked the button and before my very eyes was the data from that selfsame sheet. I clicked a few more things and the metamorphosis was complete. The layers in my Figma file were now connected to the sheet.

Mind you, it was capable of the bare minimum at this point. I still had a pile of tickets to work through. But the feeling I felt at that moment was one of immense pleasure and enormous potential. It was undeniable. I pointed Claude Code at another ticket and walked away. My son wanted to play in the snow, so we played in the snow. The floors needed vacuuming, so I vacuumed the floors. Eventually, I'd find myself back at my desk to test whatever had been built. I'd nod my head and then point it at the next ticket and walk off again.

This was the kind of programming I could get behind.

By Sunday evening, all the tickets were done. The plugin could do everything I dreamt for it. It could read a Google Sheets URL in any of its various forms and work out whether the sheet data ran horizontally or vertically. It could populate your Figma layers with text, images, components, colors. It could duplicate a frame to match the number of rows in a sheet, or fill an existing set of layers in place. All of it just worked.

Preview Data for the Products worksheet, five columns and 240 rows of titles, variants, styles, image links and component names, with tabs for Q1 O’Brien, A1, One Column, Duplicate Headers and Empty Data
Preview mode shows a worksheet before anything syncs. The tabs along the bottom are test sheets, each named for the case it checks.

On Monday, I thought of a few more features and did a polish pass that brought the design quality up to a level I would feel comfortable claiming as my own. I fixed the spacing and gave the whole workflow a nice preview mode so that a person could see what they were about to do before doing it. I've always called this part of a build "design tweaking" and it was the part of building this plugin that I was most qualified to run.

The top of the playground file’s cheatsheet, with cards for the #text, #text-styling, #component, #image, #color and #visibility layer names, each showing a column of sheet data and the result on the canvas

Layer name syntax

#Label
bind to Label column
#Label.N
use row N specifically
#Label.n
auto-increment rows
#Label.x
random row
// Tab
use worksheet tab
@#
repeat children to match rows
-Name
ignore this layer
The playground file's cheatsheet pairs each layer name with the sheet data it reads and what the layer becomes. For more, check the docs.

On Tuesday, I stress-tested the plugin. This meant I brought a litany of questions like "What happens if the plugin edits a text layer that uses a missing font?" I love thinking of edge cases, so this part was fun. This made the plugin more resilient.

On Wednesday, I created a playground file to demonstrate the various capabilities of the plugin. I created an icon for the plugin and a cover image to be displayed in the Figma Community. I wrote the description for the plugin and then submitted it for review.

The eagle-eyed reader will note that this did not take one day. Those placeholder values littering my prototype did not get replaced until Thursday. But when they were replaced, it was done with an uncommon joy and in a matter of moments. Not only that, but this same dilemma faced by another designer in a not-so-distant day in the future may very well be resolved with little trouble because of my efforts in procrastination.

I can certainly tell you I haven't moved a single inch closer to being able to write a Figma plugin myself. I suppose I now know that the manifest.json file is the entry point to a Figma plugin. But what that file contains remains a complete mystery to me. And maybe that simply doesn't matter anymore. If you can describe what you want and if you can break a big problem into small ones, and if you can tell if the thing that you want works the way it should (and moreover, explain why it doesn't and what it should do instead), then you too can be a Figma plugin developer.