Figma Reader
Reads tokens from Figma and parses them into a unified format.
To start use figmaReader()
from @theemo/figma
. Here is a basic config as template:
import { figmaReader } from '@theemo/figma';
import { defineConfig } from '@theemo/cli';
const { FIGMA_SECRET } = process.env;
export default defineConfig({
sync: {
reader: {
sources: figmaReader({
secret: FIGMA_SECRET,
files: ['<your-figma-file-id>'],
parser: {
// ...
}
})
}
}
});
Connecting to Figma
The figmaReader()
is accessing your Figma file through their REST API, which requires an API Key. Go to your Figma settings and generate a new Personal Access Token.
Usage in Development
To use it in development, put your secret into a .env
file:
FIGMA_SECRET=
Theemo CLI will read from .env
files.
Usage in Production
For production, add it as secure environment variable in your CI provider.
Enterprise Plan
For Figma users on an enterprise plan, you can request more information from the REST API over users with a lower tier plan, such as variables.
WARNING
At the time of Writing, I never had the chance to access such an endpoint, so support is not built-in.
Please refer to Issue #691
Files
It is possible to collect tokes across multiple files. Add all the file IDs you wish to sync from into the files
array.
figmaReader
will parse tokens from each file and then merge them together.
Plugins
As much as Figma supports plugins, the same applies to figmaReader()
. Theemo in its basic usage does not rely on plugins. As Figma plugins are allowed to expose data through the Figma Rest API, you can write plugins, that consume this data to alter your tokens.
Theemo Plugin
When using the Theemo Plugin for Figma to export variables, you can use the Theemo plugin for figmaReader()
as its counterpart to consume those variables.
import { figmaReader, theemoPlugin } from '@theemo/figma';
import { defineConfig } from '@theemo/cli';
export default defineConfig({
sync: {
reader: {
sources: figmaReader({
plugins: [
theemoPlugin()
]
})
}
}
});
Adding the plugin as shown above will make variables available in sync mode.
Write Your own Plugin
Whether you are a Figma Plugin author and want to provide the sync counterpart to your plugin or you are are in need to consume a third party Figma plugin, you can write your own plugin.
Parser
The heart of figmaReader()
is the parser. The parser let's you read tokens from variables, styles and text nodes.
The flow for reading tokens from Figma is outlined here:
Customization
As per the flow graph above and in all examples on this page. The green functions is where you can intersect the default behavior and apply custom logic.
That is all default behavior is exported (for those where it is available). So you can safely write your customizations and defer to the default implementation for all other situations. All examples in this page work follow this idea and showcase the default behavior.
Variables
Variables are the most atomic representation of design tokens in Figma. You can read them with the theemo plugin
or enterprise plan. Here is the flow, how variables are parsed:
isTokenByVariable()
Tokens can be hidden from publishing within Figma. However, accessing through the REST API is exposing all variables. The default behavior mimics Figma's behavior. You have the chance to add some additional logic, custom to your system on checking which variables shall result in tokens or not.
import { figmaReader, isTokenByVariable } from '@theemo/figma';
import { defineConfig } from '@theemo/cli';
export default defineConfig({
sync: {
reader: {
sources: figmaReader({
// ...
parser: {
isTokenByVariable: (variable: FigmaVariable) => {
// implement your custom logic here
if (yourCustomLogic(variable)) {
return false;
}
// ...or else use default behavior
return isTokenByVariable(variable);
};
}
})
}
}
});
References
Default
import { isTokenByVariable } from '@theemo/figma';
API
getNameFromVariable()
Even though Figma shows hierarchy for your styles and variables, internally those are separated with a /
character, hence Figma disallows the usage of .
as part of the variable name, to align with DTCG Format as the .
is used as group separator. The default implementation takes care of this transformation. You may use this functions to apply name transformations, according to your token specification.
import { figmaReader, getNameFromVariable } from '@theemo/figma';
import { defineConfig } from '@theemo/cli';
export default defineConfig({
sync: {
reader: {
sources: figmaReader({
// ...
parser: {
getNameFromVariable: (variable: FigmaVariable) => {
// implement your custom logic here
if (variable.name.includes('primary')) {
return variable.name.replace('primary', 'ARE-YOU-SURE-WHAT-THAT-MEANS?');
}
// ... or else use default behavior
return getNameFromVariable(variable);
};
}
})
}
}
});
References
Default
import { getNameFromVariable } from '@theemo/figma';
API
considerMode()
Variables support different modes. Some are relevant for your design system, others you can completely ignore for synching tokens. As theemo cannot predict which ones you want to consider for exporting tokens, you can declare such.
INFO
It only applies to collections with more then one mode.
For considering your light
and dark
modes, here is how:
import { figmaReader } from '@theemo/figma';
import { defineConfig } from '@theemo/cli';
export default defineConfig({
sync: {
reader: {
sources: figmaReader({
parser: {
considerMode: (mode: string) => {
return ['light', 'dark'].includes(mode);
},
}
})
}
}
});
References
getConstraints()
As your modes can take arbitrary names, such as:
light
,dark
light-more
,light-less
,dark-more
,dark-less
sunrise
,day
,sunset
,night
For theemo it is impossible to guess the appropriate constraints. As such, you need to provide and explain the behavior of your system. The example above for considerMode()
, qualifies light
and dark
as appropriate modes, let's continue assigning their constraints:
import { figmaReader } from '@theemo/figma';
import { defineConfig } from '@theemo/cli';
export default defineConfig({
sync: {
reader: {
sources: figmaReader({
parser: {
getConstraints(mode: string/*, variable: FigmaVariable*/) {
if (mode === 'light' || mode === 'dark') {
return { features: { 'color-scheme': mode } };
}
}
}
})
}
}
});
References
Theemo has a very advanced and flexible system to describe constraints. Read more on features for themes.
Styles
Styles are used for composite design tokens in Figma. Theemo reads every style through the Figma API, that exists in your files.
API Reference
isTokenByStyle()
Each style found in the Figma file is passed into that function and being asked, whether this is a token or not.
The default implementation is, if the name of the token starts with a .
, it will respond false
. That is the default behavior of Figma, where you can't publish styles that begin with a .
(as it is like a hidden folder on a unix system).
You can use this function to apply your own behavior. Here in this case, styles are ignored, when they contain braces:
import { figmaReader, isTokenByStyle } from '@theemo/figma';
import { defineConfig } from '@theemo/cli';
import type { Style } from 'figma-api';
export default defineConfig({
sync: {
reader: {
sources: figmaReader({
parser: {
isTokenByStyle: (style: Style) => {
return !style.name.includes('(') && !style.name.includes(')') && isTokenByStyle(style);
}
}
})
}
}
});
References
Default
import { isTokenByStyle } from '@theemo/figma';
API
getNameFromStyle()
To actually get the name of a style. By default, it will pass through the name of the token as in Figma, replacing /
with .
.
For customizations, such as prefixing text styles with typography/
, see here:
import { figmaReader, getNameFromStyle } from '@theemo/figma';
import { defineConfig } from '@theemo/cli';
import type { Style } from 'figma-api';
export default defineConfig({
sync: {
reader: {
sources: figmaReader({
parser: {
getNameFromStyle(style: Style) {
if (style.styleType === 'TEXT') {
return getNameFromStyle({
...style,
name: `typography/${style.name}`
});
}
return getNameFromStyle(style);
},
}
})
}
}
});
References
Default
import { getNameFromStyle } from '@theemo/figma';
API
Text Nodes
Theemo is not only able to extract tokens from styles, but also from text nodes on the canvas itself.
WARNING
Theemo is older than Variables in Figma. Variables nowadays are a better fit and if you already use them, all good. Keep reading to support legacy systems.
Let's assume we use a modular scale system for our sizing, we only need base
and ratio
parameters for this.
Figure 1 shows such a sizing configuration in Figma. Notice the [token]
tags as suffix for the node names. This will serve as indicators to recognize such nodes as tokens in theemo.
Reference
isTokenByText()
By default no tokens are recognized, so we need to teach theemo to understand text nodes who include the [token]
tag:
import { figmaReader } from '@theemo/figma';
import { defineConfig } from '@theemo/cli';
import type { Node } from 'figma-api';
export default defineConfig({
sync: {
reader: {
sources: figmaReader({
// ...
parser: {
isTokenByText(node: Node<'TEXT'>) {
return node.name.includes('[token]');
}
}
})
}
}
});
References
Default
import { isTokenByText } from '@theemo/figma';
API
getNameFromText()
Next up is to actually get the name from the node we let pass earlier. Here we drop the [token]
tag to get the clean token name:
import { figmaReader } from '@theemo/figma';
import { defineConfig } from '@theemo/cli';
import type { Node } from 'figma-api';
export default defineConfig({
sync: {
reader: {
sources: figmaReader({
// ...
parser: {
getNameFromText(node: Node<'TEXT'>) {
return node.name.replace('[token]', '').trim();
}
}
})
}
}
});
References
Default
import { getNameFromText } from '@theemo/figma';
API
getValueFromText()
So far theemo is handling a node
which it knows it is a token and what the name is. Finally we need to get the actual value from the node. Theemo provides a default implementation by returning the characters
property from the node
(= the contents), which should already do it. However, you are free to overwrite this behavior at this point.
References
Default
import { getValueFromText } from '@theemo/figma';
API
Classify Tokens
Variables, Styles and Text Nodes will create an internal FigmaToken
. At the end of the process, FigmaToken
s will be compiled into Token
s and the result of the reader is to return an array of Token
s which will be put into the lexer. In the last classification step, you have the chance to do some cleanup or pass any data along with the token that are relevant to your setup.
getTypeFromToken()
While handling the tokens from reading and parsing the source, theemo tries to detect the type of a token, based on the related Figma construct (ie. Node
or Style
). You can provide your own customization to this:
import { figmaReader } from '@theemo/figma';
import { defineConfig } from '@theemo/cli';
export default defineConfig({
sync: {
reader: {
sources: figmaReader({
// ...
parser: {
getTypeFromToken: (token: FigmaToken) => {
if (token.style) {
return getTypefromStyle(token.style);
}
return '';
};
}
})
}
}
});
References
Default
import { getTypeFromToken } from '@theemo/figma';
API
getPropertiesForToken()
You might find yourself in the need to add additional properties to the token that is parsed from Figma. For example, you might want to include the Figma name of the related style to be able to show them both on the generated token documentation. Here is how:
import { figmaReader } from '@theemo/figma';
import { defineConfig } from '@theemo/cli';
export default defineConfig({
sync: {
reader: {
sources: figmaReader({
// ...
parser: {
getPropertiesForToken: (token: FigmaToken/*, document?: FigmaDocument*/) => {
if (token.figmaName) {
return {
figmaName: token.figmaName
};
}
};
}
})
}
}
});
You'll also receive the Figma document
as second parameter. With that you can perform your own lookups with the Figma document. Please refer to their REST API documention.
References