首页
/ Novu Figma 插件技能:用 Plugin API 程序化创建与管理文本样式的完整模式

Novu Figma 插件技能:用 Plugin API 程序化创建与管理文本样式的完整模式

2026-09-05 20:28:52作者:裴麒琰

figma-use 技能 的参考体系中,text-style-patterns.md 是处理字体排印(typography)的权威参考:它给出了用 Figma Plugin API 通过 use_figma 工具在文件上下文中执行 JavaScript、创建文本样式(TextStyle)、批量建立字号阶梯(Type Ramp)、导入团队库样式并将样式批量应用到文本节点的完整代码模式。读完本文,你可以直接复用这些可运行脚本片段,在 Figma 文件中程序化地搭建与代码库 token 对齐的完整字体排印系统,并规避「字体未加载」「lineHeight 传裸数字」等高频报错。

文本样式(TextStyle)的模型与可写属性

Figma 中的文本样式是「具名、可复用的字体排印定义」,等价于设计 token 库中的字号阶梯(type ramp):一个 TextStyle 把字体族、字号、字重、行高、字距等排印属性打包进一个命名实体,可整体应用到文本节点上。它与设计变量(variables)是两种东西——Figma 没有「复合变量」类型,不能把整套排印塞进一个变量;但文本样式上的单个属性可以绑定变量(例如把 fontSize 绑到尺寸变量、fontFamily 绑到字符串变量),从而让样式参与 token 体系。这一点在 wwds-text-styles.md 中进一步展开。

从类型定义看,plugin-api-standalone.d.tsinterface TextStyle extends BaseStyleMixin 声明了完整的可写字段:

属性 类型 说明
name string / 分隔做分组(如 "Heading/XL"),斜杠仅是 UI 展示手段
fontSize number 单位:像素
fontName FontName { family, style }——设置前必须先 loadFontAsync 加载字体
letterSpacing LetterSpacing { value: number, unit: 'PIXELS' | 'PERCENT' }
lineHeight LineHeight { value: number, unit: 'PIXELS' | 'PERCENT' }{ unit: 'AUTO' }
textCase TextCase 'ORIGINAL' | 'UPPER' | 'LOWER' | 'TITLE' | 'SMALL_CAPS'
textDecoration TextDecoration 'NONE' | 'UNDERLINE' | 'STRIKETHROUGH'
paragraphSpacing / paragraphIndent number 段落间距 / 段落缩进
description string 继承自 BaseStyleMixin,常用来记录对应的 CSS 变量名

两个最容易踩坑的字段是 lineHeightletterSpacing。它们的类型定义(plugin-api-standalone.d.tsLetterSpacingLineHeight)都要求对象而不是裸数字:

// 错误——裸数字会抛异常
style.lineHeight = 1.5;
style.letterSpacing = 0;

// 正确
style.lineHeight = { unit: "AUTO" };                 // 自动行高
style.lineHeight = { value: 24, unit: "PIXELS" };   // 固定 24px
style.lineHeight = { value: 150, unit: "PERCENT" }; // 150% 行高

style.letterSpacing = { value: 0, unit: "PIXELS" };  // 无字距
style.letterSpacing = { value: -2, unit: "PIXELS" }; // 收紧字距
style.letterSpacing = { value: 5, unit: "PERCENT" }; // 百分比字距

读回 lineHeight 时务必先判断 unit——{ unit: 'AUTO' } 形式没有 value,直接访问会拿到 undefined。同样的规则也适用于 TextNode 的同名属性,与 gotchas.md 中 "lineHeight and letterSpacing must be objects, not bare numbers" 一节一致。

列出本地文本样式

figma.getLocalTextStylesAsync() 返回文件中所有本地文本样式(getLocalTextStyles() 同步版已废弃,且当插件清单包含 "documentAccess": "dynamic-page" 时会直接抛异常,见 plugin-api-standalone.d.ts 中的 @deprecated 标注)。文档给出的标准列表面板脚本:

