code-transformer for Figma, code-generation from design with OpenAI API
In this article, I would like to document the process of creating a plugin that generates code from Figma using OpenAI’s API.
My current impression is that the generated code is quite practical, as long as the design is well-structured and the components are synchronized between design and code.
However, there are still many challenges, not only in terms of the plugin’s functionality but also in the design and implementation processes of the organization. We have made the project open source, and we would be happy if those interested could contribute or share their insights on work styles.
Also, although features are limited, I have published the plugin to community, so please try from below!
However, currently, GPT-3.5-turbo doesn’t output high-quality code, so GPT-4 is required. If you don’t have access to the GPT-4 API, please wait or try to find a prompt that works well with GPT-3.5.
Demo Video
First, please check out the demo video below to see what the plugin is actually like.
*Note: While the video may make it seem like the code is generated instantly, it actually takes about a minute.
Basic Structure
First, the general process for generating code is as follows: convert Figma layers to JSON and then pass the JSON to the OpenAI API to generate code.
In the Figma plugin, you can access information about Figma layers and components. For example, the diagram below shows the type definition of information obtainable from a Frame layer, including Auto Layout-related data.
However, since this raw data contains excessive information and Figma-specific vocabulary (such as combinations of primaryAxisSizingMode and counterAxisSizingMode), we first convert the data into CSS vocabulary to make it more interpretable by the OpenAI API.
I will not go into too much detail, but a rough overview of the code is shown below, where we simply traverse the children of Figma nodes (layers) and convert them into objects containing only necessary information.
In the case of component instances, we want to display them as components, so we do not delve any deeper into their children and instead convert them into separate objects of the “ComponentNode” type (https://github.com/kazuyaseki/figma-code-transformer/blob/main/src/figmaNode/buildTagTree.ts).
node.children.forEach((child) => {
if (child.type === 'INSTANCE') {
const props = Object.keys(child.componentProperties).reduce(
(_props, key) => {
const value = child.componentProperties[
key
] as ComponentProperties[string];
// component property keys are named like this: "color#primary"
// thus we need to split the key to get the actual property name
const _key = value.type === 'VARIANT' ? key : key.split('#')[0];
return { ..._props, [_key]: value.value };
},
{} as { [property: string]: string | boolean }
);
if ('Instance' in props) {
delete props['Instance'];
}
childTags.push({
name: child.name.replace(' ', ''),
props,
isComponent: true,
children: [],
});
if (child.mainComponent) {
componentNodes.push(child.mainComponent);
}
} else {
const childTag = buildTagTree(child, componentNodes);
if (childTag) {
childTags.push(childTag);
}
}
});
The getCssDataForTag
function simply retrieves data from Figma layers and converts it into CSS vocabulary.
export function getCssDataForTag(node: SceneNode): CSSData {
const properties: CSSData = {};
if(!node.visible) return;
if ('opacity' in node && (node?.opacity || 1) < 1) {
properties['opacity'] = node.opacity || 1;
}
if ('rotation' in node && node.rotation !== 0) {
properties['transform'] = `rotate(${Math.floor(node.rotation)}deg)`;
}
...etc
▼ Detailed code
By the way, the code above is an extension of a React code generation plugin I created a while back. It’s an emotional development that proves useful across time.
Once the JSON is created, all that remains is to create a prompt and have the code generated.
Convert this Figma JSON object into React, TypeScript, Tailwind code.
## Figma JSON
{paste Figma JSON here}
## constraints
- component does not take any props
- do not omit any details in JSX
- Do not write anything besides code
- import components from @/components directory
- if a layer contains more than 1 same name child layers, define it with ul tag and create array of appropriate dummy data within React component and use map method to render in JSX
- use export rather than default export
That concludes the basic explanation of how the plugin works.
What’s innovative about LLM in code generation?
Over the past few years, I’ve been wondering if it’s possible to generate code from design, so I don’t have to code the visual aspects. Although it deviates slightly from the main topic, I’d like to briefly discuss the innovations that LLM has brought to this context.
Previously, I created and published a plugin that generates React components from Figma in the community.
Despite creating this plugin, there were several challenges:
1.Adapting to various formats is extremely labor-intensive
For example, to make the plugin compatible with Tailwind, you would need to write conversion code for class names for each property, with great care.
2. Conflicts with existing code
When the design of an already-implemented page is updated, the existing code is ignored, making it unusable as-is.
3. Differences between generated code and components
Although the plugin allowed for setting components, the effort required to input information specifically for the plugin was significant, making it difficult to call it a practical solution.
While I believe it was possible to implement these features with enough effort, the process was incredibly time-consuming, and I felt it wasn’t worth it. Then LLM appeared like a comet.
With a prompt, you can simply request:
- “Write CSS using Tailwind”
- “This is the existing code, so don’t modify this part”
- “This is the code of component, so write Props accordingly”
And LLM will generate code of the expected quality. Thank you, OpenAI. With the implementation hurdles lowered so much, there’s no choice but to strive for realization.
Additionally, one thing I thought had good compatibility with the use case is that the generated code is intended to be revised by an engineer afterward.
While LLM’s output can be improved upon, there are inevitable inconsistencies and hallucinations in the responses. Therefore, if LLM can produce 90-point outputs, humans can use that as a basis to kick off development, making code generation use cases well-suited to LLM’s characteristics.
Additional Features
I’ve explained the basic logic so far, but there are still some aspects that are insufficient for practical use.
For example, it doesn’t address the following cases:
- Component Props are based on Figma Variants properties, so Props that only exist in the code are not written, and properties that only exist in the design are written
- Existing page logic is erased
- The OpenAI API has a token length limit, causing errors when generating code for large layers
- CSS is not using design tokens
To solve these challenges, I tried various implementations and will introduce them one by one.
Pass a summary of Props and have the code write Props in the same way
The goal is to pass a summary of the component’s code Props to the prompt, solving the problem of “Props in the code not being written / design-only properties being written” because the component’s Props are based on Figma Variants properties.
so I prepared summary of props as follows
### Button component:
Props:
- variant?: 'primary' | 'secondary' | 'outlined' | 'basic' | 'success' | 'dangerous' | 'dangerousOutlined' | 'dangerousPlain'
- size?: 'xsmall' | 'small' | 'medium' | 'large'
- subText?: string
- loading?: boolean
- disabled?: boolean
- rounded?: boolean
- startIcon?: ReactNode
- endIcon?: ReactNode
- children: ReactNode
Then add this sentence to prompt
- if Props summaries are provided for the components used in JSON, write props that are required in code props and omit props that only exist in Figma JSON
By doing this, the code will intelligently insert the values of Variants according to the Props in the code, and it will no longer write Variant Properties that only exist in the design.
How to create a summary
This time, I also relied on the OpenAI API to create this summary.
First, use the GitHub API to get the corresponding component file. (Note that this assumes the component name is consistent between design and code.)
const { data } = await octokit.repos.getContent({
owner: 'gaudiy',
repo: 'design-system',
path: `packages/designed-components/src/components/${componentName}/${componentName}.tsx`,
});
Then, have the summary written using the following prompt. By the way, this task is sufficient for gpt-3.5-turbo.
export const buildPromptForPropsSummary = (
componentName: string,
codeString: string
) => {
return `Write Summaries of Props of ${componentName} component in the following code
## Code
\`\`\`tsx
${codeString}
\`\`\`
## constraints
- start with a sentence "{ComponentName} component:"
- write list of props with TS type
`;
};
The Props summary created this way is also included in the code generation prompt. While I’ve explained this in a somewhat abridged manner, when retrieving component information, there are cases where referencing a single directory is not enough, and the number of requests increases.
Therefore, it would be nice to have a more flexible search. I’m considering using embeddings when requesting to OpenAI API or even embedding the information in the plugin code (although how to reflect updates in this case is undecided).
Split and merge large layers into chunks
As a premise, the OpenAI API has token length limitations. Strictly speaking, it’s different, but token length is like the number of characters, and if the prompt is too long, an error will occur.
So, as it is, it’s impossible to generate code for pages containing a large number of layers, but I don’t want to worry about such limits when using the plugin… So, I implemented splitting into chunks and merging them later.
I created a function that arranges the nodes of the Tree in descending order of Chunk size, and removes them from the original Tree in the order of token count close to the upper limit. (※ There is currently a bug that splits within the same tree)
import {
APPROX_TOKEN_COUNT_PER_CHARACTER,
MAX_TOKEN_COUNT_FOR_CODE,
} from '../constants';
import { Tag } from './buildTagTree';
function getTokenCount(tag: Tag) {
const stringLength = JSON.stringify(tag).length;
return stringLength * APPROX_TOKEN_COUNT_PER_CHARACTER;
}
export const divideTagTreeToChunks = (tag: Tag): Tag[] => {
let totalTokenCount = getTokenCount(tag);
if (totalTokenCount < MAX_TOKEN_COUNT_FOR_CODE) {
return [];
}
let result: Tag[] = [];
// Explore all nodes, create an array of {node, childIndex, parentNodeRef}, and sort by token count
const nodes: {
node: Tag;
childIndex: number;
parentNodeRef: Tag | null;
}[] = [];
const traverse = (
node: Tag,
childIndex: number,
parentNodeRef: Tag | null
) => {
nodes.push({ node, childIndex, parentNodeRef });
if ('children' in node) {
node.children.forEach((child, index) => {
traverse(child, index, node);
});
}
};
traverse(tag, 0, null);
nodes.sort((a, b) => getTokenCount(b.node) - getTokenCount(a.node));
// Add nodes to chunk in descending order of token count, as long as the total token count is smaller than maxChunkSize
nodes.forEach(({ node, childIndex, parentNodeRef }) => {
if (totalTokenCount < MAX_TOKEN_COUNT_FOR_CODE) {
return;
}
const tokenCount = getTokenCount(node);
if (
tokenCount < MAX_TOKEN_COUNT_FOR_CODE &&
'id' in node &&
parentNodeRef &&
'children' in parentNodeRef
) {
result.push(node);
parentNodeRef.children[childIndex] = {
nodeId: node.id || '',
isChunk: true,
};
totalTokenCount = getTokenCount(tag);
}
});
return result;
};
Then, I added a command to write only the ID to the JSX if the child layer is a Chunk in the prompt.
- if child is chunk, render it as --figmaCodeTransformer=nodeId, it will later be used to replace with another code
By doing so, it will render like this.
<div className="flex flex-col justify-start items-center p-16 gap-16">
--figmaCodeTransformer=149:20062
Then, for the parts made into Chunks, have only the JSX written, and replace the rest manually.
export function getChunkReplaceMarker(id: string) {
return `--figmaCodeTransformer=${id}`;
}
export function integrateChunkCodes(
rootCode: string,
chunks: { id: string; code: string }[]
) {
let integrated
Code = rootCode;
chunks.forEach((chunk) => {
if ('id' in chunk) {
const replace = getChunkReplaceMarker(chunk.id);
const regex = new RegExp(replace, 'g');
integratedCode = integratedCode.replace(regex, chunk.code);
}
});
return integratedCode;
}
// Excerpt from the part where the code is merged
const codes = await Promise.all([
createChatCompletion(prompt, []),
...chunkPrompts.map((chunkPrompt) => {
return createChatCompletion(chunkPrompt, []);
}),
]);
const rootCode = integrateChunkCodes(
codes[0],
chunks.map((chunk, index) => ({
id: 'id' in chunk ? chunk.id || '' : '',
code: codes[index],
}))
);
With this, working code is created, but the format is not beautiful, so I’m thinking of adding prettier support later.
By the way, in the prompt, I specified the part where only the JSX is output like this. When I added an example, it became stable to output. Write only JSX
alone included the function declaration part, so adding an example stabilized the output.
- Write only JSX
for instance if the result code is like below:
\`\`\`
import { Hoge } from "hoge";
export const ExampleComponent = () => {
return <div>....</div>
}
\`\`\`
Then output only
\`\`\`
<div>....</div>
\`\`\``;
};
Generate only the differences when given existing code
Simply by passing the existing code of a page and specifying do not change previous code
, it will only add and output the changed parts. Smart!
So, I’d like to provide a feature to make the experience of “selecting existing code” feel good. I have a feeling that if I can create something like the following, it would cover most use cases.
- Manually copy and paste into a text area
- Save the file name in the plugin if it has been generated before
- Display the files in a repository as a tree and select them
Convert design tokens to class names
At the company work for, we are using Tailwind, so I’d like to convert the so-called design tokens to class names as specified in tailwind.config.js.
First, there are two major patterns for obtaining the design token information in Figma: styles, a standard feature, and Token Studio, a plugin that allows you to define things other than colors and typography in styles.
I’ll try to support both.
For styles, if color or typography values are specified, we pass the style name instead of the raw value.
function setColorProperty(
fills: Paint[],
colorStyleId: string,
properties: CSSData,
colorProp: 'background-color' | 'color'
) {
if ((fills as Paint[]).length > 0 && (fills as Paint[])[0].type !== 'IMAGE') {
const style = figma.getStyleById(colorStyleId);
const paint = (fills as Paint[])[0];
const color = style ? style.name : buildColorString(paint);
properties[colorProp] = color;
}
}
TokenStudio, thankfully, saves data in a format that can be referenced by other plugins using [setSharedPluginData](https://www.figma.com/plugin-docs/api/properties/nodes-setsharedplugindata/)
, so I'll try to obtain it through that.
function setFigmaTokens(node: SceneNode, properties: CSSData) {
const tokenKeys = node
.getSharedPluginDataKeys('tokens')
// Omit "version" and "hash" because they are not tokens
.filter((key) => key !== 'version' && key !== 'hash');
tokenKeys.forEach((key) => {
const value = node.getSharedPluginData('tokens', key);
if (value) {
// remove css that's represented by token
if (key === 'itemSpacing') {
delete properties['gap'];
}
properties[key] = value.replaceAll('"', '');
}
});
}
Once you can obtain it like this, in the prompt, you can specify something like “if there’s no proper value, it’s a design token variable. It is defined with the name in kebab-case in tailwind.config.js”.
- if string value other than hex or rgb() format is specified for color property, it is design token variable. it is defined with the name in kebab-case in tailwind.config.js
- if "typography" property is specified, it is defined in tailwind config as typography token that has multiple properties such as font-family, font-size, font-weight, line-height
Saving GraphQL queries to layers
Although this slightly deviates from the context of “generating code from design,” the company I work for uses GraphQL, and in the context of fragment colocation, there was a conversation about how it would be nice to be able to save fragments to layers or components -> issue a combined Query when generating. So, I tried creating it as a test.
For example, by saving Fragments to two different layers like this:
When you open the plugin in the root layer, a Query with expanded Fragments underneath will be displayed.
The editor uses a library called graphiql-explorer.
I won’t go into too much detail about the inner workings, but to give a brief overview, graphiql-explorer allows you to conveniently select properties based on the schema, but it does not support Fragment descriptions.
So, I save the data as a Query, and when displaying the appearance or passing data to the prompt, I convert it to the form of a Fragment and merge it.
▼ Code to convert Query to Fragment
▼ Code to merge Fragments
Using this, I save Fragments and Queries for each layer using setPluginData. I was able to create a saving mechanism, but I’m considering using the gql written here in the prompt to write data connections or, as a small improvement, creating a PR with gql files together.
Features to reduce a bit of effort
In addition to the above, I’ve prepared some small improvements to reduce effort, which I’ll introduce briefly.
Create Storybook
ChatGPT can write Storybook if you give it the component code.
So, I ask for Storybook to be written with the following prompt (works with gpt-3.5-turbo).
export const buildPromptForStorybook = (
codeString: string,
componentName: string
) => {
return `Write storybook for the following component in Component Story Format (CSF).
\`\`\`tsx
${codeString}
\`\`\`
## constraints
- Do not write anything besides storybook code
- import component from the same directory
- do not have to write stories for components used in ${componentName}
`;
};
Support for Creating PRs
Even if the code is generated, there are still some steps to be taken:
- Create a new branch
- Create a new file
- Copy and paste the code into the file
We want to reduce these extra steps as well. So, we’re using the GitHub API to support creating PRs up to this point.
The code for creating PRs looks like this:
By the way, we’re also asking ChatGPT to come up with branch names and such, using the following prompt (gpt-3.5-turbo compatible):
export const buildPromptForSuggestingBranchNameCommitMessagePrTitle = (
codeString: string
) => {
return `Suggest
- branch name
- commit message
- PR title
- component name
for the following code
\`\`\`tsx
${codeString}
\`\`\`
## constraints
out put as JSON in following property names, do not include new line since this string is going to be parsed with JSON.parse
{ "branchName": "branch name", "commitMessage": "commit message", "prTitle": "PR title", "componentName": "component name" }
`;
};
Display Cat Videos During Waiting Time
The GPT-4 API takes a very long time. It’s so long that some ingenuity is needed. So I embedded cat movie while waiting.
However, the chatCompletion API does return results in a streaming manner, so one character at a time, like the ChatGPT’s original implementation, might give a better idea of how much longer it will take and provide a better overall experience.
Future Challenges
So far, I’ve written about what has been achieved up to this point.
However, there are still many challenges ahead, and in order to ensure that the right expectations are set for what has been created so far, I’ll list the challenges I’m currently aware of candidly.
Performance
This is a rather tricky one, but the GPT-4 API takes an incredibly long time. For code generation, it depends on the number of layers, but it usually takes about 40–120 seconds. While we’re covering this with cat videos, people might still get bored.
There’s not much we can do about this except hope that OpenAI works hard to improve it, but if this becomes a significant challenge:
- Break it down so it can be solved with 3.5-turbo
- Cache the same prompts or already output layer structures
- Prepare something enjoyable or useful during waiting time
- Pray for the development of Web LLM
Also, currently, we’re running this as a plugin, but it might be worth considering a workflow like designers create feature designs, refactor designs, and then execute them all together through the Figma API.
Making It Easy to Create Structural Designs
One of the bottlenecks in using this plugin is the need to “create a structured design.”
In this case, we’re asking the OpenAI API to write code based on Figma’s layer data, and if Auto Layout is not used, layout information is not available, and if the layer structure is created in a strange way (e.g., a list is created as a single text), it will be reflected in the generated code as well.
Although I believe it’s generally better to have a well-structured design, as it leads to better maintainability for design itself, there might be situations where it’s not necessary to put in so much effort. However, the ability to generate code might actually incentivize putting more effort into creating well-structured designs. (From a designer’s perspective, they might not feel the benefits, which could lead to siloing and potential issues.)
By the way, what I think constitutes a “structured design” includes the following:
- No Group, Just use Frame
- Use Auto Layout for Frames by default
- Name components according to naming conventions
- Use Variants / Component Properties
- Apply Styles and/or TokenStudio properly
- Match layer structures to the code
- name layers appropriately
If these are done, the design should generally be in good shape. Here are some ideas to support this:
- Provide tutorials or guidelines to educate designers on creating well-structured designs that work well with the plugin.
- Implement features within the plugin to automatically detect and suggest improvements to the design structure.
- Collaborate with other tools or plugins that help designers create better structured designs more easily.
In conclusion, while there have been significant achievements in code generation from Figma designs, there are still challenges to address. As we continue to refine the plugin and work on these challenges, the goal is to create a tool that significantly streamlines the design-to-code process, making it easier for both designers and developers to collaborate effectively.
Extension of DesignLint
First, let’s think about a solution that relies as little as possible on humans.
Figma already has a plugin called Design Lint that tells you things like “Styles are not applied” and “Non-standard border-radius values are entered.”
Since it is published as OSS, you can fork it and add your own rules or features.
It’s quite easy, for example, if you want to add a rule that gets forbids a frame without Auto Layout, just add the following function:
export function checkAutoLayout(node, errors) {
if (node.type === 'FRAME' && node.layoutMode === 'NONE') {
return errors.push(
createErrorObject(
node,
'frame',
'Missing Auto Layout',
'Did you forget it?'
)
);
}
}
And simply add it to the place where the check functions for Frame in controller.ts are summarized.
function lintFrameRules(node) {
...
checkAutoLayout(node, errors);
tyreturn errors;
}
And one good news is that Daniel Destefanis, the creator of design lint and recently joined Figma!, is working on fixing feature.
Incorporate design refactoring into the workflow
I wrote about it in detail in the following article, but I think design is divided into a phase of hypothesis testing that you don’t want to create so rigidly, and a phase of structuring for future maintainability and collaboration.
For the latter, it might be a good idea to have a rule to refactor the design before handing it off to the engineer at the right time and then say “This is implementation ready.”
Create a model that outputs nice code even if it’s not structurally made
Another option is to see if there is any way to get proper code output even if Figma is not properly made.
One thing is that OpenAI will someday provide an API that can input images, so-called multimodal, so maybe if you give Figma’s drawing content it might be nice.
There are also various things on Hugging Face (I tried the following but it was not the expected quality) so maybe something will come out in the open.
Combining these things might make us happy. It might take too much time for an ML novice like me to try making it myself, so someone who sees this, please make it!
Use a style-less UI Kit
There are various UI Kits created in Figma Community that are style-less and have only structure. Examples include the following:
Things that can’t be expressed in Figma can’t have styles applied
Figma has become capable of realizing most things, but there are still some things that feel insufficient as frequently used CSS. The major ones would be:
- flex-wrap
- table layout
- grid layout
There’s not much you can do about things that don’t have information, so you’ll have to rely on Figma to work hard. Fortunately, flex-wrap seems to appear in Auto Layout v5.0 (the following is probably from someone who got the chance to try it early)
As for tables, they have already appeared in FigJam, and since they can be copied and pasted into Figma, I have a feeling they will be released in Figma eventually.
So there are challenges, but I’m arbitrarily expecting that the gap between implementation and design tool origins will almost disappear.
Handling multiple frames on the same page
When I actually started using this plugin in design, I thought, “What should I do with this?” when there are “multiple frames on the same page.”
For example, it is common for designs to be represented by multiple frames on the same page, as shown below, with loading displays or modals popping up.
It’s okay for humans to run the plugin only for differences and manually copy and paste, but if you could select frames together and have the code written for each state, it would be a huge boost in excitement, so I’d like to think about some way to implement it.
Continuously pursuing a better design process
I’ve been discussing the development of this plugin in detail so far, but to be honest, while creating it, I felt that it’s important to consider the question, “Do we really need to do design in Figma in the first place?”.
More specifically, the questions that come to mind are, “Does the task of creating a precise design equal the cost of coding it?” or “Can’t we just express design ideas directly in code?”.
Regarding the first point, you might want to create a more structured design or add metadata for better code generation output, but if you have to learn specific rules for design, wouldn’t it be better to just give up and write it in code?
Getting too caught up in the goal of “generating code from design” might not lead to overall process improvement, which would be counterproductive. It’s essential to always judge whether creating a detailed design not only benefits code generation but also contributes positively to design maintainability and other objectives.
On the other hand, I want to be open to the possibility that the design process itself may change significantly due to LLM.
For instance, currently, designers create designs from scratch (following component and design system rules), but in the future, we might see a UI design process where AI generates design proposals based on desired specifications and use cases, and we just need to fine-tune them for implementation.(like a experience of Midjourney)
When that happens, the design output from AI doesn’t necessarily have to be expressed in design tools like Figma. It could be output directly in code that users interact with.
It seems that jsngr, who has created numerous Figma plugins utilizing AI like Magician, is already starting to develop new design tools based on such AI-centric experiences.
When such new design processes emerge, the Figma-based code generation plugin that I’ve explained in this article might lose its value. However, I’d like to keep exploring better processes without being tied down by sunk costs.
Conclusion
So far, I’ve discussed the efforts to generate code using ChatGPT.
Personally, I feel that the experience of no longer having to write code for the visual part has come much closer to reality.
However, there are still many challenges, and while I’d like to try addressing them in the company I work for, I also think it would be more fun to have people from various companies try this and share insights on building better design-to-implementation processes. That’s why I made this plugin as an OSS.
Please give it a try and share your experiences, both good and bad. I would be delighted to hear from you.
Lastly, a promotional corner.
I work for a company called Gaudiy based in Tokyo, Japan. Gaudiy is actively recruiting product development talent. The company is investing in LLM, and both engineers and designers are focusing heavily on AI-driven business improvements, or so-called AIOps. If you’re excited about such initiatives, I think you’ll enjoy working here.
If you are interested, please contact from below!(sorry the page is in Japanese)
https://recruit.gaudiy.com/engineer
That’s all for now!