Block/API Reference

Documentation

Three hooks for code-driven Figma components.

Attach inputs to a component, hydrate them from canvas with initial(), push changes through write(), and keep derived state in bind().

inputswrite()initial()bind()

Block API

Block attaches code-driven inputs to a Figma component. The script runs inside the plugin and syncs values to the canvas through three hooks: write, initial, and bind.

Mental Model

block.define({
  inputs: {
    myInput: {
      type: "text",
      label: "My Input",
      default: "Hello",

      // Runs when this input's value changes → push value to canvas
      write(node, value) { ... },

      // Runs once on open → pull current canvas state into this input
      initial(node) { return ... },
    },
  },

  // Runs when any input changes → cross-input logic and derived state
  bind(inputs, node) { ... },
});

Every API method exists in service of one of these three slots. NodeProxy read methods belong in initial (and conditionally in write/bind). NodeProxy write methods belong in write and bind. Traversal methods work everywhere.

block.define

block.define({
  key: "flight",       // required when expose: true
  expose: true,        // makes this block reusable by other blocks
  window: {
    width: 420,
    height: 560,
  },
  inputs: { ... },
  bind(inputs, node) { ... },
});

Rules:

  • inputs is required
  • key is required when expose: true
  • window is optional. width and height can be provided independently. Values above 720px width or 900px height are clamped.
  • bind is optional

Errors:

  • block.define() was never called — script ran but never called block.define
  • block.define({ expose: true }) requires a non-empty key

Inputs

Each input is a plain object with at minimum type, label, and default.

inputs: {
  myInput: {
    type: "text",     // required
    label: "Title",   // required, non-empty string
    default: "Hello", // required, must match the type
    write(node, value) { ... },   // optional
    initial(node) { return ... }, // optional
  },
}

Errors shared by all input types:

  • inputs.X.type is required
  • inputs.X.type "foo" is invalid
  • inputs.X.label must be a non-empty string
  • inputs.X.default is required
  • inputs.X.initial must be a function
  • inputs.X.write must be a function
  • inputs.X.read is not supported. Use inputs.X.initial instead

text

title: {
  type: "text",
  label: "Title",
  default: "Hello",
}

Default must be a string.

number

count: {
  type: "number",
  label: "Count",
  default: 3,
  min: 0,   // optional
  max: 12,  // optional
}

min and max are optional. Default must be a valid number within range if min/max are set.

Error: inputs.X.default must be a valid number

slider

progress: {
  type: "slider",
  label: "Progress",
  default: 60,
  min: 0,    // optional
  max: 100,  // optional
  step: 1,   // optional, must be a positive number
}

min, max, and step are optional. Default must be a valid value within range.

Errors:

  • inputs.X.step must be a positive number
  • inputs.X.default must be a valid slider value

boolean

enabled: {
  type: "boolean",
  label: "Enabled",
  default: true,
}

Default must be true or false.

Error: inputs.X.default must be a boolean

select

variant: {
  type: "select",
  label: "Variant",
  options: ["Primary", "Secondary", "Danger"],
  default: "Primary",
}

options may also be a function that receives current input values — useful when options depend on another input:

featuredFlight: {
  type: "select",
  label: "Featured flight",
  options(inputs) {
    return (inputs.flights || []).map(function(flight, index) {
      return {
        label: String(flight.flightNo || ("Flight " + (index + 1))),
        value: String(index),
      };
    });
  },
  default: "0",
}

Errors:

  • inputs.X.options must contain at least one value
  • inputs.X.default must be one of its options

block

Use type: "block" to embed the inputs of another exposed block.

featuredFlight: {
  type: "block",
  label: "Featured flight",
  blockKey: "flight",
  default: {},
  includeInputs: ["airline", "flightNo"], // optional — limits which child inputs are shown
  write(node, value) {
    node.findChild("FEATURED_SLOT").setBlockValue(value);
  },
  initial(node) {
    return node.findChild("FEATURED_SLOT").getBlockValue();
  },
}

default must be an object. write and initial are optional.

Errors:

  • inputs.X.blockKey "flight" could not be resolved
  • Cyclic block reference detected for blockKey "flight"
  • inputs.X.default must be an object
  • inputs.X.includeInputs must be an array of strings
  • inputs.X.includeInputs contains unknown child input "X"

collection

Use type: "collection" to manage a list of items from another exposed block.

flights: {
  type: "collection",
  label: "Flights",
  blockKey: "flight",
  default: [],
  view: "table",                              // optional: "list" | "table"
  includeInputs: ["airline", "flightNo"],     // optional
  write(node, value) {
    node.findChild("FLIGHTS_SLOT").setBlockValues(Array.isArray(value) ? value : []);
  },
  initial(node) {
    return node.findChild("FLIGHTS_SLOT").getBlockValues();
  },
}

default must be an array of objects. write and initial are optional.

Errors:

  • inputs.X.view must be "list" or "table"
  • inputs.X.default must be an array of objects

write(node, value)

Called every time this input's value changes. Use it for one-to-one canvas bindings — one input maps to exactly one canvas target.

title: {
  type: "text",
  label: "Title",
  default: "Hello",
  write(node, value) {
    node.findChild("_title").setText(String(value));
  },
}

Rules:

  • node is the root instance
  • value is the new input value — always coerce it to the expected type (String(value), Number(value), etc.)
  • Each canvas target can only be owned by one write(). Two inputs cannot write the same layer.
  • bind() cannot write to a target already owned by any write().

Available NodeProxy methods: all traversal, all read, all write.

Error: Duplicate write target: text node "_title". Already owned by input "otherInput".

initial(node)

Called once when the editor opens. Use it to read the current canvas state and seed the input with the real value.

