Building a Figma Plugin That Generates Responsive Type Scales for Beginners

JP
FigmaPluginGuide

After reading this, you will be able to build a Figma plugin that takes a base font size, a scale ratio, and a viewport range, then generates a complete set of responsive text styles as Figma styles your whole team can apply. You will know how the modular scale math works, which Figma plugin APIs create and update text styles, how to compute fluid sizes that interpolate between mobile and desktop viewports, and where beginners typically get the design wrong even when the code runs without errors.

The goal is not a polished product. It is a working plugin you can run inside Figma’s development environment, inspect the output of, and iterate on.


Myth 1: “A Responsive Type Scale Is Just Fixed Sizes With Breakpoints”

The myth: You pick a few breakpoints, assign a fixed size to each one, and call it responsive.

The reality: Breakpoints are step functions, and step functions produce visible jumps. When a window resizes from 1023px to 1024px and the heading jumps from 32px to 40px, every element around it reflows. On a fluid layout that shift reads as a glitch.

A responsive type scale interpolates continuously between two endpoints. The heading does not jump at a breakpoint; it grows smoothly as the viewport widens. In CSS this is usually done with clamp() and a calculated slope, and the same math applies inside a Figma plugin when you generate styles for two or more named viewport modes.

The formula for a fluid value between a minimum and maximum viewport is a linear interpolation:

// Interpolate a font size between two viewport widths.
// minSize: font size at minViewport (e.g. 24px at 375px)
// maxSize: font size at maxViewport (e.g. 40px at 1440px)
function fluidSize(minSize, maxSize, minViewport, maxViewport, currentViewport) {
  const slope = (maxSize - minSize) / (maxViewport - minViewport);
  const interpolated = minSize + slope * (currentViewport - minViewport);
  // Clamp so the value never falls below minSize or rises above maxSize
  return Math.min(Math.max(interpolated, Math.min(minSize, maxSize)),
                  Math.max(minSize, maxSize));
}

For a plugin, you typically do not need a currentViewport at all. You generate two style sets — one for a mobile frame width, one for a desktop frame width — and let designers switch between them using Figma’s variable modes or separate style groups. The interpolation slope matters only if you want to preview a specific width.

When NOT to use continuous interpolation: If your product ships on a small, fixed set of devices (an embedded dashboard at three exact resolutions, for example), a step-function scale is simpler and easier to reason about. Interpolation adds complexity that pays off only when the layout itself is fluid.


Myth 2: “You Need the Figma Variables API to Make This Work”

The myth: Responsive type requires variables, and variables require an enterprise plan, so a beginner cannot build this.

The reality: Text styles are the original and still-supported mechanism, they work on every plan, and the plugin API for them is stable. Figma Variables add mode-switching and are the more modern approach, but a beginner plugin can ship a fully functional responsive scale using text styles alone.

Two API surfaces matter here:

  • figma.createTextStyle() — creates a new text style in the current document.
  • style.fontSize, style.fontName, style.lineHeight, style.letterSpacing — the properties you set on it.

Variables would use figma.variables.createVariable() and a collection with modes, but that path requires understanding variable scoping and mode binding on top of the scale math. For a first plugin, text styles are the shorter road.

A concrete limitation to accept up front: text styles are document-scoped, so a designer has to apply them manually to text layers. They do not propagate automatically the way CSS classes do. If your goal is “apply once and forget,” you will eventually want variables. For a beginner build, manual application is an acceptable trade-off.


Myth 3: “Any Ratio Works — Just Pick a Big Number”

The myth: The scale ratio is a stylistic choice with no real constraints, so a larger ratio means a more dramatic and more useful scale.

The reality: The ratio controls the gap between adjacent steps, and that gap has to stay legible across a small base size. Ratios commonly cluster into four practical bands:

Ratio NameValueTypical Use
Minor Second1.067Dense UI, high information density
Major Third1.250General web and app UI
Perfect Fourth1.333Marketing pages, editorial
Golden Ratio1.618Display-heavy layouts, few sizes

At a 16px base, a 1.618 ratio produces sizes of 16, 26, 42, 68, 110 — four steps cover a huge range, which means you have very few usable intermediate sizes. At a 1.067 ratio, you get 16, 17, 18, 19 — the steps are so close they look accidental.

For most product UI, a ratio in the 1.2 to 1.3 range produces a scale where each step is clearly distinct without skipping sizes you would want. The plugin should expose the ratio as a number input and let the designer see the generated list before committing.

When NOT to generate a scale from a single ratio: If your design needs specific sizes for specific components (a 13px table label, an 11px badge), forcing those into a modular scale makes them non-conforming outliers. A scale is a starting point, not a mandate.


Building the Plugin: Setup to Verified Output

The implementation below is a minimal but complete plugin. It generates a set of text styles from a base size, ratio, and step count, with optional fluid interpolation between two viewport modes.

Step 1: Project structure

A Figma plugin needs a manifest.json and a JavaScript file. The smallest working setup:

{
  "name": "Responsive Type Scale Generator",
  "id": "1234567890123456789",
  "api": "1.0.0",
  "main": "code.js",
  "editorType": ["figma"],
  "ui": "ui.html"
}

The ui file holds the input form; main (code.js) runs in the sandbox and talks to the Figma document.

Step 2: The scale math

// code.js — sandbox side

function generateScale(base, ratio, steps, direction) {
  const sizes = [];
  for (let i = 0; i < steps; i++) {
    // direction: 1 for upward steps, -1 for downward
    const exponent = i * direction;
    sizes.push(base * Math.pow(ratio, exponent));
  }
  return sizes;
}

