figma_tokens_gen 1.1.0
figma_tokens_gen: ^1.1.0 copied to clipboard
Generate Flutter Color constants and palette maps from Figma design-token JSON exports. Ships a CLI and a customisable library API.
figma_tokens_gen #
Generate Flutter Color constants and palette maps from Figma design-token
JSON exports.
Point it at the JSON your design team exports, and get a checked-in Dart file
with one static const Color per token — no runtime parsing, no asset lookup,
no string keys in your widget code.
Container(color: AppColors.primaryMain);
Requirements #
| Version | |
|---|---|
| Dart SDK | 3.13.0 or newer |
| Flutter (for the generated code) | 3.47.0 or newer |
Flutter 3.47 moved the Material widgets into their own
material_ui package, so that is what
the generated file imports:
dependencies:
material_ui: ^1.0.0
On a project still importing package:flutter/material.dart, pass
--material-import package:flutter/material.dart and the generated file will
use the old import instead. The generator itself is pure Dart and depends on
neither.
Install #
As a dev dependency in the project you want to generate into:
dart pub add --dev figma_tokens_gen
Or add it to pubspec.yaml yourself. It is a build-time tool, so it belongs in
dev_dependencies — it never ships in your app:
dev_dependencies:
figma_tokens_gen: ^1.0.0
then:
dart pub get # or: flutter pub get
Or install it globally, to use across projects without adding a dependency:
dart pub global activate figma_tokens_gen
Use #
dart run figma_tokens_gen --input tokens --output lib/generated/theme
| Option | Default | Meaning |
|---|---|---|
-i, --input |
tokens |
Directory (searched recursively) or a single JSON file |
-o, --output |
lib/generated/theme |
Directory the generated file is written to |
--class-name |
AppColors |
Name of the constants class |
--palette-class-name |
AppColorPalette |
Name of the palette-map class |
--file-name |
app_colors.dart |
Name of the generated file |
--material-import |
package:material_ui/material_ui.dart |
Import the generated file uses for Color |
--no-palettes |
(palettes on) | Skip the Map<String, Color> class |
--strict |
off | Exit non-zero if any token was skipped |
-q, --quiet |
off | Suppress progress output (warnings still print) |
Input #
Every top-level object is a category; objects inside it are tokens, or further groups of tokens. Three value shapes are accepted, so you should not have to change your export settings:
{
"primary": {
"main": { "$value": { "hex": "#3B5BFF" }, "$type": "color" },
"light": { "$value": "#8FA3FF" },
"extraLight": "#E4E9FF"
}
}
- DTCG —
{"$value": ...}, the current format: the W3C draft that Figma's own variable export and recent Tokens Studio versions write - Legacy —
{"value": ...}, written by older Figma token plugins - Bare — the value directly
Both the current $-prefixed export and older exports work as-is, so upgrading
your Figma plugin does not require changing anything here — and the two shapes
can be mixed within a single file, which is what a partly re-exported token set
looks like in practice.
Colour values may be #RGB, #RGBA, #RRGGBB, #RRGGBBAA, a {"hex": ...}
map with an optional a field, or {"r":…, "g":…, "b":…, "a":…} channels in
either 0–1 or 0–255 form.
Non-colour tokens (spacing, typography) and plugin metadata keys ($extensions,
$themes, $metadata) are skipped.
Nested groups #
Groups nest to any depth, which is how Figma's own Variables export writes a
name like color/brand/primary. The top-level key stays the category and the
rest becomes the token name:
{
"color": {
"brand": { "primary": { "$value": "#3B5BFF" } },
"surface": { "$value": "#F7F8FA" }
}
}
static const Color colorBrandPrimary = Color(0xFF3B5BFF);
static const Color colorSurface = Color(0xFFF7F8FA);
The palette key drops the category the same way it always has, so the token
above is AppColorPalette.color['brandPrimary'].
Aliases #
A {group.token} value is followed to the token it names — the reference a
semantic layer uses to point at a primitive one:
{
"primitive": { "blue500": { "$value": "#3B5BFF" } },
"action": { "primary": { "$value": "{primitive.blue500}" } }
}
Both AppColors.primitiveBlue500 and AppColors.actionPrimary come out as
Color(0xFF3B5BFF). References resolve after every input file is merged, so
the primitive may live in a different file from the token pointing at it —
which is how Tokens Studio splits them. Chains and /-separated paths work
too.
A reference that names nothing, or a cycle, is reported rather than silently dropped:
Warning: Token "action/primary" references "{primitive.blue600}", which no
token defines. It was skipped.
Warnings go to stderr and print even under --quiet, because a skipped token
is a token missing from the generated file, not progress noise. The rest of the
tokens still generate; pass --strict to make a warning fail the run, which is
what you want in CI. Pass TokenParser(resolveAliases: false) to go back to
treating a reference as an ordinary unparseable value.
Output #
// GENERATED CODE - DO NOT MODIFY BY HAND
import 'package:material_ui/material_ui.dart';
abstract final class AppColors {
// primary
static const Color primaryMain = Color(0xFF3B5BFF);
static const Color primaryExtraLight = Color(0xFFE4E9FF);
}
/// The same tokens grouped by category, for lookup by name.
abstract final class AppColorPalette {
/// Tokens under the `primary` category.
static const Map<String, Color> primary = {
'main': Color(0xFF3B5BFF),
'extraLight': Color(0xFFE4E9FF),
};
}
Member names are category + Token in lowerCamelCase — primary/extraLight
becomes primaryExtraLight. Names that would collide with a Dart reserved word
get a trailing underscore; names starting with a digit get a $ prefix. Palette
map keys are strings, so they keep the token name as authored — a 500 token is
AppColorPalette.gray['500'].
Because nesting is flattened, two tokens can ask for the same name —
brand/primary and brandPrimary both want colorBrandPrimary. The second
one gets a 2 suffix and a comment in the generated file saying so, since two
members of one name (or two identical keys in a const map) would not compile.
The output contains no timestamp, so re-running the generator with unchanged tokens produces no diff.
Multiple files #
When --input is a directory, every .json file under it is read in sorted
path order and merged. Categories and nested groups combine; a token declared
twice is taken from the file that sorts last. Sorting is what makes the merge
deterministic across machines and CI.
The merge happens before aliases are resolved, so a reference can cross files in either direction — a semantic file may point at primitives that sort after it.
Library API #
Every stage is separately usable when the CLI is not enough — for example to fetch tokens over HTTP, or to emit a different shape of code:
import 'package:figma_tokens_gen/figma_tokens_gen.dart';
// Parse without touching the filesystem.
final tokens = const TokenParser().parseJson(jsonString);
print('${tokens.colorCount} colours in ${tokens.categories.length} categories');
for (final warning in tokens.warnings) print(warning);
// Or several documents at once, so aliases can resolve across them.
const parser = TokenParser();
final merged = parser.parseDocuments([
parser.documentOf(primitivesJson),
parser.documentOf(semanticJson),
]);
// Emit with your own class names.
final source = const DartColorsEmitter(className: 'BrandColors').emit(tokens);
// Or run the whole pipeline.
final result = await TokenConverter().convert(
inputPath: 'tokens',
outputPath: 'lib/generated/theme',
);
To generate something other than a Flutter colour class — a ThemeExtension,
CSS variables, Compose tokens — implement TokenEmitter and pass it to
TokenConverter. The parser and CLI need no changes.
Wiring it into a build #
Add it to whatever runs your codegen, next to build_runner:
dart run figma_tokens_gen -i tokens -o lib/generated/theme -q --strict
dart format lib/generated/theme
Commit the generated file. Checking it in keeps builds reproducible and makes token changes visible in review.
License #
MIT