Spock stood still on the transporter pad, waiting. And then … ENERGIZE! He transforms into glittery streams of light, disappears, and is away somewhere doing good. Some time later, he reappears through that same swirling energy. His shirt’s still blue, his ears pointy, his hair still a stark, bowled edge. Spock’s back, right? Mission complete!
No systems expert would rate that sufficient evidence of a successful transport. Glancing at shirt color, ears shape and hair style isn’t “good enough.” You want Spock: half-human, half-Vulcan, hyper-calculating, mindmeld-capable and yet humanly empathetic. We love Spock for all his complicated powers and nuance.
What I see today is designers shipping kind-of-Vulcans and expecting agents and engineering teammates to make that more “Spock-like.” Contracts carry color, fold in shape, and itemize styles. They add prop tables and variant axes. Cue amazed reactions from consumers of your markdown and json? At least designers aren’t shipping red shirts, right?
Contracts should carry design intent as exhaustively as needed. One way to test is a looping “round trip:” beam away, beam back, and evaluate the difference. This will expose whether a contract carries a kind-of-Vulcan or actually Spock.
I experienced a nearly-but-not-Spock humbling by accident. Round-trip testing a new feature revealed that my specs transformation and model was incomplete and lossy. Looping revealed defects, model gaps, and a lack of clarity around what the contract can’t, doesn’t, and doesn’t yet carry. This blog post describes the journey through a contract’s loss and depth and how round trips exposed how to make it better.
Loss
In a spec-driven UI component workflow, we strive for lossless transformation: converting data from one structure into another without losing, altering or inventing a single bit of information. Loss accumulates in the conversion from Figma asset to spec data, and then compounds as agents and engineers make interpretations and inferences on an incomplete picture .
When translating from Figma to spec, loss can take many forms:
Emitting a hex code when a color token is bound.
Missing a layer’s
maxWidthapplied to only the last of many options.Omitting a change in
backgroundColoror layer presence when properties interact, such asselected:trueandstate:hoversimultaneously occur.
Loss can occur when intent gets more complicated:
Representing an
Alert’s decorative icon as customizable when it shouldn’t be in anerrorvariant or not customizable when it should be in aneutralvariant.Missing how a
Pill's horizontalpaddingvaries acrosssizes depending on if content is composable in a slot or includes just a built-in label.Errantly consolidating a Figma
BOOLEANandSLOTprop into a singlenullableslot property when the layer is only bound as aSLOTin some but not all variants.
These complicated examples are like Spock’s capabilities for logic, Vulcan pinch and mindmeld. These losses are worse: dropping or inventing complicated, powerful intent and easiest to miss when reviewing an asset visually. Nobody noticed because the contract had no place for it.
Depth
Contracts can carry a little or contracts can carry a lot. It’s simple to devise a format (in markdown and/or json) and an agentic skill to extract colors, spacing, corner radius, width and height across a tree of named layers. Another iteration, and a contract may also emit detected changes across each variant axis and generate a props table. A simple contract is easy to evaluate and unlikely to lose much. But it’s not carrying much, either.
Shallow contracts don’t work when UI components are intricate.
Design system teams express far more intent, authored at far finer grain in Figma assets. Variants subtly shift styling across interdependent props. Cases vary tokens and prop bindings, rules and intents.
The focus on contract depth is evident in the very first setting I implemented in the latest Specs generation: variant depth. Many publicized contract tools evaluate a Figma component set’s variant axes, comparing how design changes across enumerated options. That’s variantDepth:1. The Specs Classic (first generation) tool offered a “Two way” feature that evaluated difference of every combination of two properties, such as selected:true|false and state:rest|hover|pressed. That’s variantDepth:2.
Most tools and demos I see go no more than two levels deep. Why stop there? Designers express intent in any variant, and contracts promise exhaustiveness, right? I run Specs on variantDepth:9999 every time: no variant, layer, styling, or binding is ever missed.
Plus, depth isn’t just about variants. Subcomponents, nested instances, slots, content and props all interact in fantastic ways. Implicit naming is used to express explicit concepts. Layouts makes layers shift, appear and disappear, show and hide. Phew, I’m out of breath, and that’s not all of it! Design intent can gets deep and complicated across these concerns.
The reward for expressing more intent is that you have more to lose. A shallow contract round-trips perfectly: carry eight properties, render eight, empty diff. That’s flawless yet entirely misleading losslessness, because so much wasn’t carried in the first place. The trap is thinking shallow contracts are good. What matters for a strong contract is both how much the contract carries and how much survives the trip.
The round trip
So how would you rate depth and losslessness? Reading a schema tells you what it meant to model. It doesn’t tell what the contract actually carried. The best way to find out what a contract holds is to make it give the component back.
Starting in reverse
I recently built a render command in my Specs ecosystem, which takes a spec and renders it as a component asset in Figma via a script. It takes a few seconds per component. Why? Design system intent is originating outside of Figma. If a design system is supports building and expressing designs in both Figma and prototype, then the system must be able to move the expression between them cleanly. Therefore, the render command bring a spec’ed expression back into a Figma library.
The challenge is that the schema model underlying specs is incredibly rich: components, anatomy, props, styling, bindings, variants, slots and recursive nested composition, code only props and more. Generating a Figma asset could be agentically done, but it’d likely be lossy and error prone. Writing a deterministic render script instead was possible if bloody complicated.
The loop
As render development progressed and most features were implemented, I found inspiration in Addy Osmani’s writings like Loop Engineering. As a result, I began to loop between generated specs and rendered Figma assets to refine and polish the render's implemented features. The goal is to remove as much loss at depth as possible, proving that render has reached parity with generate in the opposite direction.
An agent would run each round, using an orchestrating script to set the harness for one component or entire catalog. The harness uses a bridge to connect Figma through a plugin to the LLM chat and cached library data (variables, icons, assets). The loop takes these steps:
generatea baseline spec from an existing Figma componentrendera test Figma component from a baseline specgeneratea test spec from the test Figma componentdiffthe baseline and test specsCreate a
reportof what’s change (once for an entire library)deletethe test Figma component
Anything the pipeline loses, mangles, or invents shows up concretely rather than a hunch. The report skill validates diff counts and variant shapes and separates signals of unimplemented features from defects and regressions. Data in hand, the skill then groups symptoms into themes with root causes and create prioritized Github issues that I smash immediately or save for later. Let the loop-by-loop-burn-down commence!
The first loop against an entire library was loud: many big themes, each requiring an investment to parse, plan and smooth the render engine. Over many loops, differences diminished to residue that I could precisely inspect, refine the code, and loop more narrowly. As themes and defects quieted down, I gained confidence as less and less data was being lost. Eventually, the render command converged on feature parity with generate command.
Whether or not you build a render script, the broader takeaway is to use round trips and loops to test your contract. Agents and skills can beam back and compare to what agents beamed out. Even if it’s not deterministic, you can still find ways to distinguish carried intent from loss and lacking depth.
Yet, the loop yielded something more valuable than getting render done.
The Reckoning
I began my render work more than confident that the Figma-to-Specs generate transform and underlying schema was complete and stable. Yet, each loop exposed pipeline weaknesses. generate was dropping details silently. The schema lacked shape to express details. The contract wasn’t capturing intent I didn’t realize I valued. Mind you, my confidence in generate + schema's viability wasn’t shaken. But the tools were neither perfect nor reaching their potential.
A round trip is a brutal test of a contract.
Turns out, a round trip is a brutal test of a contract. Anything a transform and schema can’t capture gets dropped beaming out, and anything a spec captures too loosely risks loss and differences beaming back.
So, converging a round trip’s baseline and test differences to zero wasn’t actually the right goal. The diff doesn’t converge to nothing. Instead, it converges on sets I can name: reaffirmed assumptions, defects to fix, and what the contract can’t, won’t, or doesn’t yet carry that I can clearly communicate.
Expose assumptions
As I’ve built Specs, there are choices about data that I knew and others I sensed. Making the round trip made them explicit and visible, such as:
Alphabetical order is not neutral; instead, it destroys authored order.
Booleans must be booleans everywhere, if that’s how you want to treat them. Even
VARIANTprop options that aretrueorTrue,falseorFalse.Visibility is not existence; instead, hidden layers must be respected exhaustively.
If not recognized and handled correctly, transformations break. Speaking of which…
Fix the transformation
Lots of small defect emerged over time, implemented feature by implemented feature:
Broken styling:
clipsContent– very rarely used and lacking unit tests and text fixtures – sneakily never worked defined asclipContent. Not just a transformer fix, but an ADR because it changed the model’s mistake. Sigh. Details.Warnings where there had been silence: unresolved slot constraint keys, an ignored binding, an unbindable property. Knowing when and how much processing trips up enables smoothing more rough edges.
Examples lacked top-level rules: content in slots wasn’t evaluating the relationship with the slot layer parent. As a result, data suggested content with a fixed width, when instead it was filling horizontally.
Such small defects go unnoticed to those reviewing visual test cases. Even visual regression snapshot tests would have missed the last Examples issue. Yet round trip transformations and differencing of data caught so much more.
Other defects rest in the backlog, with an unclear priority awaiting when they’ll actually negatively impact transmission. Authors in Figma shouldn’t be naming a code-only prop as the same name as a native prop. Detecting that should be some other tool’s job even if a Specs warning might be nice.
Expand the schema
The model does a great job carrying intent, but surprisingly tripped over basic identities. Naming is hard.
Converting Figma labels to camelCase or snake_case (a long supported, high-demand feature) is lossy, dropping characters and capitalization on round trips. Therefore, it became essential to add a safe key grammar to carry names losslessly and preserve Figma names when needed. Establishing a figmaKey:NONE|SENTENCE|TITLE convention to declare how designers are naming in Figma came along for the ride.
What can’t be supported
Unfortunately, the round trip also made clear intent that Specs can’t support, like:
Customized prop order in Figma’s Props panel. You can derive
VARIANTprop order from component layer names. However, Figma’s plugin API doesn’t carry the order of remainingBOOLEAN,INSTANCE_SWAP,TEXT, andSLOTprops.Locking aspect ratio means that bound variables for both
widthandheightcan’t both be carried even if both remain bound “under the covers” of Figma’s UI.
Designers won’t care about the bound variable-dropping of locked aspect ratio. But I’ve witnessed more than enough designers fiddling with Figma Props panel property order to know there’s intent there. Through order, there’s priority and proximity. Through proximity, there’s groups and relationships. Those mean something, even if implicit.
What isn’t yet supported
Doing the work on the opposite render direction reaffirmed the boundary of how much Figma expression is carried through Specs. I don’t prematurely optimize to cover all cases, and demand suggests how the model and transformations grow.
As a result, I continue to not yet support Figma features like Grid autolayout, esoteric line end point styles, and stacks of stroke and fill colors. Designers aren’t yet using them in work I support (although Liquid Glass seems to have something to say about that; stay tuned!).
What won’t be supported
This exercise also reinforced the higher cost that outweighs any current benefit of building around Figma’s new motion capabilities. Figma’s motion data model is necessarily very complicated: pages and pages of docs and many nested levels of data depth, making typography and effects look elementary.
Motion is a whole separate language, few in our field know it well, nobody I know is yet expressing it in Figma, and it’d be enormously expensive for me to build a whole ecosystem around it. Maybe that day will come. But I’m not planning on it, and I’m overt about it to curious customers.
So, if you beamed your component back, what wouldn’t it return? Most don’t know, having focused on only the good and obvious stuff they’d beamed out. So take a round trip. Or two. Or some series of loops until you settle where you need to. Look for what you are missing, and how much you value it. Your diffs probably won’t converge on empty either. But you’ll be able to name the gaps and provide the polish elsewhere to ensure the components you make are the best they can be.



