Skip to content

How detection works

Figma nodes don't carry a semantic "this is a button" flag. The tool uses a three-layer strategy.

Layer 1: Name patterns

Component names are matched case-insensitively against a curated list:

button, btn, cta
link, anchor
input, field, text_field, search_bar
toggle, switch
checkbox, check_box
radio
dropdown, select, picker
tab, tab_bar
card
modal, dialog, sheet
nav_bar, bottom_nav
carousel, slider
chip, tag, badge

Layer 2: Prototype interactions

Any node with a Figma prototype interaction (click, hover, drag, etc.) is classified as interactive regardless of its name. This catches elements that look like cards or tiles but are actually clickable.

Legacy non-empty transitionNodeID also counts as a prototype interaction, as documented in the Figma node reference. Decorative-name exclusions still take precedence by default, including icon-only CTAs whose names begin with Icon.

Layer 3: Component types

Only these Figma node types are considered:

  • COMPONENT - the master definition
  • COMPONENT_SET - a variant container
  • INSTANCE - a placed instance

Plain FRAME, GROUP, TEXT, and RECTANGLE nodes are skipped unless they have a prototype interaction attached.

Names such as Button/Continue and Link/Terms retain their inferred type even on raw frames or text. A node with an interaction and no type match uses interactive. Exclusions below still take precedence.

Exclusion patterns

detection.overrides lets a design system include or exclude controls explicitly. Rules run in order; the first match wins. A rule selects either a case-insensitive name glob (match) or an exact Figma node_id. Included nodes use the rule's type (default interactive); naming and action lookup still use the normal configuration. Overrides take precedence over built-in name exclusions, but cannot resurrect an excluded/hidden ancestor or bypass the hidden-layer policy.

detection:
  overrides:
    - node_id: "1:10"
      type: button
    - match: "Acme/Action*"
      type: button
    - match: "Acme/Decorative*"
      action: exclude

action defaults to include; exclude prunes the matching subtree. A child override is visited only if its ancestors are traversed. To target independent controls inside detected containers, also enable traverse_interactive_children. Unknown keys, empty selectors and invalid actions fail during config loading.

Even if a name matches layer 1, these exclusions drop it:

  • icon (icons inside buttons, not standalone CTAs)
  • divider, separator
  • placeholder, hint
  • loader, spinner

Screen name derivation

Screens are frames directly under a page or nested inside SECTION containers. Discovery stops at each screen frame, so its internal layout frames do not become additional screens. Section names do not enter event names; the Figma page still supplies the flow. This traversal does not use screen_name.max_depth, which is reserved: only its legacy value 2 is accepted; other values fail validation.

The "screen" for each event comes from the Figma hierarchy, not the element. Given:

Page: "Home"
  Frame: "Home - Default"           → screen: home
  Frame: "Home - With Offer"        → screen: home  (variant, collapses)
  Frame: "Home - Skeleton"          → screen: home  (variant, collapses)

The - With Offer example requires adding that suffix to the configured list; it is not a default suffix. Every variant frame is visited. Controls that generate the same full event name within a page share one event with all their source node IDs; controls unique to later variants create additional events. The first control supplies the description and legacy primary source. This also applies to repeated labels in one frame and names intentionally combined by a custom naming pattern.

Variant collapsing uses naming.screen_name.strip_suffixes. Numbered prefixes like 01 - Welcome strip to welcome when naming.screen_name.strip_prefixes: true.

JSON includes both the original source field and a sources array containing all figma:node_id:... references. Older single-source files still load. When upgrading, regenerate the taxonomy and review newly discovered variant controls and source additions before accepting the new baseline. Validation and diff report source additions/removals as drift; source ordering alone is ignored. A rename can match through any shared node when both events have a unique correspondence. Ambiguous splits/merges use unchanged names where possible, otherwise additions and removals, rather than guessing renames.

Names that differ before truncation but collide at max_event_length cause an actionable error before output is written. Increase the limit, adjust the naming pattern, or rename the controls.

Pages and pageviews

The owning page supplies each event's flow/category. Page IDs distinguish pages even when their display names match. Controls from different pages cannot share an event name: add {page} to naming.pattern or rename the screens. {page} is the normalized page name; if page names normalize identically, rename the pages as well. The default pattern remains unchanged, and qualification is explicit so filtering to one page does not rename its events.