/**
 * 列出所有本地文本样式及其关键属性。
 *
 * @returns {Promise<Array<{id: string, name: string, key: string, fontSize: number, fontName: FontName, lineHeight: LineHeight, letterSpacing: LetterSpacing}>>}
 */
async function listTextStyles() {
  const styles = await figma.getLocalTextStylesAsync();
  return styles.map(s => ({
    id: s.id,
    name: s.name,
    key: s.key,
    fontSize: s.fontSize,
    fontName: s.fontName,
    lineHeight: s.lineHeight,
    letterSpacing: s.letterSpacing
  }));
}

// 可直接运行的完整脚本:
const results = await listTextStyles();
return results;

注意 use_figma 的执行约定:代码自动包裹在 async 上下文中,输出只能靠顶层 returnconsole.log() 不会回传),且 figma.notify() 会抛 "not implemented"——这些规则定义在 SKILL.md 的 Critical Rules 一节。另外文本样式名称并不唯一(两个样式可以同名),查找已知样式时应当按 idkey 匹配,而不是仅靠 name

创建文本样式:字体加载是前置硬条件

fontName 的设置有一个硬前提:对应字体必须先用 figma.loadFontAsync 加载。文档给出的完整属性创建函数:

/**
 * 创建一个设置好全部排印属性的文本样式。
 * 调用前字体必须已经加载。
 *
 * @param {string} name - 斜杠分隔的名称,如 "body/base"
 * @param {{ family: string, style: string }} fontName
 * @param {number} fontSize - 像素
 * @param {{ value: number, unit: 'PIXELS' | 'PERCENT' } | { unit: 'AUTO' }} lineHeight
 * @param {{ value: number, unit: 'PIXELS' | 'PERCENT' }} [letterSpacing]
 * @param {string} [description] - 例如对应 CSS 变量 "CSS: var(--font-body-base)"
 * @returns {TextStyle}
 */
function createTextStyleFull(name, fontName, fontSize, lineHeight, letterSpacing, description) {
  const style = figma.createTextStyle();
  style.name = name;
  style.fontName = fontName;
  style.fontSize = fontSize;
  style.lineHeight = lineHeight; // { unit: 'AUTO' } | { value, unit: 'PIXELS'|'PERCENT' }
  if (letterSpacing) style.letterSpacing = letterSpacing;
  if (description) style.description = description;
  return style;
}

description 字段值得注意:文档推荐把它写成 CSS: var(--font-body-base) 这样的形式,把 Figma 样式和代码库里的 CSS 变量显式关联起来——这是设计 token 与前端代码保持同步的实用技巧。

发现可用字体样式:不要猜测字体名

字体样式名(style string)因字体提供商和具体 Figma 文件而异——例如 "SemiBold""Semi Bold" 是两种真实存在的写法差异。文档的要求很明确:用 figma.listAvailableFontsAsync() 发现精确的样式字符串,永远不要凭记忆猜测,也不要用 try/catch 逐个试探

/**
 * 用 listAvailableFontsAsync 发现指定字体族可用的样式名。
 *
 * @param {string} family - 字体族名,如 "Inter"
 * @returns {Promise<string[]>} - 该字体族所有可用样式名
 */
async function getAvailableFontStyles(family) {
  const allFonts = await figma.listAvailableFontsAsync();
  return allFonts
    .filter(f => f.fontName.family === family)
    .map(f => f.fontName.style);
}

从类型定义看,listAvailableFontsAsync() 返回 Promise<Font[]>,其中 Font = { fontName: FontName }plugin-api-standalone.d.ts)。loadFontAsync(fontName: FontName) 的结果是有缓存的——重复加载同一字体不会重新从磁盘取,但每次调用仍是一个 Promise 往返,所以不应在循环里对同一字体反复调用(类型文件 L1621-L1625 的注释明确说明了这一点)。SKILL.md 更进一步强调:只要文档里已存在文本节点,脚本开头就应预加载这些节点用到的所有字体——因为 appendChildfindAll 回调等许多操作在涉及未加载字体时同样会失败,而不仅是「改文本内容」时。