function fluidize(base, ratio, steps, mobileVw, desktopVw) {
  // Produce two parallel sets: one at mobile, one at desktop
  const mobileBase = base;
  const desktopBase = base * 1.15; // base often grows slightly on desktop
  return {
    mobile: generateScale(mobileBase, ratio, steps, 1),
    desktop: generateScale(desktopBase, ratio, steps, 1),
  };
}

Math.pow(ratio, exponent) is the entire modular scale. The direction parameter lets you generate smaller-than-base sizes for captions and metadata.

Step 3: Create the styles

The Figma side is where most beginners get stuck, because text style creation requires resolving a font first. Font loading is asynchronous and fails silently if the family name is wrong.

// code.js — creating a text style for each generated size
async function createTextStyles(sizes, family, style, prefix) {
  const created = [];
  for (let i = 0; i < sizes.length; i++) {
    const size = Math.round(sizes[i] * 100) / 100;

    // Load the font before touching the style. If this throws,
    // the font family/style combination does not exist.
    await figma.loadFontAsync({ family, style });

    const textStyle = figma.createTextStyle();
    textStyle.name = `${prefix}/Step ${i + 1} — ${size}px`;
    textStyle.fontName = { family, style };
    textStyle.fontSize = size;
    textStyle.lineHeight = { unit: 'PERCENT', value: 150 };
    textStyle.letterSpacing = { unit: 'PERCENT', value: 0 };

    created.push(textStyle);
  }
  return created;
}

Two details matter for correctness:

loadFontAsync is mandatory. Without it, assigning fontName or fontSize throws. Call it once per unique font before the loop rather than per style if you want to reduce round trips.

The style name matters for team use. Prefixing with a group (Mobile/Step 1 — 16px) makes the styles file in the Figma panel readable. Teams that skip the prefix end up with a flat alphabetical mess.

Step 4: Wire up the UI

<!-- ui.html -->
<form id="scale-form">
  <label>Base size <input type="number" id="base" value="16" /></label>
  <label>Ratio <input type="number" id="ratio" value="1.25" step="0.05" /></label>
  <label>Steps <input type="number" id="steps" value="6" /></label>
  <label>Prefix <input type="text" id="prefix" value="Type Scale" /></label>
  <button type="submit">Generate</button>
</form>
<script>
  document.getElementById('scale-form').onsubmit = (e) => {
    e.preventDefault();
    const payload = {
      base: parseFloat(document.getElementById('base').value),
      ratio: parseFloat(document.getElementById('ratio').value),
      steps: parseInt(document.getElementById('steps').value, 10),
      prefix: document.getElementById('prefix').value || 'Type Scale',
    };
    parent.postMessage({ pluginMessage: { type: 'generate', payload } }, '*');
  };
</script>

Step 5: Verify the output

After running the plugin inside Figma via Plugins → Development → Your Plugin, open the Local styles panel. You should see one style per step, named with your prefix, each showing the resolved pixel size in the name. Click a style and apply it to a text layer to confirm the size matches. If a style appears but has the wrong size, the most common cause is that fontSize was assigned before fontName — Figma may accept the number but reset it once the font loads.

A second verification worth running: create a text layer, apply the largest style, then resize the frame around it. Text styles do not change size with the frame — this is the fixed-size reality discussed in Myth 1, and it confirms whether your users expect fluid behavior that styles alone cannot provide.


Myth 4: “The Generated Scale Is the Final Answer”

The myth: Once the plugin produces a set of styles, the typography work is done.

The reality: A generated scale defines available sizes, not assigned roles. A design still needs a mapping from semantic roles (page title, section heading, body, caption) to specific scale steps, and that mapping is a design decision the plugin cannot make. A common failure is generating a beautiful 8-step scale and then watching designers pick sizes arbitrarily because nothing named “Body” exists.

The fix is to let the plugin accept a role-to-step assignment or, at minimum, generate role-named styles on top of the numeric ones. That is a design choice, not a math one, but it separates a scale that gets used from one that gets abandoned.


Trade-offs to Accept Before You Build

Three decisions will shape the plugin more than any code you write:

Text styles vs. variables. Styles work everywhere and are simpler. Variables offer mode switching and automatic application but add a meaningful layer of API complexity. A beginner should ship styles first and consider variables only after the plugin is in daily use.

Fluid interpolation vs. fixed steps per viewport. Interpolation produces smoother results but complicates the style model — you either generate many styles (one per width band) or accept that Figma cannot express a single fluid size. Fixed steps per mode are easier to reason about and to explain to teammates.

One ratio vs. separate upward/downward ratios. Typography systems often use a tighter ratio below the base size (for captions and labels) and a looser one above (for headings). Supporting two ratios doubles the input complexity but produces a scale that fits real content better than a single symmetric ratio does.

None of these are permanent choices. The value of building the plugin yourself is that you can change any of them once you see how the output behaves on a real design.


Where to Start

Open a new Figma plugin project, paste the manifest.json above, and get a single text style to appear before you worry about interpolation or roles. The scale math is a two-line function; the friction is entirely in the Figma API’s asynchronous font loading and in the design decisions you defer. Get one style on screen, apply it to a text layer, and confirm the size matches what the math predicted. Everything after that is iteration.

If you build this, the most useful next step is deciding which semantic roles your product needs — heading levels, body, caption — and mapping those roles to scale steps before generating anything. That mapping, not the ratio, determines whether the generated scale survives contact with a real design.

About the Author

FigmaPluginGuide is an independent informational resource, published by GT. Articles are compiled and explained from publicly available references rather than written from personal professional experience.