CLI and MCP inventory every screen frame on included pages, even frames with no detected controls. Pageviews use the screen frame IDs as sources, combining all variants within that page. A frame organized inside a section is included; layout frames inside a screen are not separate screens. Page exclusion and explicit page selection apply equally to controls and pageviews. Control events retain control IDs rather than replacing them with frame IDs.

When upgrading an older baseline, expect added pageview sources and new pageviews for empty screens. Frame IDs allow unambiguous pageview renames to be recognized. Moving a screen or renaming its page also reports category changes when event names stay unchanged. Review these changes before accepting the new baseline.

Python callers can pass screens=extract_screens(figma_file, config) to generate_taxonomy(elements, config, screens=...) to include the full inventory. The existing two-argument call still works, deriving known screens from elements; it cannot discover empty frames. Hand-built elements without frame IDs retain source-less pageviews rather than inventing IDs. Missing page IDs fall back to the page name for legacy hand-built inputs.

Text content vs component name

When naming.element_name.use_text_content: true (the default), the tool prefers the element's visible text over the component name. This matters because:

Component: "Button/Primary/Large"
Text:      "Apply Now"

With text content, you get ..._apply_now_clicked (matches the user's mental model). Without, you'd get ..._button_primary_large_clicked (bound to the design system, breaks when the component is restyled).

When there is no immediate child label, extraction searches visible nested layout children. It does not borrow text from independently interactive descendants. If no text exists, component-name fallback is used unless explicitly disabled; disabled fallback produces an error containing the node ID.

Hidden layers and nested controls

detection.include_hidden: true preserves the existing default. Set it to false to exclude hidden nodes and their descendants, including hidden screen frames and sections. The screen inventory and control extraction follow the same policy.

detection.traverse_interactive_children: false preserves parent suppression: a detected card/form/button owns the interaction and its descendants are not separate controls. Set it to true to collect nested controls as well, then review whether these correspond to independent gestures. Every added control retains its own source ID. Decorative exclusions still remove entire subtrees.

Instance componentProperties entries of type VARIANT are retained on extracted elements as Name=Value strings. They do not automatically become event properties or change AI prompts. Synthetic coverage is in tests/fixtures/detection_policy.json.

When detection goes wrong

Use figma-taxonomy extract --fixture design.json --explain decisions.json to write an optional JSON report alongside the normal taxonomy, or pass explain=true to the MCP extraction tool. With --page, the report covers the selected page. It does not change events or enable AI.

Report schema_version: 1 contains one nodes record per supplied node, including nodes skipped by traversal. Each record has its Figma ID/source, original name and path, page/screen context, variants, element_type, control_detected, reason, zero-based matched_rule (or null), and actual generated event_names. included means the node is a source of an event, including a screen pageview. Control exclusion does not remove a screen from the separate pageview inventory.

The top-level decisions list records event-name collisions and unnamed screens or controls. Each collision entry has reason: "collision", node_id for the disambiguated control, conflicting_node_id for the original control, source_labels for both original labels (or component names in component mode or fallback), original_event_name, and the resolved event_name. Unnamed-screen entries have reason: "unnamed_screen", node_id, raw_frame_name, and the node-ID-derived screen_name. Unnamed-control entries have reason: "unnamed_control", node_id, raw_label, and the node-ID-derived element_name. The list is empty when neither disambiguation nor a screen/control name fallback was needed.

Reasons identify name or prototype detection, explicit overrides, hidden layers, excluded pages/names, suppressed descendants (*_ancestor), noninteractive nodes, pageview-only screens, or nodes outside screens. A matched rule refers to the ordered detection.overrides list. Reports include design text and should be shared with the same care as the source fixture. Choose a report path distinct from fixture/config inputs and generated taxonomy files.

If an event you expected isn't showing up:

  1. Check the component name in Figma against the pattern list
  2. Check for an exclusion (icon is a common false-negative trigger)
  3. Check that the element is a COMPONENT/INSTANCE, not a raw FRAME
  4. Add a prototype interaction to include a raw node that is not excluded
  5. Or override by renaming the component to include a keyword like button

The report connects detection decisions to the final events, including merged sources, so inspect it before changing a naming rule or accepting new controls.