批量创建字号阶梯(Type Ramp)

这是文档中最有实战价值的多步模式:从 token 定义数组一次性创建完整字号阶梯,内部处理了字体加载、去重和幂等性(重复执行不会创建重名样式)。每条形目的格式为 [name, fontFamily, fontStyle, fontSize_px, lineHeight, cssVar]

/**
 * 从 token 定义数组创建完整字号阶梯。
 * 处理字体加载、去重与幂等性。
 *
 * 每条目: [name, fontFamily, fontStyle, fontSize_px, lineHeight, cssVar]
 *   - lineHeight: { unit: 'AUTO' } 或 { value: number, unit: 'PIXELS' | 'PERCENT' }
 *
 * @param {Array} defs - [name, fontFamily, fontStyle, fontSize, lineHeight, cssVar] 元组数组
 * @returns {Promise<{ created: string[], skipped: string[] }>}
 */
async function createTypeRamp(defs) {
  const uniqueFonts = new Set();
  for (const [, family, style] of defs) {
    uniqueFonts.add(JSON.stringify({ family, style }));
  }
  await Promise.all(
    [...uniqueFonts].map(f => figma.loadFontAsync(JSON.parse(f)))
  );

  const existing = new Set(
    (await figma.getLocalTextStylesAsync()).map(s => s.name)
  );

  const created = [];
  const skipped = [];

  for (const [name, family, style, fontSize, lineHeight, cssVar] of defs) {
    if (existing.has(name)) {
      skipped.push(name);
      continue;
    }
    const ts = figma.createTextStyle();
    ts.name = name;
    ts.fontName = { family, style };
    ts.fontSize = fontSize;
    ts.lineHeight = lineHeight ?? { unit: 'AUTO' };
    if (cssVar) ts.description = `CSS: var(${cssVar})`;
    created.push(name);
  }

  return { created, skipped };
}

配套的可运行脚本,展示了一个覆盖标题、正文、代码字体的典型阶梯:

const defs = [
  ['heading/xl', 'Inter', 'Bold',      48, { unit: 'PIXELS', value: 56 }, '--font-heading-xl'],
  ['heading/lg', 'Inter', 'Bold',      36, { unit: 'PIXELS', value: 44 }, '--font-heading-lg'],
  ['body/base',  'Inter', 'Regular',   16, { unit: 'AUTO' },              '--font-body-base'],
  ['body/sm',    'Inter', 'Regular',   14, { unit: 'AUTO' },              '--font-body-sm'],
  ['code/base',  'Roboto Mono', 'Regular', 14, { unit: 'AUTO' },          '--font-code-base'],
];
const result = await createTypeRamp(defs);
return result;

值得拆解的三个工程细节:

  • 字体去重加载uniqueFontsJSON.stringify({ family, style }) 做集合键,Promise.all 并发加载。阶梯里 heading/xlheading/lg 共用同一字体,只加载一次;
  • 幂等性:创建前先 getLocalTextStylesAsync() 拉取现有样式名集合,已存在的名字进入 skipped 直接跳过——脚本可以安全地重复执行;
  • 返回值可追踪return { created, skipped } 符合技能要求「每个创建/变更节点的脚本必须 return 受影响对象」的规则(gotchas.md 的 "MUST return ALL created/mutated node IDs" 一节),后续调用可据此校验或清理。

导入团队库文本样式

如果样式定义在团队库(team library)而非本地,正确做法不是重新创建,而是用 figma.importStyleByKeyAsync(key) 按 key 导入:

// 按 key 导入库文本样式
const headingStyle = await figma.importStyleByKeyAsync("TEXT_STYLE_KEY");
// 应用到文本节点
await textNode.setTextStyleIdAsync(headingStyle.id);

