Skip to content

Variant property names are flattened to formatted keys with no inverse, corrupting rendered property names and instanceOf references #283

Description

@nathanacurtis

Found while analysing the render round-trip corpus in specs-testing (workspace workspaces/eg, a 69-component the component library). Two symptoms that look unrelated share one root cause: Figma variant property names are flattened into a formatted key with no inverse, and the original name is never preserved.

The workspace uses format.keys: CAMEL (workspaces/eg/specs.config.yaml).

Root cause

Utilities.formatKey(str, 'CAMEL') lowercases the first word and TitleCases the rest, collapsing separators and case. It is lossy and not invertible:

  • "Action 1 appearance""action1Appearance"
  • "Header overlaid""headerOverlaid"

Nothing in the spec records the original Figma name. props.action1Appearance carries type / default / enum / nullable and no $extensions pointing back at Figma. So the render path has no way to reconstruct "Action 1 appearance""Action 1 Appearance" is an equally plausible inverse and still wrong.

Symptom A — render writes formatted keys back to Figma as property names

packages/figma-from-specs/src/Variants/Variants.ts:

function variantFrameName(config) {
  return Object.keys(config).map(k => `${k}=${config[k]}`).join(', ');
}

The spec key goes onto the Figma variant frame verbatim, so the rendered component gets a property literally named action1Appearance instead of Action 1 appearance. Reading it back re-formats idempotently, so the round-trip looks self-consistent while diverging from the source library.

packages/figma-from-specs/src/Props/Props.ts has the same shape — owner.addComponentProperty(name, …) with the raw spec key — so this is not limited to variant properties.

Observed in round-trip diffs:

  • "Validation=Invalid""validation=Invalid"
  • "Platform=iOS""platform=iOS"
  • "Count=2, Action 1 appearance=Primary, Layout direction=Horizontal""action1Appearance=Primary, count=2, layoutDirection=Horizontal"
  • "Appearance=Tonal, Size=M, Disabled=False, State=Hover, Elevated=False""appearance=Tonal, disabled=false, elevated=False, size=M, state=Hover"

This is not cosmetic. The renaming also reorders variants, which cascades: FavoriteButton reported 13 differences that were entirely variants[0] and variants[1] swapping places, and ButtonExperimentalTonal's size ladder (XSSLXL) landed one index late throughout, generating dozens of spurious dropped/added rows.

Note the asymmetry: PropConfigurations.matchFormatted and Subcomponents both thread keyFormat and format-match deliberately when reading. Variant and property creation do not.

Symptom B — instanceOf encodes a variant selection as one unparseable token

Some instanceOf values name a specific variant member of a component set rather than the set itself, with the variant selection flattened into the same camelCase token:

  • workspaces/eg/specs/Sheet/variants.yamlinstanceOf: ToolbarAndroidSCloseFalseBaseOverlay
  • workspaces/eg/specs/Alert/*instanceOf: LinkOnOverlayMFalseRestStart
  • also ToolbarAndroidSCloseTrueBaseTertiary, and ButtonEndVisual (no component or set of that name exists; "End visual" is only a variant property value)

The Figma file has COMPONENT_SETs named Toolbar and Link, whose children are variant members named e.g. Platform=Android, Header=Toolbar, Header overlaid=True. So two distinct facts — which component, and which variant — are collapsed into one string that cannot be parsed back apart.

The correct representation already exists in the schema:

instanceOf: Toolbar
propConfigurations:
  platform: Android
  header: Toolbar
  headerOverlaid: true

Two consequences:

Why one issue

Both symptoms are the same lossy transform applied at different points. Fixing the encoding without fixing the write direction leaves rendered components misnamed; fixing the write direction without fixing the encoding leaves instance references unresolvable. The underlying decision is the same one: how does a spec preserve the original Figma property name?

Options worth weighing before implementing:

  1. Carry the raw name in $extensions.com.figma on the prop, and have render use it
  2. Have render reconcile against the target file's existing property names rather than writing new ones
  3. Emit instanceOf + propConfigurations rather than a flattened variant-member name (needed regardless, for Symptom B)

This likely warrants an ADR rather than a direct patch — it touches the schema's contract about what a spec records.

Reach

  • Symptom A: 4 of 8 cleanly-measured components showed the renaming directly; the ordering cascade was visible in most of the 13 components that completed a round-trip
  • Symptom B: 3 flattened variant-member references, plus ButtonEndVisual, out of 48 instance edges in workspaces/eg/specs/_analysis/dependencies.graph.yaml

Verification

  • A rendered component set's Figma property names match the source library's (Action 1 appearance, not action1Appearance)
  • detectedIn strings round-trip unchanged
  • Variant order round-trips unchanged, eliminating the dropped/added pair storms
  • instanceOf values resolve to a component plus an explicit propConfigurations block, and the rendered instance selects the correct variant rather than the set default

Part of #281.

Metadata

Metadata

Assignees

No one assigned

    Labels

    generatorspecs-from-figma processing engine

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions