Install
openclaw skills install @monikazapisek/text-typesettingUse when setting or reviewing line-height, letter-spacing (tracking), and vertical-trim/leading-trim for a text style — computing the geometric relationship between font size and these values for body text, headings, UI labels, or captions.
openclaw skills install @monikazapisek/text-typesettingCompute a coherent line-height and letter-spacing for a given font-size and text role, and correctly account for vertical-trim (CSS leading-trim / Figma leadingTrim) when it changes what "line-height" visually means.
font-size (px or rem).font-family-category: sans-serif / serif / display / monospace (serifs generally need more line-height breathing room than sans at the same size).text-role: body / heading / UI label / caption.text-case: original (sentence/title case) or uppercase/small-caps — this is the primary driver of letter-spacing direction, see Workflow step 2.x-height: the ratio of x-height to font size (xr; Times is 0.447, Verdana 0.545), measured or read from the font — it sets where line-height falls inside its range (Workflow step 1a). For tracking, high / low is enough (step 2b). Optional: without it the skill falls back to the family rule and says so.font-weight: numeric (100–900) if known — scales the tracking magnitude from step 2/2b, see Workflow step 2c.variable-font-axes: if the font is variable, the actual wght value and whether an opsz (optical size) axis exists and is bound to size — see Workflow step 2d.leadingTrim / vertical-trim state (see Figma Node Integration) — this changes the recommended line-height, not just its visual effect.line-height value (unitless ratio preferred for body text, so it scales with font-size).letter-spacing value (in em, since tracking should scale with font-size, not be a fixed px).This skill is an audit, not an interview. Work in this order: read what the file or the input already says, report, then ask.
Questions this skill may need:
1.2–1.45 (Butterick: 120–145% of the point size).1.1–1.3 — a working range of this skillset; no direct source was found for these exact endpoints.1.2–1.4 — a working range of this skillset; no direct source was found for these exact endpoints.1a. Position inside the range comes from the typeface's x-height, computed, not picked. The x-height ratio is the height of a lowercase x divided by the font size (xr). A face with a large x-height fills more of the line, so its lines look closer together and need more room; a face with a small x-height carries its own air.
t = clamp((xr − 0.447) / (0.545 − 0.447), 0, 1)
LH = LH_min + t × (LH_max − LH_min)
xr = 0.447 or lower lands on the bottom of the range, 0.545 or higher on the top. The two anchors are the reference faces the W3C uses to explain x-height: Times, 0.447, as a low aspect value and Verdana, 0.545, as a high one (CSS Fonts Module, font-size-adjust). The direction is Butterick's: fonts that run small need less line spacing, and vice versa. Round LH to two decimals. Show xr, the range and the result.
How to get xr:
sxHeight / unitsPerEm from the font's OS/2 table; in a browser, 1ex measured against 1em.x in the font at 100px, flatten it to a vector (figma.flatten([node])), read its height, divide by 100, and remove the vector. This creates and deletes a layer, so ask before doing it. Checked values from this method: Inter 0.545, Roboto 0.528, IBM Plex Sans 0.516, Source Serif 4 0.452, EB Garamond 0.409.1b. Measure compensation ("Typography Triangle") — only when the column is wider than ideal: if line-length-optimizer reports (or the input states) a measure wider than the ~65-character ideal — the eye has more trouble tracking back to the start of the next line the wider the column gets — compensate by increasing line-height beyond the step-1 base:
LH_new = LH_base + ((characters_per_line − 65) / 100) × 0.1
E.g. at 85 characters per line, line-height grows from 1.35 to 1.37. Only apply this when measure is confirmed wider than ~65–75 characters (i.e. already outside line-length-optimizer's comfortable range) — don't apply a correction for a column that's merely at the high end of normal. This is a compensation for a business constraint that's forcing an over-wide container, not a substitute for fixing the width via line-length-optimizer when that's actually available.
Letter-spacing (tracking) — driven first by case, then by size. text-case is the primary branch — check it before applying any size-based rule:
Branch A — uppercase or small-caps (Figma textCase: "UPPER" / "SMALL_CAPS" / "SMALL_CAPS_FORCED", or CSS text-transform: uppercase): default font spacing is tuned for mixed-case letterforms, which lose their shape variety when flattened to all-caps — the eye can't recognize word-shapes anymore, only a dense block of equal-height letters. Always add positive tracking, scaled inversely with size (small caps text needs proportionally more room than large). One formula, one result per size (FS = font-size in px):
TS = max(3%, min(12%, 160 / FS))
10px → +12%, 14px → +11.4%, 16px → +10%, 20px → +8%, 32px → +5%, 40px → +4%, 54px and above → +3%.glyph-fidelity Rule 1. There are no separate size bands: both skills return the same number for the same size.letter-spacing: 0.08em in CSS, or letterSpacing = { unit: "PERCENT", value: 8 } in Figma.Branch B — sentence/title case (textCase: ORIGINAL): lowercase letters carry their own internal/external counters that already aid legibility — no tracking adjustment needed at body/reading sizes. Only tighten at larger sizes, where letter-spacing optically opens up and headings start to look "broken apart":
font-size < 24px → letter-spacing: 0 (normal) — don't touch it.24px ≤ font-size < 36px, sans-serif → letter-spacing: -1.5% (-0.015em).font-size ≥ 36px, sans-serif → letter-spacing: -2% (-0.02em).2b. x-height refinement (optional, only if the specific typeface's x-height is known):
2c. Font-weight modifier (apply after Branch A/B + x-height, as a final scaling pass on whatever tracking value step 2/2b produced):
Heavier weights (Bold/700, Black/900) carry thicker strokes, which already reduce the white space inside and between letters — the same numeric tracking value reads as looser on a Black weight than on a Regular weight, because there's less counter-space for the extra room to sit in. Reduce the computed tracking magnitude (keep the sign from Branch A/B, scale the number down):
×0.7.×1.15 — thin strokes leave more open counters already, and default spacing can look slightly cramped without a small boost.This applies in both branches: an uppercase Black-weight label still gets positive tracking (Branch A), just proportionally less than the same label in Regular weight.
If weight is unknown, skip this modifier — Branch A/B values already assume Regular/Medium as the default case.
Light weights at small sizes (platform constraint): Apple's Human Interface Guidelines advise against Ultralight, Thin and Light weights, which are hard to see especially when text is small. Flag any weight below 400 on body, label or caption text. This is a legibility flag from the platform layer; it doesn't change the tracking computed above.
2d. Variable fonts — read the actual axis value, don't assume a named instance:
wght: 550), not a fixed Regular/Bold step. Apply the font-weight modifier (step 2c) by interpolating between the anchor points (400→×1, 700→×0.7, 300→×1.15) rather than snapping to the nearest named instance — a wght: 550 gets a modifier roughly halfway between ×1 and ×0.7.opsz), check it before manually adjusting tracking at all: opsz is specifically designed to auto-adjust spacing, stroke contrast, and x-height proportions for the rendering size, which is much of what steps 2/2b/2c are compensating for manually. When opsz is bound to font-size (CSS font-optical-sizing: auto — the default in modern browsers — or the Figma equivalent), treat the manual tracking correction as a smaller supplementary nudge, not the full computed value, and say so in the output rather than silently stacking both corrections.Vertical-trim / leading-trim guard (critical — check before finalizing line-height):
line-height values above assume this default.leading-trim is on (CSS leading-trim: both / text-box-trim, or Figma leadingTrim: "CAP_HEIGHT"): the line box is cropped to cap-height/baseline, removing the font's built-in air. In this state, the same numeric line-height reads as visually tighter — recompute upward (roughly +10–15%) if trim is on and the target visual density should stay the same as the untrimmed baseline. State explicitly whether the recommended value assumes trim on or off — never hand over a bare number without saying which.textNode.leadingTrim, with the values "CAP_HEIGHT" and "NONE". A property called textLeadingTrim does not exist on a text node, and reading it throws. CSS calls the same thing text-box-trim (leading-trim in earlier drafts).OpenType number styles — pick by content role, not by default. Digits aren't one glyph set; the font exposes up to four figure styles along two independent axes (form × spacing), and picking wrong is a common, avoidable error:
| Content role | Figure form | Spacing | CSS | Figma OpenType feature |
|---|---|---|---|---|
| Running prose (articles, body copy) | Oldstyle (varying heights, ascenders/descenders — blends into lowercase text instead of forming bright "spikes") | Proportional | font-variant-numeric: oldstyle-nums proportional-nums; | ONUM |
| Headings / display / UI counters (prices, single stats, non-tabular labels) | Lining (uniform cap-height) | Proportional (natural rhythm — a 1 shouldn't occupy the same width as an 8) | font-variant-numeric: lining-nums proportional-nums; | LNUM |
| Tables, financial statements, any column of stacked numbers | Lining | Tabular (fixed-width per digit, so units/tens/hundreds columns align vertically) | font-variant-numeric: lining-nums tabular-nums; | LNUM + TNUM |
textNode.openTypeFeatures is read-only and lists only the features changed from the font's default (e.g. { TNUM: true }). There is no fontFeatures property and no setter. Read the current state, report which figure style the role needs, and ask the user to switch it in the Type settings panel (Details tab), or apply a text style that already carries it. Never report a figure style as applied.11 or 74 produce visible gaps without it. Tabular figures have kerning intentionally suppressed by the font itself to preserve fixed-width column alignment — do not try to "fix" the spacing of a tabular-figure node by changing kerning; that's the font working as designed, not a bug.11 — the two vertical strokes nearly touch) or too loose (74 — the diagonal and the crossbar leave a visible gap) at large display sizes, where the flaw becomes visible. If a display heading contains one of these known-risky pairs, apply a local setRangeLetterSpacing on just that pair, ±2%, rather than adjusting the whole node's tracking. Don't apply this pre-emptively — only when the specific pair is present and the size is large enough to make it visible (roughly ≥32px).99.90 zł, $4.99): the cents/grosze portion is a common candidate for the OpenType Numerator/superscript feature (sups or a fraction feature frac) to visually subordinate it to the whole-number part, matching classic price-tag typesetting. Only apply when the content is genuinely a price/currency amount, not a generic decimal (3.14, a percentage) — those stay as plain lining figures. Also check the decimal separator (./,) isn't visually colliding with a preceding 0 — a rare kerning gap in some fonts — and apply the same local range-tracking fix as above if it is.font-variant: small-caps / Figma textCase: "SMALL_CAPS") only when explicitly requested — it's a stylistic choice, not a default correction.Kerning/tracking integrity guard (hard rule, not a recommendation):
AV, Ta, We) and tracking/letter-spacing (a uniform global offset applied to every letter) are complementary, not substitutes. Adjusting tracking in steps 2/2b/2c/2d must never disable or bypass the font's native kerning table.font-kerning: normal (the default) — never set font-kerning: none as a side effect of a tracking change.fontKerning property. A tracking change writes letterSpacing only, so it cannot switch kerning off. Read textNode.openTypeFeatures; if it explicitly reports KERN: false, flag it as likely unintentional. If the key is absent, report the state as "not explicitly overridden" rather than claiming that kerning was verified on: a live 1,913-node reference file exposed no KERN key. A plugin script cannot switch kerning back on (openTypeFeatures is read-only), so tell the user where to re-enable it instead of reporting it as fixed.Dash rendering in Figma (which character/spacing to use is microtypography Rule 1c — this step is only the rendering layer on top of that decision):
-, –, or —, kerning must be on (per step 5) so the font's OpenType pair tables can prevent collisions with adjacent outward-leaning letterforms (V—, —A, 1–9) — this is the same guard as step 5, just called out explicitly because dash collisions are the most visible failure case.+3% setRangeLetterSpacing across the em dash and the single character immediately on each side of it, not the dash alone — tracking just the dash glyph itself doesn't change its distance to its neighbors (letter-spacing is applied between characters, not as padding on one glyph), so the range must include at least one adjacent character per side to actually open the gap.microtypography Rule 1c.case-sensitive forms): a default hyphen is vertically centered for lowercase text; in an all-caps run it can visually hang too low relative to the capital letters. Check whether the font's OpenType case feature (case-sensitive forms — raises hyphens, dashes, parentheses, and similar punctuation to align with cap-height) is available and enable it (CSS font-feature-settings: "case"; in Figma, the CASE feature in the Type settings panel — a plugin script can read it through openTypeFeatures but cannot set it) for all-caps/small-caps nodes rather than leaving the punctuation optically low. Skip if the font doesn't expose the feature — don't fake it with manual vertical offsets.Hanging punctuation in Figma (the same switch microtypography Rule 4 uses, reached from the type-style side):
", “, „, «) or parenthesis, where leaving it inside the block makes the following letter's left edge look indented relative to body copy below it.textNode.hangingPunctuation = true. A live Inter probe confirmed left overhang for ", “, „, «, and (, but not for an em dash (—). Use the switch for the confirmed marks. Don't simulate the effect with a negative paragraphIndent.When running with Figma access:
textNode.fontSize, lineHeight, letterSpacing, fontName, leadingTrim, textCase, fontWeight (read-only), openTypeFeatures (read-only), hangingPunctuation. Any of them returns figma.mixed when the node holds more than one style — read per range with textNode.getStyledTextSegments([...]) instead of taking the first value.getStyledTextSegments(["fontName"]) returns the axis values in fontName.variationSettings (e.g. { wght: 500 }). Use that number for Workflow step 2c/2d instead of trusting the named style string — an instance named "Regular" can carry a custom wght like 435. Check the same object for an opsz axis before stacking a manual tracking correction on top of it. If variationSettings is absent, the font is static: use fontWeight.textCase check is the mandatory first branch for letter-spacing (Workflow step 2): "UPPER", "SMALL_CAPS", "SMALL_CAPS_FORCED" → Branch A; "ORIGINAL", "LOWER", "TITLE" → Branch B. Read this before computing any tracking value — the two branches produce opposite-sign results, so skipping this check risks recommending negative tracking on all-caps text (actively harmful, not just suboptimal).leadingTrim before recommending a line-height value — see Workflow step 3. If it's "NONE" and the layer sits inside an Auto Layout frame with a tight itemSpacing, flag the interaction with vertical-spacing (optical gap will read smaller than the set gap value).letterSpacing is written, read openTypeFeatures first. If it reports KERN: false, flag it as likely unintentional and point the user to the Type settings panel — it can't be changed from a script.textStyleId, or boundVariables lists lineHeight / letterSpacing, the value belongs to that text style or variable. Report the corrected value as a change to the style or variable; don't override it on the single node unless the user asks for a local override.lineHeight and letterSpacing as { unit, value } objects (e.g. { unit: "PERCENT", value: 150 }), and textCase or hangingPunctuation where the step calls for it. OpenType features (figure styles, CASE, kerning) can't be written from a script — list them as steps for the user. Report the values first; only write to the node when the user asks for a direct application, not as a silent default.hangingPunctuation = true only on an eligible display heading — see Workflow step 7 — and only on explicit request, same as every other direct Figma write in this skill.text-case checked first; Branch A (uppercase/small-caps) always positive, Branch B (sentence/title case) only negative above 24px — never the reverse sign.wght read as a continuous value and interpolated, not snapped to the nearest named instance; opsz axis checked before stacking a full manual correction on top of it.KERN: false found on a node is flagged, not silently kept.CASE, kerning) reported as a step for the user — never reported as applied by script.case feature — never overrides the character/spacing choice already made by microtypography Rule 1c.hangingPunctuation switch, on eligible display headings only, and verified visually.case OpenType feature checked (not assumed) before claiming a font can raise punctuation to cap-height; skipped cleanly if unsupported.vertical-spacing noted when trim state affects Auto Layout gap perception.font-size-adjust — aspect value (x-height divided by font size); Verdana 0.545 as a high value, Times 0.447 as a low one.case feature (case-sensitive forms) — raises punctuation (hyphens, dashes, parentheses) to align with cap-height in all-caps/small-caps text.onum (oldstyle), lnum (lining), pnum (proportional), tnum (tabular).microtypography Rule 1c — decides which dash character and spacing to use in prose; this skill only handles Figma-rendering-layer kerning/tracking on top of that choice.wght, opsz registered axes) — variable fonts expose weight as a continuous value and, when present, an optical-size axis designed to auto-adjust spacing/contrast per rendering size.kern feature / GPOS pair-positioning tables — per-pair optical correction (e.g. AV, Ta, We) baked into the font by its designer, complementary to (not replaced by) a uniform tracking value.leading-trim / text-box-trim — crops the line box to cap-height/baseline, removing font-internal leading.TextNode.leadingTrim, TextNode.letterSpacing, TextNode.openTypeFeatures (read-only), TextNode.textCase, TextNode.hangingPunctuation, TextNode.getStyledTextSegments.line-length-optimizer (column width), microtypography (character-level fixes) — apply after typesetting values are set, since column width and rag risk depend on the chosen font-size/line-height.