Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/temzasse/figmage

A simple CLI tool that helps you generate design tokens as code from your Figma project.
https://github.com/temzasse/figmage

cli design-system design-tokens figma

Last synced: 22 days ago
JSON representation

A simple CLI tool that helps you generate design tokens as code from your Figma project.

Awesome Lists containing this project

README

        


Figmage logo

# 🧙‍♂️ Figmage 🧙

A CLI tool that helps you generate design tokens as code from your Figma project.

## Installation

This tool can be used as a global package if you don't want to include it as a dependency in each of your projects:

```sh
npm install -g figmage
```

You can also utilize `npx` to run this tool without installing it:

```sh
npx figmage tokenize --config ./path/to/.figmage.json
```

Or you can install it locally in your project:

```sh
npm install figmage
```

### Environment variables

1. Get an [access token](https://www.figma.com/developers/api#access-tokens) for Figma API
2. Retrieve the file id of the Figma file
3. Create an `.env` file for [dotenv](https://github.com/motdotla/dotenv)
4. Paste the access token and the file id in the env file

```sh
FIGMA_ACCESS_TOKEN="xxxxx-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx"
FIGMA_FILE_ID="xxxxxxxxxxxxxxxxxxxxxx"
```

> [!TIP]
> You can change the env file path with the CLI `--env` or `-e` option.

## Configuration

Create a file called `.figmage.json` or `.figmagerc` in your project or add the config in your `package.json` under `"figmage"` key.

In addition to generic options the config has two concepts that map directly to the available commands: `tokenize` and `codegen`.

```js
{
"outDir": "tokens",
"tokenize": {},
"codegen": {}
}
```

> [!TIP]
> You can change the config file path with the CLI `--config` or `-c` option.

### Tokenize

Fetch meta data about your Figma project and turn them into a generic design token specification.

Command:

```sh
figmage tokenize
```

There are two different sources for generating design tokens from a Figma project:

1. **Local styles**
2. **Components inside frames**

Local styles are your basic variable-like styles that you use in Figma to assign colors, text styles, effects, etc. to layers.
Figmage can turn these styles into design tokens.

> [!CAUTION]
> Figmage doesn't yet support the new [Figma variables](https://www.figma.com/developers/api#variables) as they are only available for Enterprise org users via the REST API 😔

Figma components inside top-level frames can be used as sources for design tokens.
Figmage can measure various properties of a component and turn that property into a design token.
In order to get access to a component Figmage needs to know either the **ID** or the **name** of the frame that contains all the components that you want to measure and convert into design tokens.

> [!TIP]
> You can get the ID of a frame by selecting it and copying the `node-id` parameter value from the URL.

See [Supported tokens](#supported-tokens) section for more info about all the different token source types.

To define all the sources for design tokens that should be handled by Figmage add `tokens` property under `tokenize` in the Figmage config file:

```js
{
"tokenize": {
"tokens": [
{ "name": "colors", "type": "color" },
{ "name": "gradients", "type": "linear-gradient" },
{ "name": "typography", "type": "text" },
{ "name": "shadows", "type": "drop-shadow" },
// Reference frames by ID
{ "name": "icons", "nodeId": "x:x", "type": "svg" },
{ "name": "assets", "nodeId": "x:x", "type": "png" },
{ "name": "spacing", "nodeId": "x:x", "type": "height" },
{ "name": "elevation", "nodeId": "x:x", "type": "width" },
{ "name": "sizing", "nodeId": "x:x", "type": "dimensions" },
{ "name": "radii", "nodeId": "x:x", "type": "radius" }
// Or reference frames by their name
{ "name": "icons", "nodeName": "My Icons", "type": "svg" },
{ "name": "assets", "nodeName": "My Assets", "type": "png" },
{ "name": "spacing", "nodeName": "My Spacing", "type": "height" },
{ "name": "elevation", "nodeName": "My Elevation", "type": "width" },
{ "name": "sizing", "nodeName": "My Dimensions", "type": "dimensions" },
{ "name": "radii", "nodeName": "My Radii", "type": "radius" }
]
}
}
```

#### Grouping

Figmage will automatically group tokens based on top-level folders in Figma. For example if you have grouped all your colors inside `Light` and `Dark` folders or text styles inside `Web` and `Native` folders they will be also separated into respective groups during tokenization and code generation. This grouping logic applies to any type of token so you can also group things like spacing, sizing, radii, etc. scales by adding the group name to the layer name: `/`.

#### Supported tokens

> [!IMPORTANT]
> For all tokens that are not variable-like styles in Figma (colors, text styles, or effects) you need to turn the layer you want to target into a **component**! You can turn a layer into a component via ⌥⌘K (option+command+K). Figmage will ignore all layers inside a frame that are **not** components.

| Property | Description |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `color` | [Fill color styles](https://help.figma.com/hc/en-us/articles/360038746534-Create-styles-for-colors-text-effects-and-layout-grids#Colors_paints) |
| `linear-gradient` | [Linear gradient color styles](https://help.figma.com/hc/en-us/articles/360038746534-Create-styles-for-colors-text-effects-and-layout-grids#Colors_paints) |
| `text` | [Text styles](https://help.figma.com/hc/en-us/articles/360038746534-Create-styles-for-colors-text-effects-and-layout-grids#Text) |
| `drop-shadow` | [Drop shadow effect](https://help.figma.com/hc/en-us/articles/360038746534-Create-styles-for-colors-text-effects-and-layout-grids#Effects) |
| `width` | Width of the component |
| `height` | Height of the component |
| `dimensions` | Both width and height of the component |
| `radius` | Corner radius of the component |
| `svg` | Vector graphics component (eg. an icon) |

##### Colors

Color tokens are parsed from the color styles in Figma.

> [!NOTE]
> You don't need to define a node id or node name for colors.

```js
{
"tokenize": {
"tokens": [
{ "name": "colors", "type": "color" }
// Other tokens...
]
}
}
```

##### Typography

Typography tokens are parsed from the text styles in Figma.

> [!NOTE]
> You don't need to define a node id or node name for typography.

```js
{
"tokenize": {
"tokens": [
{ "name": "typography", "type": "text" }
// Other tokens...
]
}
}
```

##### Effects

Effect tokens are parsed from the effect styles in Figma.

Effects in Figma include things like drop/inner shadows and blurs. **Figmage currently only supports drop shadows!**

> [!NOTE]
> You don't need to define a node id or node name for effects.

```js
{
"tokenize": {
"tokens": [
{ "name": "shadows", "type": "drop-shadow" }
// Other tokens...
]
}
}
```

##### Dimensions

Measure a property or multiple properties of a Figma component and use the measured value(s) as a design token.

Supported types: `width` | `height` | `dimensions`.

You can use dimension based tokens for things like spacing and sizing.

> [!NOTE]
> The `dimension` type will produce a token like: `{ width: number, height: number }`

```js
{
"tokenize": {
"tokens": [
{ "name": "spacing", "type": "height", "nodeId|nodeName": "xxx" },
{ "name": "elevation", "type": "width", "nodeId|nodeName": "xxx" },
{ "name": "sizing", "type": "dimensions", "nodeId|nodeName": "xxx" }
// Other tokens...
]
}
}
```

##### Corner radius

Measure the corner radius of a Figma component as a design token.

```js
{
"tokenize": {
"tokens": [
{ "name": "radii", "nodeId|nodeName": "xxx", "type": "radius" }
// Other tokens...
]
}
}
```

##### Image assets

```js
{
"tokenize": {
"tokens": [
{ "name": "icons", "nodeId|nodeName": "xxx", "type": "svg" }
{ "name": "assets", "nodeId|nodeName": "xxx", "type": "png" }
// Other tokens...
]
}
}
```

#### Output example

Below you can see how the output of `figma tokenize` looks like based on the example configuration.

See example JSON

```json
{
"colors": {
"dark": {
"Muted 4": "#3a3a3c",
"Press Highlight": "rgba(150, 150, 150, 0.2)",
"Focus Ring": "#009a48",
"Error Muted": "#3e1c1d",
"Error": "#ef4444",
"Muted 3": "#48484a",
"Elevated": "#333333",
"Success": "#10b981",
"Warn Text": "#ffc93d",
"Primary Text": "#1cff87",
"Warn Muted": "#40351a",
"Error Text": "#ff7070",
"Muted 2": "#636366",
"Primary": "#009a48",
"Primary Muted": "#24392a",
"Muted 1": "#8e8e93",
"Muted 6": "#1d1d1f",
"Text": "#ffffff",
"Surface": "#222222",
"Backdrop": "rgba(0, 0, 0, 0.5)",
"Background": "#111111",
"Info": "#3b82f6",
"Info Muted": "#1b2940",
"Success Text": "#1ee8a5",
"Muted 5": "#2c2c2e",
"Text Muted": "#999999",
"Info Text": "#81aef7",
"Hover Highlight": "rgba(150, 150, 150, 0.08)",
"Success Muted": "#193328",
"Warn": "#fbbf24",
"Border": "rgba(150, 150, 150, 0.3)"
},
"light": {
"Muted 1": "#8e8e93",
"Error": "#ef4444",
"Primary Muted": "#d6ebdb",
"Warn Text": "#8a6200",
"Text Muted": "#666666",
"Muted 4": "#d1d1d6",
"Muted 6": "#f2f2f7",
"Muted 3": "#c7c7cc",
"Text": "#222222",
"Surface": "#ffffff",
"Info": "#3b82f6",
"Elevated": "#ffffff",
"Success": "#10b981",
"Muted 2": "#aeaeb2",
"Info Text": "#0a45a6",
"Info Muted": "#cfdef7",
"Success Muted": "#cee8df",
"Success Text": "#06734e",
"Error Muted": "#f3d2d3",
"Background": "#f3f4f6",
"Primary": "#009a48",
"Warn Muted": "#f3ead1",
"Primary Text": "#015227",
"Backdrop": "rgba(0, 0, 0, 0.5)",
"Press Highlight": "rgba(150, 150, 150, 0.2)",
"Border": "rgba(150, 150, 150, 0.3)",
"Error Text": "#8c0606",
"Focus Ring": "#009a48",
"Warn": "#fbbf24",
"Hover Highlight": "rgba(150, 150, 150, 0.1)",
"Muted 5": "#e5e5ea"
}
},
"typography": {
"web": {
"Title 2": {
"fontFamily": "Inter",
"fontWeight": 700,
"fontSize": 32,
"textTransform": "none",
"letterSpacing": 0,
"lineHeight": 1.172
},
"Body Large": {
"fontFamily": "Inter",
"fontWeight": 400,
"fontSize": 18,
"textTransform": "none",
"letterSpacing": 0,
"lineHeight": 1.172
},
"Caption": {
"fontFamily": "Inter",
"fontWeight": 400,
"fontSize": 10,
"textTransform": "none",
"letterSpacing": 0,
"lineHeight": 1.172
},
"Subtitle": {
"fontFamily": "Inter",
"fontWeight": 700,
"fontSize": 16,
"textTransform": "none",
"letterSpacing": 0,
"lineHeight": 1.172
},
"Body": {
"fontFamily": "Inter",
"fontWeight": 400,
"fontSize": 16,
"textTransform": "none",
"letterSpacing": 0,
"lineHeight": 1.172
},
"Title 1": {
"fontFamily": "Inter",
"fontWeight": 700,
"fontSize": 48,
"textTransform": "none",
"letterSpacing": 0,
"lineHeight": 1.172
},
"Overline": {
"fontFamily": "Inter",
"fontWeight": 400,
"fontSize": 10,
"textTransform": "uppercase",
"letterSpacing": 0.5,
"lineHeight": 1.172
},
"Title 3": {
"fontFamily": "Inter",
"fontWeight": 700,
"fontSize": 24,
"textTransform": "none",
"letterSpacing": 0,
"lineHeight": 1.172
},
"Body Small": {
"fontFamily": "Inter",
"fontWeight": 400,
"fontSize": 12,
"textTransform": "none",
"letterSpacing": 0,
"lineHeight": 1.172
}
},
"native": {
"Subtitle": {
"fontFamily": "Inter",
"fontWeight": 700,
"fontSize": 16,
"textTransform": "none",
"letterSpacing": 0,
"lineHeight": 1.172
},
"Overline": {
"fontFamily": "Inter",
"fontWeight": 400,
"fontSize": 10,
"textTransform": "uppercase",
"letterSpacing": 0.5,
"lineHeight": 1.172
},
"Title 3": {
"fontFamily": "Inter",
"fontWeight": 700,
"fontSize": 24,
"textTransform": "none",
"letterSpacing": 0,
"lineHeight": 1.172
},
"Caption": {
"fontFamily": "Inter",
"fontWeight": 400,
"fontSize": 10,
"textTransform": "none",
"letterSpacing": 0,
"lineHeight": 1.172
},
"Title 2": {
"fontFamily": "Inter",
"fontWeight": 700,
"fontSize": 32,
"textTransform": "none",
"letterSpacing": 0,
"lineHeight": 1.172
},
"Title 1": {
"fontFamily": "Inter",
"fontWeight": 700,
"fontSize": 48,
"textTransform": "none",
"letterSpacing": 0,
"lineHeight": 1.172
},
"Body Small": {
"fontFamily": "Inter",
"fontWeight": 400,
"fontSize": 12,
"textTransform": "none",
"letterSpacing": 0,
"lineHeight": 1.172
},
"Body Large": {
"fontFamily": "Inter",
"fontWeight": 400,
"fontSize": 18,
"textTransform": "none",
"letterSpacing": 0,
"lineHeight": 1.172
},
"Body": {
"fontFamily": "Inter",
"fontWeight": 400,
"fontSize": 16,
"textTransform": "none",
"letterSpacing": 0,
"lineHeight": 1.172
}
}
},
"shadows": {
"Shadow Small": {
"boxShadow": "0px 2px 6px rgba(0, 0, 0, 0.12)",
"offset": {
"x": 0,
"y": 2
},
"radius": 6,
"opacity": 0.12,
"color": {
"hex": "#000000",
"rgba": "rgba(0, 0, 0, 0.12)"
}
},
"Shadow Normal": {
"boxShadow": "0px 4px 16px rgba(0, 0, 0, 0.12)",
"offset": {
"x": 0,
"y": 4
},
"radius": 16,
"opacity": 0.12,
"color": {
"hex": "#000000",
"rgba": "rgba(0, 0, 0, 0.12)"
}
},
"Shadow Medium": {
"boxShadow": "0px 8px 24px rgba(0, 0, 0, 0.12)",
"offset": {
"x": 0,
"y": 8
},
"radius": 24,
"opacity": 0.12,
"color": {
"hex": "#000000",
"rgba": "rgba(0, 0, 0, 0.12)"
}
},
"Shadow Large": {
"boxShadow": "0px 16px 32px rgba(0, 0, 0, 0.12)",
"offset": {
"x": 0,
"y": 16
},
"radius": 32,
"opacity": 0.12,
"color": {
"hex": "#000000",
"rgba": "rgba(0, 0, 0, 0.12)"
}
}
},
"icons": {
"Check Circle": "",
"Arrow Down Left": "",
"Arrow Left Circle": "",
"Aperture": "",
"Camera": "",
"Eye": "",
"Cloud Lightning": "",
"Heart": ""
},
"spacing": {
"Xxsmall": 4,
"Xsmall": 8,
"Small": 12,
"Normal": 16,
"Medium": 24,
"Large": 32,
"Xlarge": 48,
"Xxlarge": 56,
"Xxxlarge": 72
},
"sizing": {
"Icon Size Large": 32,
"Icon Size Normal": 24,
"Icon Size Small": 16,
"Focus Ring Size": 1,
"Focus Ring Offset": 2,
"Button Height Small": 32,
"Button Height Normal": 44,
"Button Height Large": 60,
"Button Padding Horizontal Large": 28,
"Button Padding Horizontal Normal": 24,
"Button Padding Horizontal Small ": 16
},
"radii": {
"Full": 24,
"Large": 999,
"Medium": 16,
"Normal": 8,
"Small": 4
}
}
```

### Codegen

Generate code from the output of `figmage tokenize` (design token specification).

Command:

```sh
figmage codegen
```

The `codegen` property allows you to modify the code generation behaviour.

You can configure codegen for all tokens under `"defaults"` key and also for each token type separately by defining the configuration under the token type name.

```json
{
"codegen": {
"defaults": {
"filetype": "ts",
"tokenCase": "camel"
},
"typography": {
"filetype": "json",
"tokenCase": "kebab"
},
"icons": {
"dirname": "icons",
"filetype": "svg",
"tokenCase": "kebab"
},
"assets": {
"filetype": "png",
"tokenCase": "kebab"
}
}
}
```

#### Available options

| Field | Type | Description |
| -------------------- | ------------- | ----------------------------------------------------------------- |
| `defaults.filetype` | `Filetype` | File type for all tokens by default |
| `defaults.tokenCase` | `Casing` | Casing used for all tokens by default |
| `defaults.include` | `IncludeRule` | What Figma variables and groups should be included for all tokens |
| `defaults.exclude` | `ExcludeRule` | What Figma variables and groups should be excluded for all tokens |
| `[token].filename` | `string` | Filename for the token (defaults to token's name) |
| `[token].filetype` | `Filetype` | File type for this token |
| `[token].tokenCase` | `Casing` | Casing used for this token |
| `[token].include` | `IncludeRule` | What Figma variables and groups should be included for this token |
| `[token].exclude` | `ExcludeRule` | What Figma variables and groups should be excluded for this token |

```ts
type Casing = "camel" | "kebab" | "snake" | "lower";

type Filetype = "ts" | "js" | "json" | "svg" | "png";

// For example: "\\bFoobar\\b$" (NOTE: that you don't need the wrapping //)
type EscapedRegexString = string;

type ExactMatchString = string;

type Rule = EscapedRegexString | ExactMatchString[];

type IncludeRule = {
include: {
group: Rule;
token: Rule;
};
};

type ExcludeRule = {
exclude: {
group: Rule;
token: Rule;
};
};
```

#### Including and excluding

For example if you want to apply rules for all tokens:

```js
{
"codegen": {
"defaults": {
"include": {
// Only include variable groups that have a name of "System"
"group": "\\bSystem\\b$",
// Only include variables which name starts with "System"
"token": "^\\bSystem\\b.*"
}
}
}
}
```

Or if you want to apply rules for a specific token types only:

```js
{
"codegen": {
"colors": {
"exclude": {
// Exclude all colors which name starts with "Figma"
"token": "^\\bFigma\\b.*"
}
},
"typography": {
"include": {
// Only include text style groups that have a name of "Web"
"group": ["Web"]
}
}
}
}
```

#### SVG icon spritesheet

For SVG icons it is possible to bundle all generated SVG tokens into one SVG spritesheet
to improve performance by not including SVGs in your JS bundle.

You can achieve this by setting the `sprite` key to `true` for the token that has `"filetype": "svg"` - note that this only really useful for icons.

It is also possible to set the value to `{ writeIds: true }` if you want to write the token names which are used as ids of the SVG sprite parts into a separate file (for usage in TypeScript etc.).

> [!IMPORTANT]
> By setting the `sprite` key only one SVG file will be generated instead of multiple separate SVG files.

```js
{
"codegen": {
"icons": {
"filetype": "svg",
"filename": "icon-sprite",
"sprite": {
"writeIds": true
}
}
}
}
```

With the config above Figmage would generate two files: `icon-sprite.svg` and `icon-sprite-ids.ts`.

The following options are available for spritesheets:

```ts
"sprite": {
"writeIds": boolean,
"idsFilename": string, // name of the generated TS file for token name ids
"spriteDir": string, // where to write the sprite SVG file
}
```

### Standalone SVG icon spritesheet

If you dont' have your SVG icons in Figma but instead you have some existing SVG files in your project that you want compile into a spritesheet you can use the `figmage spritesheet` command:

```sh
figmage spritesheet --sprite-input ./icons --sprite-out-dir ./public --sprite-ids-out-dir ./app/components
```

The `spritesheet` command expects that you have your icons under the `--sprite-input` directory like so:

```txt
└── icons
├── arrow-right.svg
├── arrow-left.svg
├── twitter.svg
├── github.svg
└── home.svg
```

The output of the command will look like this:

```txt
├── public
│ ├── ...other public assets...
│ └── icons-sprite.svg
├── app
│ └── components
│ ├── ...your components...
│ └── icons-ids.ts
```

Figmage will use the name of the directory where the icons are read from as the filename suffix to generate:

- `${dirname}-sprite.svg`
- `${dirname}-ids.ts`

The `figmage spritesheet` accepts the following flags:

```sh
# Directory where input SVG files are (required)
--sprite-input

# Where to write the SVG spritesheet (required)
--sprite-out-dir

# Where to store the TS file for icon name ids (optional)
--sprite-ids-out-dir

# Change icon name casing: kebab (default), camel, snake, or lower (optional)
--sprite-case
```

## Figma template

In the screenshot below you can see how the example Figma template looks like that is used in the `/example` folder of this repo.

> ⚠️ TODO: add instructions about Figma.


Example Figma template