title: {
  type: "text",
  label: "Title",
  default: "Hello",
  initial(node) {
    return node.findChild("_title").getText();
  },
}

Rules:

  • Must return a value. If initial returns undefined, or a value that fails type validation, the plugin falls back to the last saved value, then to default.
  • Runs once on open. After that, the UI state is the source of truth for the session.
  • If the returned value differs from the last saved value, the plugin treats the input as drifted (canvas was edited directly while the plugin was closed) and automatically runs bind() to re-sync any derived state.
  • Manual canvas edits while the editor is already open are not pulled back into the UI and may be overwritten by later input changes.

Available NodeProxy methods: all traversal, all read. Write methods are available but should not be used in initial.

bind(inputs, node)

Called every time any input changes. Use it for cross-input logic, derived state, and anything that depends on more than one input.

bind(inputs, node) {
  node.findChild("Badge").setVisible(Boolean(inputs.title) && !inputs.disabled);
}

Rules:

  • inputs contains the current value of every input
  • Cannot write to any canvas target already owned by an input's write()
  • A canvas target that is only written in bind() should have no initial()bind() owns it entirely as a pure output

Available NodeProxy methods: all traversal, all read, all write.

Error: Duplicate write target: visibility on "Badge". Already owned by input "title". Remove one writer or move this logic fully into bind().

Derived values

A layer whose value is always computed from inputs should have no initial() and live entirely in bind():

block.define({
  inputs: {
    value: {
      type: "number",
      label: "Value",
      default: 0,
      initial(node) {
        return Number(node.findChild("_value").getText());
      },
      write(node, value) {
        node.findChild("_value").setText(String(value));
      },
    },
  },
  bind(inputs, node) {
    // _calculatedValue is derived — bind() owns it, no initial()
    node.findChild("_calculatedValue").setText(String(inputs.value + 2));
  },
});

When _value is edited directly on canvas while the plugin is closed, drift is detected on next open and bind() runs automatically to bring _calculatedValue back in sync.

NodeProxy

Traversal — available in write, initial, and bind

node.findChild("Layer Name")      // first direct or nested child matching the name
node.findOne("Layer Name")        // first descendant at any depth matching the name
node.findOne(fn)                  // first descendant matching a predicate function
node.findById("123:456")          // descendant by Figma node ID
node.findAllByName("Layer Name")  // all descendants matching the name
node.findAll("TEXT")              // all descendants of a given Figma node type
node.children                    // direct children as NodeProxy[]

findOne with a predicate:

var icon = node.findOne(function(n) { return n.name.startsWith("_icon"); });

Properties — available in write, initial, and bind

node.id     // Figma node ID string
node.name   // layer name
node.type   // Figma node type: "INSTANCE", "TEXT", "FRAME", etc.

Useful for conditional logic:

bind(inputs, node) {
  node.children.forEach(function(child) {
    if (child.type === "INSTANCE") {
      child.setProperty("Variant", String(inputs.variant));
    }
  });
}

Read — available in write, initial, and bind

node.getText()              // returns string — only meaningful on TEXT nodes
node.getVisible()           // returns boolean
node.getOpacity()           // returns number 0–1
node.getProperty("Variant") // returns string | boolean | number | undefined — only on INSTANCE nodes
node.getBlockValue()        // returns object — reads stored block value
node.getBlockValues()       // returns array — reads stored collection values

Read methods are most commonly used in initial() to seed inputs from canvas state, but can also be used in write() or bind() for conditional logic.

Write — available in write and bind

node.setProperty("Variant", "Primary")       // only on INSTANCE nodes
node.setProperties({ "Variant": "Primary", "Size": "Medium" }) // only on INSTANCE nodes
node.setText("Hello")                        // only on TEXT nodes
node.setVisible(true)
node.setOpacity(0.4)                         // clamped to 0–1
node.setBlockValue({ airline: "Emirates" })  // targets a block-powered child instance
node.setBlockValues([                        // targets a collection host
  { airline: "Emirates" },
  { airline: "Qatar" },
])

setProperty, setProperties — silently no-op on non-INSTANCE nodes.

setText — silently no-op on non-TEXT nodes.

setOpacity — value is clamped to [0, 1].

Reusable Blocks

Expose a block once with key and expose: true:

block.define({
  key: "flight",
  expose: true,
  inputs: {
    airline: { type: "text", label: "Airline", default: "" },
    flightNo: { type: "text", label: "Flight #", default: "" },
  },
});

Reference it from another block using type: "block" or type: "collection":

block.define({
  inputs: {
    featuredFlight: {
      type: "block",
      label: "Featured flight",
      blockKey: "flight",
      default: {},
    },
    flights: {
      type: "collection",
      label: "Flights",
      blockKey: "flight",
      view: "table",
      default: [],
    },
  },
});

Ownership Rules

Block validates write ownership at definition load time.

  • Each canvas target may only be written by one write() across all inputs
  • bind() may not write to any target already owned by an input's write()
  • Violations are caught before the block runs

Validtitle.write() owns _title, bind() owns Badge:

block.define({
  inputs: {
    title: {
      type: "text",
      label: "Title",
      default: "Hello",
      write(node, value) { node.findChild("_title").setText(String(value)); },
      initial(node) { return node.findChild("_title").getText(); },
    },
  },
  bind(inputs, node) {
    node.findChild("Badge").setVisible(Boolean(inputs.title));
  },
});

Invalid — both title.write() and bind() write _title:

block.define({
  inputs: {
    title: {
      type: "text",
      label: "Title",
      default: "Hello",
      write(node, value) { node.findChild("_title").setText(String(value)); },
    },
  },
  bind(inputs, node) {
    node.findChild("_title").setText(String(inputs.title)); // error: already owned by title.write
  },
});