agentsclimarketplace

Figma style binding

Skill senlindesign/claude2figma/.claude/skills/figma-style-binding

Triggers on any visual property change in Figma — creating text, setting colors, adjusting spacing/padding/gap/radius. Enforces that ALL values bind to Figma Styles or Variables, never hardcoded. Includes post-write QA verification.From its SKILL.md

Install
npx -y skills add senlindesign/claude2figma --skill figma-style-binding

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

SKILL.md

4.3 KB, as published. Nobody here has run it

Style Binding + QA

Every visual value must come from the design system. Supplements figma-generate-design. Prerequisite: figma-preflight must have run this session.


Binding Hierarchy

For any visual property, follow this order. Stop at the first match.

1. Connected Library  →  search_design_system → import → apply
2. Local Style        →  Style Registry → apply by ID
3. Local Variable     →  Variable Registry → apply by ID
4. Gap found          →  Report to user, wait for decision

Text

Every text node must use textStyleId. Individual font properties (fontSize, fontFamily, etc.) are forbidden.

const style = await figma.getStyleByIdAsync("<id>");
await figma.loadFontAsync(style.fontName);
node.textStyleId = "<id>";

If no local style matches, search libraries via search_design_system. If no match anywhere:

⚠️ Text style gap: no style for "[role]". Available: [top 5]. Use closest, or add missing style?

Color Fills

Every fill/stroke must bind to a COLOR Variable (preferred, supports theming) or Paint Style.

// Variable binding (preferred)
const variable = await figma.variables.getVariableByIdAsync("<id>");
const fill = { type: "SOLID", color: { r: 0, g: 0, b: 0 } };
node.fills = [figma.variables.setBoundVariableForPaint(fill, "color", variable)];

// Paint Style binding
node.fillStyleId = "<id>";

Never use raw { r, g, b } without a binding.


Spacing, Padding, Gap, Radius

Bind to FLOAT Variables. layoutMode must be set BEFORE setBoundVariable.

node.setBoundVariable("paddingTop", spacingVar);
node.setBoundVariable("paddingBottom", spacingVar);
node.setBoundVariable("paddingLeft", spacingVar);
node.setBoundVariable("paddingRight", spacingVar);
node.setBoundVariable("itemSpacing", spacingVar);
node.setBoundVariable("cornerRadius", radiusVar);

Spacing can fall back to raw values temporarily with user confirmation. Color and text cannot.


Forbidden / Required

ForbiddenRequired
node.fontSize = 24node.textStyleId = id
node.fills = [{ type: "SOLID", color: { r: .2, g: .4, b: 1 } }]Variable or Style binding
node.paddingLeft = 16node.setBoundVariable("paddingLeft", var)
node.cornerRadius = 8node.setBoundVariable("cornerRadius", var)
Creating a Button from scratchimportComponentByKeyAsync from library

QA Verification

After every use_figma call that creates or modifies nodes, run this verification on the returned node IDs:

const nodeIdsToAudit = [/* paste returned IDs */];
const results = [];

for (const id of nodeIdsToAudit) {
  const node = await figma.getNodeByIdAsync(id);
  if (!node) { results.push({ id, status: "NOT_FOUND" }); continue; }

  const checks = [];

  if (node.type === "TEXT") {
    checks.push({ prop: "textStyleId", bound: !!node.textStyleId });
  }

  if ("fills" in node && Array.isArray(node.fills) && node.fills.length > 0) {
    const bound = !!node.fillStyleId || (node.boundVariables?.fills?.length > 0);
    checks.push({ prop: "fills", bound });
  }

  if ("layoutMode" in node && node.layoutMode !== "NONE") {
    for (const p of ["paddingLeft","paddingRight","paddingTop","paddingBottom","itemSpacing"]) {
      if (p in node) checks.push({ prop: p, bound: !!(node.boundVariables && p in node.boundVariables) });
    }
  }

  if ("cornerRadius" in node && node.cornerRadius > 0) {
    checks.push({ prop: "cornerRadius", bound: !!(node.boundVariables && "cornerRadius" in node.boundVariables) });
  }

  const failed = checks.filter(c => !c.bound);
  results.push({ id, name: node.name, type: node.type, status: failed.length === 0 ? "PASS" : "FAIL", failed: failed.map(c => c.prop) });
}

return { auditResults: results };

If FAIL: Fix each unbound property using the binding rules above, then re-audit. Do not proceed to the next design step until all pass.

Report format:

✅ All [N] nodes passed.
// or
❌ FAIL "Card" (FRAME) — paddingTop, cornerRadius unbound. Fixing...

Keep looking

Skills are one crate of 627,801. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.