key 是样式在文档中的唯一标识(类型定义 L11015 附近注明:节点可通过 setTextStyleIdAsync 等将样式 id 赋给自己以使属性与样式同步)。获取可用 key 的途径是 search_design_system 工具并带 includeStyles: true——它会返回可导入的样式 key。文档的取舍原则是:优先导入库样式,而不是新建本地样式,这保证了与团队设计系统的单一事实来源一致。

将文本样式批量应用到节点

创建样式本身对任何节点都没有影响,必须显式把样式 id 赋给文本节点。文档提供了一个按名称模式匹配、批量应用到当前页面全部 TEXT 节点的实用函数:

/**
 * 将文本样式应用到当前页面上名称匹配给定模式的所有 TEXT 节点。
 *
 * @param {string} styleId - 某个 TextStyle 的 ID
 * @param {string} nodeNamePattern - 对节点名称做子串匹配
 * @returns {Promise<number>} - 应用了样式的节点数
 */
async function applyTextStyleToMatchingNodes(styleId, nodeNamePattern) {
  const textNodes = figma.currentPage.findAllWithCriteria({ types: ['TEXT'] });
  let applied = 0;
  for (const node of textNodes) {
    if (node.name.includes(nodeNamePattern)) {
      await node.setTextStyleIdAsync(styleId);
      applied++;
    }
  }
  return applied;
}

// 可运行的完整脚本:
const applied = await applyTextStyleToMatchingNodes('STYLE_ID', 'Heading');
return { applied };

两个实现层面的补充:

  1. findAllWithCriteria({ types: ['TEXT'] }) 是类型化的高效查找 API——它的返回值会被收窄为 TextNode[],且在大型文档中可比 findAll 快数百倍(plugin-api-standalone.d.ts 的注释);
  2. 应用样式不需要先加载字体——只有直接编辑文本内容或字体属性才需要。类型定义中 textStyleId 的属性注释也指出:在 dynamic-page 文档访问模式下该属性只读,必须用异步的 setTextStyleIdAsync(styleId) 更新(plugin-api-standalone.d.ts)。

文本样式与变量的绑定

wwds-text-styles.md 补充了 token 化场景的关键 API:以下字段可通过 style.setBoundVariable(field, variable) 绑定变量——fontFamilyfontSizefontStylefontWeightletterSpacinglineHeightparagraphSpacingparagraphIndent;解绑则传 null。文档的建议是只要变量存在,就用绑定代替裸值

const ts = figma.createTextStyle();
ts.fontSize = 24; // 直接赋值,未绑定变量

const ts2 = figma.createTextStyle();
ts2.setBoundVariable("fontSize", fontSizeVariable); // 变量存在时优先用这个

类型定义中 TextStyle.boundVariables 字段(plugin-api-standalone.d.ts)和 setBoundVariable(field, variable | null) 方法(L11121)印证了这套绑定模型。

高频错误速查

综合文档与技能中的 gotchas,文本样式相关的检查清单是:

症状 / 错误 原因 修复
设置 fontName 报错 字体未加载 await figma.loadFontAsync({ family, style })
"SemiBold" vs "Semi Bold" 类失败 样式名因提供商/文件而异 listAvailableFontsAsync() 发现精确字符串,勿猜测
设置 lineHeight / letterSpacing 抛错 传了裸数字 一律用 { value, unit }{ unit: 'AUTO' }
找不到已知样式 name 匹配 名称不唯一,改按 id / key 匹配
样式建了但节点没变化 样式不会自动生效 必须把样式 id 赋给节点(setTextStyleIdAsync
getLocalTextStyles() 抛异常 同步版已废弃 改用 getLocalTextStylesAsync()

其中「创建样式对节点零影响」「斜杠分组只是 UI 展示」("Heading/XL""HeadingXL" 是两个不同名字)这两条认知最容易与直觉冲突,调试时值得先排除。

参考资料与延伸阅读

登录后查看全文
热门项目推荐
相关项目推荐