ADR
Examples Configuration
Context
ADR-047 and ADR-048 establish Component.slotContentExamples and Component.instanceExamples as the two example registries on a component. Neither ADR specifies how the transformer discovers or gates these fields — that is a Config concern.
The two example types have different shapes of control, and that difference drives the whole design:
-
Default slot content —
Component.slotContentExamplesentries originating from the component’s own slot layers (content authored inside a slot by default). Detection is structural: the transformer reads whatever sits inside slot layers, with no author-supplied naming patterns. There is nothing to detect-configure, only an output gate:include.defaultSlotContent(defaultfalse) decides whether this structurally-detected content is emitted and referenced from the component’s slot bindings. -
Instance examples — named frames in the Figma file that demonstrate a pre-configured whole-component usage. Detection requires configuration: the transformer needs naming patterns to identify which frames are examples and where to search. This mirrors
processing.subcomponents(ADR-031) — and, like subcomponents, the presence of that configuration block is itself the opt-in. A team that writesprocessing.instanceExampleshas, by that act, asked for instance examples; a secondincludeflag would be a redundant gate.
So the configuration is deliberately asymmetric: default slot content is gated by an include flag (it has no detection block), while instance examples are gated by the presence of their processing block (no include flag). Both registries are additionally Pro-gated at emission time — see Notes.
Decision Drivers
- Match each example type to its natural control — structural signals (default slot content) need only an output gate; pattern-detected signals (instance examples) are opted into by the presence of their detection block
processing.instanceExamplesmirrorsprocessing.subcomponents— same match/exclude vocabulary and the same “absence = off, presence = on” semantics;processing.subcomponentshas noinclude.subcomponentsflag, and instance examples should not invent one- Avoid a redundant second gate — requiring both
processing.instanceExamplesandinclude.instanceExamplesproduces the classic “I configured detection but got no output” foot-gun, with no benefit a single switch lacks defaultSlotContentis the only exampleincludeflag — it has noprocessingblock (detection is structural), so the flag is its sole control; it defaults tofalseso unannotated components are unchangedscope: NESTEDis inapplicable — component instances used as examples cannot live inside the component frame itself; the only meaningful search boundaries are the current page (PAGE, default) or the full file (FILE)scope: FILEsupports multi-page files — some teams place example frames on a dedicated page (e.g., “Examples”) separate from the component library pagematchis an optional secondary filter, not the relevance test — the primary check is structural identity (the candidate is an instance of the component being generated), whichspecs-from-figmaalready enforces. Example instances frequently carry names with no relationship to the component name (e.g. a “Card” usage named “Marketing hero”), so requiringmatchwould exclude legitimate examples. Omittingmatchmeans “every in-scope instance of this component,” narrowed byexclude/parentNames; supplyingmatchnarrows further by nameparentNames?: string[]narrows the search space — example frames are often grouped inside a named parent section or frame (e.g., a frame named"Examples") to distinguish them from test cases or playground instances- Additive, type ↔ schema symmetric, no runtime logic — new optional fields only (Constitution §I/§II); transformer detection/gating logic lives in
specs-from-figma
Options Considered
Option A: include.defaultSlotContent flag + presence-driven processing.instanceExamples (Selected)
Add a single example include flag, include.defaultSlotContent?: boolean (default false), gating the component’s structurally-detected default slot content. Add processing.instanceExamples? with { scope?, match?, exclude?, parentNames? }; its presence both configures detection and enables instance-example output — there is no include.instanceExamples flag. Within the block, match is an optional name filter: the primary relevance test is structural identity (the candidate instance is a usage of the component being generated), so when match is omitted every in-scope instance qualifies, subject to exclude/parentNames.
# Config — instance examples on a dedicated page, inside an "Examples" parent frameprocessing: instanceExamples: # presence = detect AND emit instance examples scope: FILE parentNames: - Examples match: - "{C} / *" exclude: - "* / Deprecated / *"
include: defaultSlotContent: true # emit the component's own default slot content# Config — instance examples alongside the component, default slot content offprocessing: instanceExamples: scope: PAGE match: - "{C} – *" - "{C} Example *"# Config — no name patterns: every instance of the component inside the# "Ready-made examples" frame is an example (identity + parentNames do the scoping)processing: instanceExamples: scope: PAGE parentNames: - Ready-made examplesscope values:
PAGE— search the current Figma page only (default; typical when examples live alongside components)FILE— search all pages in the Figma file (for teams that place examples on a separate page)
parentNames (optional) — one or more frame or section names; a candidate qualifies when its immediate parent matches one of the listed names. Absence means no parent-name filtering.
The parentNames field name was chosen by ranking the realistic candidates for this string[] of ancestor names:
parentNames(selected) — precise (the values are names matched against the parent’s name), plural (signals the array, resolving the ambiguity that a singularparentreads as one value), and keeps Figma’sparentvocabulary (node.parent) without introducing the user-facing “layer” term.parents— concise and parallel with the siblingstring[]filtersmatch/exclude, but hints at parent objects rather than names.parentLayerNames— unambiguous and consistent with house “layer” vocabulary (cf.Props.layer), but verbose against the terse sibling fields, and “Layer” is largely redundant onceparent…Namesalready denotes the name of a containing node.parentLayers/parentLayer— rejected: both imply you pass the layers themselves rather than their names, and the singular is the wrong number for an array.
Containment-flavored names (e.g. within) were also rejected: they imply any-depth nesting, whereas matching is scoped to the immediate parent, so “parent” must remain in the name.
Pros:
- Each example type is gated by the control that fits it — no empty config blocks, no redundant flags
- Instance examples gate exactly like
processing.subcomponents, so the mental model transfers scope: PAGE | FILEcovers all real Figma file organisations;NESTEDis explicitly excludedparentNamessolves the false-positive problem without artificially unique name patterns- One example
includeflag (defaultSlotContent) instead of two — smaller, less error-prone config surface
Cons / Trade-offs:
- The two example types are gated differently (one
includeflag, one byprocessing-block presence). This asymmetry must be documented, but it reflects a real difference (structural vs pattern-detected) and matches the establishedsubcomponentsprecedent.
Option B: Add a separate include.instanceExamples flag (Rejected — earlier draft of this ADR)
An earlier draft gated instance examples with include.instanceExamples?: boolean alongside include.defaultSlotContent, so instance examples required both a processing.instanceExamples detection block and a separate include flag to emit.
Rejected because:
- Redundant gate.
processing.instanceExamplesis already opt-in by absence — configuring detection is the opt-in. A second flag,falseby default, adds a step whose only effect is the “I set up detection but saw no output” foot-gun. - Asymmetric with
subcomponents.processing.subcomponentshas noinclude.subcomponents; its presence is the complete on-switch. Instance examples follow the same detect-and-emit model and should gate identically. - Not parallel with
defaultSlotContent.defaultSlotContentgenuinely needs anincludeflag because it has noprocessingblock (detection is structural) — the flag is its only control. Instance examples already have aprocessingblock, so the flag is pure redundancy. The two example types differ in kind, so gating them the same way was the wrong symmetry to chase.
Option C: Single include.examples flag (Rejected)
Gate all example output with one include.examples?: boolean.
Rejected because: The two example types are different in kind and readiness. Default slot content is structural and may be ready before any instance-example frames are annotated. A single flag forces all-or-nothing and cannot model partial readiness — and it still would not address instance-example detection, which needs patterns regardless.
Option D: processing.instanceExamples as a boolean (Rejected)
Gate instance example detection with processing.instanceExamples?: boolean instead of the match/exclude block.
Rejected because: A boolean provides no way to specify which Figma frames are instance examples. Instance frames are identified by name; without naming patterns the transformer must either guess (fragile) or harvest all frames (noisy). The match/exclude block gives authors control over exactly which frames are harvested, consistent with processing.subcomponents.
Option E: Add processing.defaultSlotContent alongside processing.instanceExamples (Rejected)
Mirror the full subcomponents shape for both example types.
Rejected because: Default slot content is derived from structural Figma data — content placed inside a slot layer. There is no naming convention to configure; detection is entirely structural. A processing.defaultSlotContent block would be an empty config object with no fields to set, adding author surface area for nothing. The include.defaultSlotContent flag is the right and only control.
Decision
Type changes (types/)
| File | Change | Bump |
|---|---|---|
Config.ts | Add include.defaultSlotContent?: boolean | MINOR |
Config.ts | Add processing.instanceExamples?: { scope?, match?, exclude?, parentNames? } | MINOR |
Config.ts | Add include.defaultSlotContent: boolean to ResolvedConfig; add processing.instanceExamples?: { scope, match?, exclude?, parentNames? } (scope required in resolved; match optional) | MINOR |
Config.ts | Add defaultSlotContent: false to DEFAULT_CONFIG.include | MINOR |
There is no include.instanceExamples in Config, ResolvedConfig, or DEFAULT_CONFIG. Instance-example output is governed entirely by the presence of processing.instanceExamples.
Config additions (types/Config.ts):
// processing block — new optional field; its presence is the instance-example on-switchinstanceExamples?: { /** Search boundary. PAGE = current page only (default); FILE = all pages in the file. */ scope?: 'PAGE' | 'FILE'; /** Optional name patterns narrowing which instance frames qualify. Uses {C} (component name) placeholder. Absence = every in-scope instance of the component qualifies (subject to exclude/parentNames). */ match?: string[]; /** Name patterns for frames to exclude. Same {C} syntax as match. */ exclude?: string[]; /** Immediate-parent frame or section names a candidate must be contained within. Absence = no parent-name filtering. */ parentNames?: string[];};
// include block — one new optional flag (default slot content is structural, so the flag is its only control)/** Include the component's default slot content as examples in output. Optional; defaults to false. @since 0.21.0 */defaultSlotContent?: boolean;ResolvedConfig additions (types/Config.ts):
// processing block — scope is required (defaults to PAGE); the block stays optional (absence = off)instanceExamples?: { scope: 'PAGE' | 'FILE'; match?: string[]; exclude?: string[]; parentNames?: string[];};
// include block — flag required (resolved from defaults)defaultSlotContent: boolean;DEFAULT_CONFIG additions (types/Config.ts):
include: { // existing fields unchanged invalidVariants: false, invalidCombinations: true, emptyVariants: false, // new — the only example include flag defaultSlotContent: false,},// processing.instanceExamples is NOT in DEFAULT_CONFIG — its absence means "off",// the same as processing.subcomponents.Schema changes (schema/)
| File | Change | Bump |
|---|---|---|
component.schema.json | Add defaultSlotContent to the config include block | MINOR |
component.schema.json | Add instanceExamples to the config processing block | MINOR |
The config include block gains only defaultSlotContent; it does not gain an instanceExamples boolean.
include additions:
defaultSlotContent: type: boolean description: "Include the component's default slot content as examples in output. Defaults to false."processing addition:
instanceExamples: type: object description: "Instance example detection settings. Absence means no instance example detection or output." properties: scope: type: string enum: [PAGE, FILE] description: "Search boundary. PAGE = current Figma page only (default); FILE = all pages in the file." match: type: array items: { type: string } description: "Optional name patterns narrowing which instance frames qualify. Uses {C} placeholder. Absence = every in-scope instance of the component qualifies (subject to exclude/parentNames)." exclude: type: array items: { type: string } description: "Name patterns for frames to exclude. Same {C} syntax as match." parentNames: type: array items: { type: string } description: "Immediate-parent frame or section names a candidate must be contained within. Absence = no parent-name filtering." additionalProperties: falseOut of scope for this ADR
- Transformer detection logic for instance examples — belongs in
specs-from-figma; this ADR defines the config shape only - Pro-license gating — both example registries are additionally gated on a Pro license at emission time, enforced in
specs-from-figmavia the existingentitled()mechanism (the same pattern used for token references and other premium output). This ADR does not introduce the license model; it only notes that the config flags/blocks are necessary-but-not-sufficient for output (see Notes) processing.defaultSlotContent— structural detection requires no config; if naming-pattern detection is ever added, aprocessing.defaultSlotContentblock would follow the subcomponents model
Notes
- Instance examples are presence-gated. When
processing.instanceExamplesis omitted, the transformer performs no detection and emits noinstanceExamples— exactly theprocessing.subcomponentsmodel. When present (and the license is Pro), examples are both detected and emitted. There is noinclude.instanceExamplesflag. defaultSlotContentgates the component’s own slot content. It controls whether the structurally-detected default content of the component’s slots is emitted intoslotContentExamplesand referenced from the component’s slot bindings. Instance examples contribute their own fills to the sameslotContentExamplesregistry independently of this flag (see ADR-047/048).- Pro license required for emission. Both
defaultSlotContentoutput andinstanceExamplesoutput are omitted on the free tier regardless of config, mirroring other premium output. The config flag/block is necessary but not sufficient. scopedefaults toPAGEinResolvedConfig.FILEis opt-in for teams with a dedicated examples page.NESTEDis intentionally absent — component instances used as examples cannot live inside the component frame itself.parentNamesis an immediate-parent filter, not a full path. Sufficient for the common convention of grouping examples inside a frame named"Examples", without requiring authors to express full paths.matchis optional. The primary relevance test is structural identity (the candidate is a usage of the component being generated), enforced inspecs-from-figma.matchonly narrows that set by frame name; omitting it accepts every in-scope instance (still subject toexclude/parentNames). This matters because example instances commonly have names unrelated to the component name.
Type ↔ Schema Impact
- Symmetric: Yes
- Parity check:
Config.include.defaultSlotContent?: boolean↔ configinclude.properties.defaultSlotContentConfig.processing.instanceExamples?: { scope?, match?, exclude?, parentNames? }↔ configprocessing.properties.instanceExamples(norequiredblock —matchoptional)- No
include.instanceExamplesexists in either the type or the schema — symmetric by absence
Downstream Impact
| Consumer | Impact | Action required |
|---|---|---|
specs-from-figma | Reads processing.instanceExamples to detect example frames and to gate their output (presence-driven); gates slotContentExamples output on include.defaultSlotContent. Both registries additionally gated on a Pro license | Implement detection + dual (config-presence/flag + license) gating |
specs-cli | Recompile; config surface adds include.defaultSlotContent and processing.instanceExamples. ConfigLoader allowlist must not accept a stale include.instanceExamples | Recompile; update config docs/help text |
specs-plugin-2 | Recompile; the plugin’s instance-examples toggle drives the presence of processing.instanceExamples (no include.instanceExamples); the default-slot-content toggle maps to include.defaultSlotContent | Recompile; map UI toggles accordingly; Pro-gate the controls |
Semver Decision
Version bump: 0.20.0 → 0.21.0 (MINOR)
Justification: Adds optional fields to Config, ResolvedConfig, and DEFAULT_CONFIG (include.defaultSlotContent, processing.instanceExamples) and the corresponding schema entries. Purely additive — no existing field is removed or narrowed → MINOR per Constitution §III. (Because this ADR is still DRAFT/unreleased, dropping the earlier-draft include.instanceExamples is not a consumer-facing removal — that flag never shipped.)
Consequences
include.defaultSlotContentis the single example output flag — it gives teams control over the component’s own default slot content; it defaults tofalseto preserve existing output for unannotated componentsprocessing.instanceExamplesadapts theprocessing.subcomponentsmodel exactly: presence is the complete on-switch (detection and output), absence means off, same match/exclude vocabulary;scopeusesPAGE | FILE(notNESTED), andparentNamesnarrows the search by immediate-parent name- The two example types are gated by different mechanisms by design — an
includeflag for structural default slot content,processing-block presence for pattern-detected instance examples — reflecting their different natures rather than forcing an artificial symmetry - Both registries are additionally Pro-gated at emission; the config is necessary but not sufficient for output
DEFAULT_CONFIGcarriesinclude.defaultSlotContent: false;processing.instanceExamplesis absent there (absence = off), so no special-casing is needed when merging a partialConfig
Revision History
matchmade optional. The first draft requiredprocessing.instanceExamples.match(schemarequired: [match], typematch: string[]). Implementation inspecs-from-figmaconfirmed the structural identity check (candidate instance ∈ the component’s variants) is the real relevance filter, withmatchonly a secondary name narrowing. Requiring it excluded legitimate examples whose frame names bear no relation to the component name.matchis now optional (match?: string[], no schemarequired); its absence accepts every in-scope instance, subject toexclude/parentNames. Still DRAFT/unreleased, so no consumer-facing change.- Shift from the original draft — removed
include.instanceExamples. The first draft of this ADR added two exampleincludeflags (slotContentExamples/instanceExamples) and treated detection (processing.instanceExamples) and output (include.instanceExamples) as separate concerns. Implementation surfaced that the second flag was a redundant gate, asymmetric withprocessing.subcomponents, and a source of “configured detection but no output” confusion. Instance examples are now governed solely by the presence ofprocessing.instanceExamples(Option A; the old approach is recorded as Option B). Separately, the remaining slot-content flag was renamedslotContentExamples → defaultSlotContentso the config flag (the component’s default slot content) is no longer confused with theComponent.slotContentExamplesdata registry that aggregates fills from both sources.