Audits whether the colors on a Figma frame or page are bound to design system tokens, and maps and binds hardcoded colors to the right token. Derives token-to-hex from bindings already in the file so every mapping has evidence, grades the results, and auto-proposes only the safe ones. Lint mode (check only) and bind mode are separate. Which library and which token groups come from the design_system section of figma-conventions.yaml; on auto it detects the library the file is connected to. Tri...
Scanned 9/2/2026
Install to Claude Code
npx -y skills add byjunyoung/claude-product-skills --skill tokens --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Tokens?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/byjunyoung-tokens)More formats (shields.io, HTML) on the badges page.
---
name: tokens
description: Audits whether the colors on a Figma frame or page are bound to design system tokens, and maps and binds hardcoded colors to the right token. Derives token-to-hex from bindings already in the file so every mapping has evidence, grades the results, and auto-proposes only the safe ones. Lint mode (check only) and bind mode are separate. Which library and which token groups come from the design_system section of figma-conventions.yaml; on auto it detects the library the file is connected to. Triggers - "/fig:tokens", "check the token bindings", "find unbound colors", "토큰 바인딩 검수", "하드코딩 색상 검사", "디자인시스템 연동 점검".
allowed-tools: AskUserQuestion, mcp__plugin_figma_figma__use_figma, mcp__plugin_figma_figma__search_design_system, mcp__plugin_figma_figma__get_screenshot, mcp__plugin_figma_figma__get_metadata, mcp__plugin_figma_figma__whoami
---
# fig:tokens — audit and repair design system token bindings
Checks whether the colors inside a frame are actually bound to design system variables, and connects unbound hardcoded colors to a token through **a mapping that has evidence behind it**. Theming, rebranding, and consistency only hold if every color goes through a token, so this runs as the last check before handoff.
**Prerequisites**: always load the `figma:figma-use` skill before calling `use_figma`.
**Seat check before the first write** — call `whoami` once. Where every plan it lists carries `seat: View`, stop before any `use_figma` write and say so: the reading half of this skill runs on a View seat, the writing half needs an Edit seat on the file's plan, and no retry changes that. Where the seats are mixed, go ahead — and if the first write comes back as a permission error, report the seat table and stop rather than retrying.
## When to invoke
- "check whether this frame's colors are all bound", "find colors that aren't on a variable"
- "convert the hardcoded hex values to tokens"
- The token check right before handoff, once the design is done
## When NOT to invoke
- Frame naming and section tidying → `/fig:prep`
- Structure and flow rule violations → `/fig:lint`
- Flow arrows → `/fig:arrows`
- Creating the tokens (variables) themselves → `figma:figma-generate-library`
- Designing screens → `figma:figma-generate-design`
## Inputs
- `figma_url` (required): the page or frame to audit
- `mode` (optional): lint mode (check only) / bind. Omitted, it checks and proposes a mapping table
The rules come from `figma-conventions.yaml` — read the `design_system` section (library, token groups, thresholds) with `resolve-config.py --js <fileKey>`.
## Why this needs its own skill — tool constraints
- **`get_metadata` cannot see colors.** Metadata carries node id, type, coordinates, and size, with no fill, stroke, or effect color. On a large frame it also blows the token limit and fails wholesale. So **all color inspection and binding walks nodes directly through a `use_figma` script.**
- **`search_design_system` does not return a token's hex.** It returns the name, key, scope, and owning library. The actual color has to be worked out separately (see below).
## Where token values come from — auto-detection plus reverse-derivation (the core)
A mapping is only as trustworthy as your knowledge of "what color is this token, really". Two routes run together.
1. **Detect the connected library**: query the token groups in `design_system.token_prefixes` with `search_design_system` to get names and keys. `design_system.library` decides the target, and on `auto` it detects the library the file is connected to. **Never hardcode a library name in this document** — it differs per file. When `token_prefixes` is empty, use whatever prefixes appear in the reverse-derived map (route 2) as candidates.
2. **Reverse-derive token-to-hex from existing bindings (the most reliable)**: walk the colors in the same file that are *already bound to a variable* and build a `{token name → actual hex}` map. A bound paint carries both the token id in `boundVariables.color` and the last resolved color in `paint.color`. That shows exactly what color this file actually renders that token as, which makes it the primary evidence for any mapping.
> **Caution — a token's real value may differ from the hex you were given.** "Change this grey to `fill/primary`" can still mean `fill/primary` is slightly different from that grey (which is fine if tokenizing is the goal). Whenever the color changes, **the preview must say "this token is #AAA, so the screen becomes #BBB".**
## Properties checked
fill · stroke · text color (a TEXT node's fills) · effect color (an effect's color) are all checked. Different properties match different token scopes — a background fill against `FRAME_FILL`/`SHAPE_FILL`, a border against `STROKE`, text against `TEXT_FILL`. **Only map a property to a token whose scope fits it** — never bind a text color to a border token.
## Excluded (untouched in both audit and binding)
| Target | Why |
|---|---|
| Colors **inside a component instance** | They are instance overrides, fragile, and really a separate issue to be tokenized on the source component in the library. The instance node's *own* color (an override) is in scope; its *children* are not |
| Working scaffolding | Nodes matching the scaffolding name conventions in `naming` (`arrow_delimiter`, `label_prefix`, `state_chain_prefix`) and anything painted with `section_style` or `placeholder_style` — those are `/fig:arrows` and `/fig:prep` output, not design to tokenize |
| User-designated protected areas | Archives, templates, and the like |
Excluded items that turn up are **reported, never fixed** — colors inside instances in particular get grouped separately as "a component repair issue".
## Grading — only auto-propose what is safe
Each unbound color is compared against the nearest token of the same property and graded.
| Grade | Basis | Handling |
|---|---|---|
| 1 — exact or near match | Identical to the token's hex, or within `design_system.match_threshold_channel` per channel | Binding effectively does not change the color → **auto-propose the mapping** |
| 2 — color shifts | Visibly different from the nearest token (off-palette) | Snapping changes the color → **report only; the user decides** |
| 3 — no matching token | Far from every token, or a brand or special color | Binding not advised, report only |
The threshold is a baseline, not a verdict — for borderline colors, show the delta so the user can judge.
## Lint mode — check only (no writes)
For "just check" requests, or the survey step before repair. Read-only, report only:
- Unbound colors in scope **aggregated by hex** (property, count, representative locations), classified as inside-instance / scaffolding / directly editable
- Directly editable colors get the grade table plus a recommended token and delta
- Nothing is changed; proceeding requires a separate go into the procedure below
## Procedure
### 1. Build the token dictionary
- Get names and keys for the configured token groups via `search_design_system`
- Walk the target frames and build the **existing binding → {token name, actual hex}** reverse map
- Merge both into a `{token name, key, scope, hex}` dictionary — the evidence table behind every mapping
### 2. Scan for unbound colors (read-only)
- Walk the target frames and aggregate **SOLID and unbound** colors from fills, strokes, text, and effects, by hex
- Classify each node as inside-instance / scaffolding / directly editable (see the snippet below)
- For the directly editable ones, compare against the dictionary to derive grade, recommended token, and delta → the mapping table
### 3. Settle the mapping (user's choice)
- Present grade 1 as a table. **When one hex matches two tokens with different meanings equally** (white matching both a static white and a background token, say), do not pick — present the difference in meaning and let the user choose
- Grades 2 and 3 are reported only; the user decides whether to proceed
- Any mapping that changes the color says so explicitly
### 4. Bind (preview → go, split into steps)
- Show the settled mapping as a **preview** (targets, hex → token, counts, exclusions) and get a **go**
- With a lot to do, **split by frame or by color** — never write it all at once
- Import tokens with `importVariableByKeyAsync`, and bind only unbound paints whose hex matches exactly
### 5. Verify (required)
- **Re-scan after binding** and confirm "unbound remainder among the targeted, directly editable colors = 0"
- The count from the aggregation step and the number actually bound may differ — the traversal boundary for nodes inside instances can shift between calls, changing the classification (inferred). **Prove nothing was missed by the re-scan reaching zero**, and report the discrepancy honestly
- Report remaining inside-instance colors and grades 2–3 separately, as out of scope for this pass
## Implementation snippets
Color to hex, and the inside-instance test:
```js
function hex(c){const f=x=>Math.round(x*255).toString(16).padStart(2,"0").toUpperCase();return "#"+f(c.r)+f(c.g)+f(c.b);}
function insideInstance(n){let p=n.parent;while(p){if(p.type==="INSTANCE")return true;p=p.parent;}return false;}
```
Reverse-deriving token to hex from existing bindings:
```js
const map={}; // varId -> {name, hexes:{}, count}
for(const id of ROOT_IDS){
const root=await figma.getNodeByIdAsync(id);
for(const n of [root,...root.findAll(()=>true)]){
for(const key of ["fills","strokes"]){
const arr=n[key]; if(!Array.isArray(arr)) continue;
for(const p of arr){
if(p.type==="SOLID"&&p.visible!==false&&p.boundVariables&&p.boundVariables.color){
const vid=p.boundVariables.color.id;
if(!map[vid]){const v=await figma.variables.getVariableByIdAsync(vid);map[vid]={name:v?v.name:"?",hexes:{},count:0};}
map[vid].count++; const h=hex(p.color); map[vid].hexes[h]=(map[vid].hexes[h]||0)+1;
}
}
}
}
}
return Object.values(map).map(e=>({name:e.name,count:e.count,hex:Object.keys(e.hexes).join(",")}));
```
Aggregating unbound colors (editable versus inside-instance):
```js
const agg={};
for(const id of ROOT_IDS){
const root=await figma.getNodeByIdAsync(id);
for(const n of [root,...root.findAll(()=>true)]){
const ii=insideInstance(n);
for(const key of ["fills","strokes"]){
const arr=n[key]; if(!Array.isArray(arr)) continue;
for(const p of arr){
if(p.type!=="SOLID"||p.visible===false) continue;
if(p.boundVariables&&p.boundVariables.color) continue;
const k=key+"|"+hex(p.color);
if(!agg[k]) agg[k]={prop:key,hex:hex(p.color),editable:0,inInstance:0,examples:[]};
agg[k][ii?"inInstance":"editable"]++;
if(agg[k].examples.length<4) agg[k].examples.push((ii?"⟨inst⟩":"")+n.name+" ["+n.type+"]");
}
}
}
}
return Object.values(agg).sort((a,b)=>b.editable-a.editable);
```
> Text colors arrive in `fills` above, on TEXT nodes. Effect colors are checked the same way through `n.effects[].color`, and bound through the effect-specific API (check the `setBoundVariableForEffect` family in the d.ts).
Binding (exact hex match only, reassigning a new paint array):
```js
const V={ tokenA: await figma.variables.importVariableByKeyAsync("KEY_A"), /* ... */ };
const fillMap={"#FFFFFF":V.tokenA, /* hex: variable */ };
const strokeMap={/* ... */};
const counts={};
const root=await figma.getNodeByIdAsync(FRAME_ID);
for(const n of [root,...root.findAll(()=>true)]){
if(insideInstance(n)) continue; // skip inside instances
for(const [key,mapObj] of [["fills",fillMap],["strokes",strokeMap]]){
const arr=n[key]; if(!Array.isArray(arr)||!arr.length) continue;
let changed=false;
const next=arr.map(p=>{
if(p.type==="SOLID"&&p.visible!==false&&!(p.boundVariables&&p.boundVariables.color)){
const v=mapObj[hex(p.color)];
if(v){changed=true;counts[v.name]=(counts[v.name]||0)+1;return figma.variables.setBoundVariableForPaint(p,"color",v);}
}
return p; // fills is read-only — build a new array with map
});
if(changed) n[key]=next;
}
}
return {frame:FRAME_ID, counts};
```
Verification (confirm zero remaining):
```js
let editableLeft=0;
for(const id of ROOT_IDS){
const root=await figma.getNodeByIdAsync(id);
for(const n of [root,...root.findAll(()=>true)]){
if(insideInstance(n)) continue;
for(const key of ["fills","strokes"]){
const arr=n[key]; if(!Array.isArray(arr)) continue;
for(const p of arr){
if(p.type==="SOLID"&&p.visible!==false&&!(p.boundVariables&&p.boundVariables.color)&&TARGET_HEXES.includes(hex(p.color))) editableLeft++;
}
}
}
}
return {editableLeft}; // must be 0 to be done
```
## Constraints
- **Preview → go before writing.** Settling the mapping and running the binding are separate; binding is split by frame or by color
- **Never touch colors inside instances or scaffolding** — report only
- Only map where the property and the token scope agree (text color to a text token, and so on)
- Any mapping that changes the color says so in the preview — never assume the color is unchanged from the token's name
- Keep each call small, verifying between steps with a re-scan or a screenshot
- Never write "done" without verification — prove zero remaining with a re-scan
## Notes
- The primary evidence for a mapping is always **what value this file actually renders that token as** (the reverse-derived map). Never infer from how a token's name sounds
- "Lots of unbound colors" does not automatically mean somebody forgot — off-palette colors may simply have found their way in, so grades 2 and 3 go back to design judgement
- A mass of hardcoded colors inside instances is not this skill's scope; raise it separately as **a component library repair task**
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!