Reactor Docs
Reactor Docs
  • CPE Comprehensive Developer Guide
  • Experience Cloud CPE Specification
  • Getting Started
  • Obsidian Syntax Test
  • Login

    Experience Cloud CPE Specification

    Complete technical specification for Experience Cloud LWC Custom Property Editors

    By Marc Swan

    #Experience Cloud LWC Custom Property Editors

    #Complete Technical Specification: Rules, Design Patterns & Gotchas

    Version: 1.0 Author: Marc Swan (Salesforce Architect) Last Updated: December 2025
    Minimum API Version: 61.0+


    #Table of Contents

    1. Overview
    2. CPE Declaration & Registration (Critical Differences)
    3. The Property Editor Contract (Mandatory Rules)
    4. XML Configuration Rules
    5. Event Patterns & Communication
    6. Design Patterns
    7. Critical Gotchas & Pitfalls
    8. Custom Lightning Types (ExperiencePropertyTypeBundle)
    9. Flow vs Experience Cloud CPE Differences
    10. Validation Patterns
    11. Complex Data Handling
    12. Reusability Strategies
    13. Testing & Debugging
    14. Reference Implementation

    #1. Overview

    Custom Property Editors (CPEs) are Lightning Web Components that replace default Experience Builder property inputs with custom UIs. They enable visual pickers, complex data structures, and enhanced validation beyond standard text/picklist inputs.

    #When to Use CPEs

    Use CPEUse Standard Properties
    Visual pickers (alignment, color)Simple strings/booleans
    Complex nested configurationsStatic picklists
    Real-time validation feedbackSingle integer inputs
    Grouped property tabs/accordionsProperties with datasource
    Record selection with filteringBasic required validation

    #Key Architecture Concepts

    ┌─────────────────────────────────────────────────────────┐
    │                 Experience Builder                       │
    │  ┌─────────────────────────────────────────────────┐    │
    │  │            Property Panel                        │    │
    │  │  ┌─────────────────────────────────────────┐    │    │
    │  │  │     Custom Property Editor (LWC)        │    │    │
    │  │  │  ┌─────────────────────────────────┐    │    │    │
    │  │  │  │  @api value (from framework)    │    │    │    │
    │  │  │  │  @api label, description, etc.  │    │    │    │
    │  │  │  │  ─────────────────────────────  │    │    │    │
    │  │  │  │  valuechange event (to framework)│    │    │    │
    │  │  │  └─────────────────────────────────┘    │    │    │
    │  │  └─────────────────────────────────────────┘    │    │
    │  └─────────────────────────────────────────────────┘    │
    │                         │                                │
    │                         ▼                                │
    │  ┌─────────────────────────────────────────────────┐    │
    │  │           Target Component (LWC)                 │    │
    │  │           @api propertyName                      │    │
    │  └─────────────────────────────────────────────────┘    │
    └─────────────────────────────────────────────────────────┘
    

    #2. CPE Declaration & Registration (Critical Differences)

    Understanding how to correctly declare and register CPEs is the most common source of errors when building Experience Cloud components. The patterns differ significantly from Flow/Apex CPEs.

    #2.1 Experience Cloud vs Flow/Apex: Side-by-Side Comparison

    ┌─────────────────────────────────────────────────────────────────────────────┐
    │                    EXPERIENCE CLOUD CPE                                      │
    ├─────────────────────────────────────────────────────────────────────────────┤
    │ Registration:     editor="c/myPropertyEditor" (in target component XML)     │
    │ Value Access:     @api value                                                 │
    │ Update Event:     'valuechange'                                              │
    │ Event Detail:     { value: newValue }                                        │
    │ Validation:       @api errors (received FROM framework)                      │
    │ Context:          @api schema                                                │
    │ CPE isExposed:    false                                                      │
    └─────────────────────────────────────────────────────────────────────────────┘
    
    ┌─────────────────────────────────────────────────────────────────────────────┐
    │                    FLOW/APEX CPE                                             │
    ├─────────────────────────────────────────────────────────────────────────────┤
    │ Registration:     configurationEditor="c-my-property-editor" (in Apex/XML)  │
    │ Value Access:     @api inputVariables (array of {name, value, valueDataType})│
    │ Update Event:     'configuration_editor_input_value_changed'                 │
    │ Event Detail:     { name, newValue, newValueDataType }                       │
    │ Validation:       @api validate() method (you implement, return errors)      │
    │ Context:          @api builderContext, @api genericTypeMappings              │
    │ CPE isExposed:    false                                                      │
    └─────────────────────────────────────────────────────────────────────────────┘
    

    #2.2 Experience Cloud CPE: Correct Declaration

    #⚠️ CRITICAL: Target Component XML Registration

    <?xml version="1.0" encoding="UTF-8"?>
    <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
        <apiVersion>61.0</apiVersion>
        <isExposed>true</isExposed>
        <masterLabel>My Component</masterLabel>
        
        <targets>
            <target>lightningCommunity__Page</target>
            <target>lightningCommunity__Default</target>
        </targets>
        
        <targetConfigs>
            <targetConfig targets="lightningCommunity__Default">
                <!-- Standard property (no custom editor) -->
                <property name="title" type="String" label="Title"/>
                
                <!-- Property WITH custom editor -->
                <property name="alignment" 
                          type="String" 
                          label="Text Alignment"
                          editor="c/alignmentPropertyEditor"/>
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                          EXPERIENCE CLOUD: Uses "editor" attribute
                          Format: namespace/componentName (camelCase)
                
                <!-- Property using Custom Lightning Type -->
                <property name="styling" 
                          type="cardStyleType" 
                          label="Card Style"/>
            </targetConfig>
        </targetConfigs>
    </LightningComponentBundle>
    

    #⚠️ CRITICAL: CPE Component XML (Must be isExposed=false)

    <?xml version="1.0" encoding="UTF-8"?>
    <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
        <apiVersion>61.0</apiVersion>
        <isExposed>false</isExposed>  <!-- ⚠️ MUST be false for CPEs -->
        <!-- NO targets needed - CPE is invoked by framework, not dragged onto page -->
    </LightningComponentBundle>
    

    #⚠️ CRITICAL: CPE JavaScript Class

    import { LightningElement, api } from 'lwc';
    
    export default class AlignmentPropertyEditor extends LightningElement {
        // ═══════════════════════════════════════════════════════════════════
        // EXPERIENCE CLOUD CONTRACT: These 6 @api properties are REQUIRED
        // ═══════════════════════════════════════════════════════════════════
        @api label;        // String - Display label from XML
        @api description;  // String - Help text
        @api required;     // Boolean - Is property required
        @api value;        // Any - THE CURRENT VALUE (read from here!)
        @api errors;       // Array - Validation errors FROM framework
        @api schema;       // Object - JSON Schema info for complex types
        
        // ═══════════════════════════════════════════════════════════════════
        // EXPERIENCE CLOUD EVENT: 'valuechange' with bubbles+composed=true
        // ═══════════════════════════════════════════════════════════════════
        handleChange(event) {
            this.dispatchEvent(new CustomEvent('valuechange', {
                detail: { value: event.target.value },
                bubbles: true,    // ⚠️ REQUIRED
                composed: true    // ⚠️ REQUIRED
            }));
        }
    }
    

    #2.3 Flow/Apex CPE: Correct Declaration (For Comparison)

    #Flow Screen Component Registration

    <?xml version="1.0" encoding="UTF-8"?>
    <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
        <apiVersion>61.0</apiVersion>
        <isExposed>true</isExposed>
        
        <targets>
            <target>lightning__FlowScreen</target>
        </targets>
        
        <targetConfigs>
            <targetConfig targets="lightning__FlowScreen"
                          configurationEditor="c-my-flow-cpe">
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                          FLOW: Uses "configurationEditor" attribute
                          Format: kebab-case with namespace prefix
                <property name="volume" type="Integer" role="inputOnly"/>
            </targetConfig>
        </targetConfigs>
    </LightningComponentBundle>
    

    #Apex Invocable Action Registration

    public class MyInvocableAction {
        
        @InvocableMethod(
            label='My Action'
            configurationEditor='c-my-action-cpe'  // ⚠️ kebab-case!
        )
        public static List<Result> execute(List<Request> requests) {
            // Implementation
        }
        
        public class Request {
            @InvocableVariable(required=true)
            public String inputValue;
        }
    }
    

    #Flow/Apex CPE JavaScript Class

    import { LightningElement, api } from 'lwc';
    
    export default class MyFlowCpe extends LightningElement {
        // ═══════════════════════════════════════════════════════════════════
        // FLOW CONTRACT: Uses inputVariables array, NOT single value
        // ═══════════════════════════════════════════════════════════════════
        _inputVariables = [];
        
        @api
        get inputVariables() {
            return this._inputVariables;
        }
        set inputVariables(variables) {
            this._inputVariables = variables || [];
        }
        
        @api builderContext;        // Flow context (screens, variables, etc.)
        @api genericTypeMappings;   // For generic sObject types
        @api elementInfo;           // Info about current element
        
        // Access a specific property value
        get volumeValue() {
            const param = this._inputVariables.find(
                ({ name }) => name === 'volume'
            );
            return param?.value;
        }
        
        // ═══════════════════════════════════════════════════════════════════
        // FLOW VALIDATION: You implement validate() method
        // ═══════════════════════════════════════════════════════════════════
        @api
        validate() {
            const errors = [];
            if (!this.volumeValue || this.volumeValue < 0) {
                errors.push({
                    key: 'volume',
                    errorString: 'Volume must be a positive number'
                });
            }
            return errors;  // Return empty array if valid
        }
        
        // ═══════════════════════════════════════════════════════════════════
        // FLOW EVENT: 'configuration_editor_input_value_changed'
        // ═══════════════════════════════════════════════════════════════════
        handleChange(event) {
            this.dispatchEvent(new CustomEvent(
                'configuration_editor_input_value_changed',  // ⚠️ Different event!
                {
                    bubbles: true,
                    cancelable: false,
                    composed: true,
                    detail: {
                        name: 'volume',              // ⚠️ Property name required
                        newValue: event.target.value,
                        newValueDataType: 'Number'   // ⚠️ Data type required
                    }
                }
            ));
        }
    }
    

    #2.4 Declaration Comparison Table

    AspectExperience CloudFlow/Apex
    XML Attributeeditor="c/componentName"configurationEditor="c-component-name"
    Naming FormatcamelCase with /kebab-case with -
    Where Declared<property> tag<targetConfig> or @InvocableMethod
    CPE isExposedfalsefalse
    Value Property@api value (direct)@api inputVariables (array)
    Event Namevaluechangeconfiguration_editor_input_value_changed
    Event Detail{ value }{ name, newValue, newValueDataType }
    Validation@api errors (received)@api validate() (implemented)

    #2.5 Common Declaration Mistakes

    #❌ WRONG: Using Flow Pattern in Experience Cloud

    <!-- WRONG: configurationEditor doesn't work for Experience Cloud -->
    <targetConfig targets="lightningCommunity__Default" 
                  configurationEditor="c-alignment-editor">
    
    // WRONG: inputVariables doesn't exist in Experience Cloud CPE
    @api inputVariables;
    
    // WRONG: Flow event name doesn't work in Experience Cloud
    this.dispatchEvent(new CustomEvent('configuration_editor_input_value_changed', {
        detail: { name: 'alignment', newValue: 'center', newValueDataType: 'String' }
    }));
    

    #✅ CORRECT: Experience Cloud Pattern

    <!-- CORRECT: Use editor attribute on property -->
    <property name="alignment" 
              type="String" 
              label="Alignment"
              editor="c/alignmentPropertyEditor"/>
    
    // CORRECT: Use @api value
    @api value;
    
    // CORRECT: Use valuechange event
    this.dispatchEvent(new CustomEvent('valuechange', {
        detail: { value: 'center' },
        bubbles: true,
        composed: true
    }));
    

    #❌ WRONG: Using Experience Cloud Pattern in Flow

    <!-- WRONG: editor attribute doesn't work for Flow -->
    <property name="volume" type="Integer" editor="c/volumeEditor"/>
    
    // WRONG: @api value doesn't receive Flow data
    @api value;
    
    // WRONG: valuechange isn't recognized by Flow Builder
    this.dispatchEvent(new CustomEvent('valuechange', {
        detail: { value: 50 }
    }));
    

    #✅ CORRECT: Flow Pattern

    <!-- CORRECT: configurationEditor on targetConfig -->
    <targetConfig targets="lightning__FlowScreen" 
                  configurationEditor="c-volume-editor">
        <property name="volume" type="Integer"/>
    </targetConfig>
    
    // CORRECT: Use inputVariables array
    @api inputVariables;
    
    // CORRECT: Use Flow event with full detail
    this.dispatchEvent(new CustomEvent('configuration_editor_input_value_changed', {
        bubbles: true,
        cancelable: false,
        composed: true,
        detail: {
            name: 'volume',
            newValue: 50,
            newValueDataType: 'Number'
        }
    }));
    

    #2.6 Quick Reference: Which Pattern to Use?

    Is your component for Experience Builder (Experience Cloud sites)?
    │
    ├─ YES → Use Experience Cloud Pattern:
    │        • editor="c/componentName" on <property>
    │        • @api value
    │        • 'valuechange' event
    │        • @api errors (received)
    │
    └─ NO → Is it for Flow Builder?
            │
            ├─ YES → Use Flow Pattern:
            │        • configurationEditor="c-component-name" on <targetConfig>
            │        • @api inputVariables
            │        • 'configuration_editor_input_value_changed' event
            │        • @api validate() method
            │
            └─ NO → Standard LWC (no CPE needed)
    

    #3. The Property Editor Contract (Mandatory Rules)

    #⚠️ RULE: Required @api Properties

    Every Experience Cloud CPE MUST expose these six public properties:

    import { LightningElement, api } from 'lwc';
    
    export default class MyPropertyEditor extends LightningElement {
        // ═══════════════════════════════════════════════════════════
        // MANDATORY: These six properties form the CPE contract
        // ═══════════════════════════════════════════════════════════
        
        @api label;        // String: Display label from XML
        @api description;  // String: Help text for property
        @api required;     // Boolean: Whether property is mandatory
        @api value;        // Any: Current property value (READ FROM HERE)
        @api errors;       // Array: Validation error objects [{message: '...'}]
        @api schema;       // Object: JSON Schema for complex types
    }
    

    #⚠️ RULE: The valuechange Event Contract

    To update property values, dispatch valuechange with both flags set to true:

    handleChange(event) {
        const newValue = event.target.value;
        
        // CRITICAL: bubbles AND composed must be true
        this.dispatchEvent(new CustomEvent('valuechange', {
            detail: { value: newValue },
            bubbles: true,    // ← Required
            composed: true    // ← Required (crosses shadow boundary)
        }));
    }
    

    #⚠️ RULE: CPE XML Configuration

    The property editor component must have isExposed="false":

    <!-- myPropertyEditor.js-meta.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
        <apiVersion>61.0</apiVersion>
        <isExposed>false</isExposed>  <!-- CPEs are NOT draggable -->
    </LightningComponentBundle>
    

    #4. XML Configuration Rules

    #Target Component XML Structure

    <?xml version="1.0" encoding="UTF-8"?>
    <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
        <apiVersion>61.0</apiVersion>
        <isExposed>true</isExposed>
        <masterLabel>My Configurable Component</masterLabel>
        
        <targets>
            <target>lightningCommunity__Page</target>
            <target>lightningCommunity__Default</target>
        </targets>
        
        <targetConfigs>
            <targetConfig targets="lightningCommunity__Default">
                <!-- Standard property (default UI) -->
                <property name="title" 
                          type="String" 
                          label="Title"
                          default="Welcome"/>
                
                <!-- Property with custom editor -->
                <property name="alignment" 
                          type="String" 
                          label="Text Alignment"
                          editor="c/alignmentEditor"/>
                
                <!-- Property using ExperiencePropertyTypeBundle -->
                <property name="layoutConfig" 
                          type="layoutStyleType" 
                          label="Layout Settings"/>
            </targetConfig>
        </targetConfigs>
    </LightningComponentBundle>
    

    #⚠️ RULE: XML Modification Restrictions (Critical for Packages)

    Once a component is used in a site or managed package, you CANNOT:

    Prohibited ChangeWorkaround
    Add required="true" to existing propertyCreate new optional property
    Remove existing <property> tagDeprecate with default value
    Change property type (Integer→String)Create new property with correct type
    Remove default if required="true"Keep default value
    Add min where none existedHandle in CPE validation
    Reduce existing max valueHandle in CPE validation

    Best Practice: Finalize component design before packaging. Consider future extensibility.


    #5. Event Patterns & Communication

    #Understanding bubbles and composed

    ┌────────────────────────────────────────────────────────────┐
    │ bubbles: false, composed: false (DEFAULT - Most Restrictive)│
    │ ───────────────────────────────────────────────────────────│
    │ Event stays within component, doesn't cross shadow boundary │
    │ Use for: Internal component events                          │
    └────────────────────────────────────────────────────────────┘
    
    ┌────────────────────────────────────────────────────────────┐
    │ bubbles: true, composed: false                              │
    │ ───────────────────────────────────────────────────────────│
    │ Event bubbles up but stops at shadow boundary               │
    │ Use for: Parent component communication (same shadow tree)  │
    └────────────────────────────────────────────────────────────┘
    
    ┌────────────────────────────────────────────────────────────┐
    │ bubbles: true, composed: true (REQUIRED FOR CPE)           │
    │ ───────────────────────────────────────────────────────────│
    │ Event bubbles up AND crosses shadow boundaries              │
    │ Use for: Experience Builder CPE valuechange events          │
    └────────────────────────────────────────────────────────────┘
    
    ┌────────────────────────────────────────────────────────────┐
    │ bubbles: false, composed: true                              │
    │ ───────────────────────────────────────────────────────────│
    │ ⚠️ UNSUPPORTED on Salesforce Platform - Undefined behavior  │
    └────────────────────────────────────────────────────────────┘
    

    #⚠️ RULE: Event Data Handling

    Never pass @api or @wire data directly in events:

    // ❌ BAD: Passing reactive data directly
    handleSelect(event) {
        this.dispatchEvent(new CustomEvent('valuechange', {
            detail: { value: this.someWiredData }  // Read-only membrane issue!
        }));
    }
    
    // ✅ GOOD: Copy data to new object
    handleSelect(event) {
        const valueCopy = JSON.parse(JSON.stringify(this.someWiredData));
        this.dispatchEvent(new CustomEvent('valuechange', {
            detail: { value: valueCopy }
        }));
    }
    

    #6. Design Patterns

    #Pattern 1: Visual Selection Editor

    Best for: Alignment pickers, icon selectors, color pickers

    <!-- alignmentEditor.html -->
    <template>
        <div class="slds-form-element">
            <label class="slds-form-element__label">{label}</label>
            <div class="slds-form-element__control">
                <lightning-button-group>
                    <lightning-button-icon
                        icon-name="utility:left_align_text"
                        data-value="left"
                        variant={leftVariant}
                        onclick={handleSelect}
                        alternative-text="Left">
                    </lightning-button-icon>
                    <lightning-button-icon
                        icon-name="utility:center_align_text"
                        data-value="center"
                        variant={centerVariant}
                        onclick={handleSelect}
                        alternative-text="Center">
                    </lightning-button-icon>
                    <lightning-button-icon
                        icon-name="utility:right_align_text"
                        data-value="right"
                        variant={rightVariant}
                        onclick={handleSelect}
                        alternative-text="Right">
                    </lightning-button-icon>
                </lightning-button-group>
            </div>
            <template if:true={description}>
                <div class="slds-form-element__help">{description}</div>
            </template>
        </div>
    </template>
    
    // alignmentEditor.js
    import { LightningElement, api } from 'lwc';
    
    export default class AlignmentEditor extends LightningElement {
        @api label;
        @api description;
        @api required;
        @api value = 'left';
        @api errors;
        @api schema;
        
        get leftVariant() {
            return this.value === 'left' ? 'brand' : 'border';
        }
        
        get centerVariant() {
            return this.value === 'center' ? 'brand' : 'border';
        }
        
        get rightVariant() {
            return this.value === 'right' ? 'brand' : 'border';
        }
        
        handleSelect(event) {
            const newValue = event.currentTarget.dataset.value;
            this.dispatchEvent(new CustomEvent('valuechange', {
                detail: { value: newValue },
                bubbles: true,
                composed: true
            }));
        }
    }
    

    #Pattern 2: Schema-Driven Editor

    Best for: Reusable editors that adapt to different configurations

    // schemaAwareEditor.js
    import { LightningElement, api } from 'lwc';
    
    export default class SchemaAwareEditor extends LightningElement {
        @api label;
        @api description;
        @api required;
        @api value;
        @api errors;
        @api schema;
        
        get options() {
            // Read allowed values from schema
            if (this.schema?.enum) {
                return this.schema.enum.map(val => ({
                    label: this.formatLabel(val),
                    value: val
                }));
            }
            return [];
        }
        
        get minValue() {
            return this.schema?.minimum ?? null;
        }
        
        get maxValue() {
            return this.schema?.maximum ?? null;
        }
        
        formatLabel(value) {
            // Convert camelCase/snake_case to Title Case
            return value
                .replace(/([A-Z])/g, ' $1')
                .replace(/_/g, ' ')
                .replace(/^\w/, c => c.toUpperCase());
        }
        
        handleChange(event) {
            this.dispatchEvent(new CustomEvent('valuechange', {
                detail: { value: event.target.value },
                bubbles: true,
                composed: true
            }));
        }
    }
    

    #Pattern 3: Accordion/Grouped Properties

    Best for: Complex components with many related properties

    <!-- groupedPropertyEditor.html -->
    <template>
        <lightning-accordion allow-multiple-sections-open active-section-name={activeSections}>
            <lightning-accordion-section name="appearance" label="Appearance">
                <lightning-input 
                    label="Background Color" 
                    type="color"
                    value={backgroundColor}
                    onchange={handleColorChange}>
                </lightning-input>
                <!-- More appearance properties -->
            </lightning-accordion-section>
            
            <lightning-accordion-section name="layout" label="Layout">
                <lightning-combobox
                    label="Alignment"
                    options={alignmentOptions}
                    value={alignment}
                    onchange={handleAlignmentChange}>
                </lightning-combobox>
                <!-- More layout properties -->
            </lightning-accordion-section>
            
            <lightning-accordion-section name="advanced" label="Advanced">
                <!-- Advanced properties -->
            </lightning-accordion-section>
        </lightning-accordion>
    </template>
    

    #7. Critical Gotchas & Pitfalls

    #🚨 GOTCHA #1: Component Isolation

    Problem: CPEs are completely isolated from their target components. You cannot dynamically push data from the target component into the CPE.

    // ❌ This will NOT work
    // Target component trying to update CPE
    this.template.querySelector('c-my-property-editor').customData = someData;
    

    Solution: Use property defaults or accept that CPE only receives value from framework.


    #🚨 GOTCHA #2: Flow CPE vs Experience Cloud CPE

    These are completely different APIs!

    AspectExperience Cloud CPEFlow CPE
    Value Source@api value@api inputVariables (array)
    Value Update Eventvaluechangeconfiguration_editor_input_value_changed
    Event Detail{ value: newValue }{ name, newValue, newValueDataType }
    Validation@api errors@api validate() method
    Schema@api schema@api builderContext, @api genericTypeMappings

    Experience Cloud CPE:

    handleChange(event) {
        this.dispatchEvent(new CustomEvent('valuechange', {
            detail: { value: event.target.value },
            bubbles: true,
            composed: true
        }));
    }
    

    Flow CPE:

    handleChange(event) {
        this.dispatchEvent(new CustomEvent('configuration_editor_input_value_changed', {
            bubbles: true,
            cancelable: false,
            composed: true,
            detail: {
                name: 'myProperty',
                newValue: event.target.value,
                newValueDataType: 'String'
            }
        }));
    }
    

    #🚨 GOTCHA #3: Kebab-Case vs CamelCase

    Problem: Component references use kebab-case while file names use camelCase.

    <!-- XML configuration -->
    <property name="alignment" editor="c/alignmentPropertyEditor"/>
                                       ^^^^^^^^^^^^^^^^^^^^^^
                                       camelCase (namespace/componentName)
    
    force-app/main/default/lwc/
      alignmentPropertyEditor/       ← Folder name: camelCase
        alignmentPropertyEditor.js   ← File name: camelCase
        alignmentPropertyEditor.html
        alignmentPropertyEditor.js-meta.xml
    

    #🚨 GOTCHA #4: Array/List Property Limitations

    Problem: Complex types like arrays are not natively supported in property values.

    Solution: Serialize to JSON strings:

    // In CPE
    get itemsArray() {
        try {
            return this.value ? JSON.parse(this.value) : [];
        } catch (e) {
            console.error('Failed to parse items:', e);
            return [];
        }
    }
    
    handleAddItem(event) {
        const newItem = event.detail;
        const updated = [...this.itemsArray, newItem];
        
        this.dispatchEvent(new CustomEvent('valuechange', {
            detail: { value: JSON.stringify(updated) },
            bubbles: true,
            composed: true
        }));
    }
    
    // In target component
    @api itemsJson;
    
    get items() {
        try {
            return this.itemsJson ? JSON.parse(this.itemsJson) : [];
        } catch (e) {
            return [];
        }
    }
    

    #🚨 GOTCHA #5: Missing bubbles or composed

    Symptom: Value changes don't persist; Experience Builder doesn't receive updates.

    Cause: Missing bubbles: true and/or composed: true on valuechange event.

    Debugging:

    handleChange(event) {
        console.log('Dispatching valuechange with:', event.target.value);
        
        const customEvent = new CustomEvent('valuechange', {
            detail: { value: event.target.value },
            bubbles: true,    // Check this!
            composed: true    // Check this!
        });
        
        console.log('Event bubbles:', customEvent.bubbles);
        console.log('Event composed:', customEvent.composed);
        
        this.dispatchEvent(customEvent);
    }
    

    #🚨 GOTCHA #6: LWR Site Publishing Model

    Problem: Components are "frozen" at publish time. Changes to component logic require republishing the entire site.

    Impact:

    • Hot fixes require full site republish
    • Cannot push updates to published components without admin action
    • All active sessions see old version until refresh after republish

    Mitigation:

    • Test thoroughly in preview before publishing
    • Consider feature flags stored in Custom Metadata for runtime toggles
    • Document republish requirements for production changes

    #🚨 GOTCHA #7: Single Editor Limitation

    Problem: Only one custom property editor can be active per component instance.

    Workaround: Create a single "master" CPE that handles all properties with tabs/accordion:

    // masterPropertyEditor.js - handles multiple properties
    export default class MasterPropertyEditor extends LightningElement {
        @api label;       // May contain combined label
        @api value;       // May contain JSON with multiple property values
        @api schema;      // May contain schema for all properties
        
        get parsedValue() {
            try {
                return this.value ? JSON.parse(this.value) : {};
            } catch {
                return {};
            }
        }
        
        handlePropertyChange(event) {
            const propertyName = event.target.dataset.property;
            const newValue = event.target.value;
            
            const updated = {
                ...this.parsedValue,
                [propertyName]: newValue
            };
            
            this.dispatchEvent(new CustomEvent('valuechange', {
                detail: { value: JSON.stringify(updated) },
                bubbles: true,
                composed: true
            }));
        }
    }
    

    #🚨 GOTCHA #8: Conditional Properties Not Supported

    Problem: You cannot show/hide properties based on other property values without a CPE.

    Solution: Use CPE to implement conditional rendering:

    <template>
        <lightning-combobox
            label="Display Type"
            options={displayTypeOptions}
            value={displayType}
            onchange={handleDisplayTypeChange}>
        </lightning-combobox>
        
        <!-- Only show if displayType is 'custom' -->
        <template if:true={showCustomOptions}>
            <lightning-input
                label="Custom Value"
                value={customValue}
                onchange={handleCustomValueChange}>
            </lightning-input>
        </template>
    </template>
    

    #🚨 GOTCHA #9: Dynamic Picklists in Experience Cloud

    Problem: Apex VisualEditor.DynamicPickList works for lightningCommunity__Default but NOT for lightning__FlowScreen.

    // This works for Experience Cloud, NOT for Flows
    public class RecordTypePicklist extends VisualEditor.DynamicPickList {
        VisualEditor.DesignTimePageContext context;
        
        public RecordTypePicklist(VisualEditor.DesignTimePageContext context) {
            this.context = context;
        }
        
        public override VisualEditor.DynamicPickListRows getValues() {
            // Implementation
        }
    }
    

    #🚨 GOTCHA #10: Expression Bindings

    Problem: Special expressions like {!recordId} and {!objectApiName} work in property values but not within CPE logic.

    What Works:

    <!-- In Experience Builder property panel -->
    <property name="recordId" type="String" expression="{!recordId}"/>
    

    What Doesn't Work:

    // CPE cannot access these expressions programmatically
    // The value comes pre-resolved from the framework
    

    #8. Custom Lightning Types (ExperiencePropertyTypeBundle)

    Custom Lightning Types allow you to create reusable, complex property configurations with grouped sub-properties, built-in validation, and optional custom UI layouts. They're defined using the ExperiencePropertyTypeBundle metadata type.

    #8.1 Architecture Overview

    force-app/main/default/
      experiencePropertyTypeBundles/
        myCustomType/
          schema.json       ← REQUIRED: Property structure & validation
          design.json       ← OPTIONAL: UI layout & editor overrides
    

    #8.2 When to Use Custom Lightning Types

    Use Custom TypeUse Standard Properties
    Multiple related properties (borders, spacing)Single isolated properties
    Reusable configurations across componentsComponent-specific settings
    Grouped UI (tabs/accordion) neededFlat property list is fine
    Complex validation rulesSimple required/min/max
    Team standardization of property patternsOne-off implementations

    #8.3 Available Built-in Lightning Types

    Salesforce provides these out-of-the-box lightning:type values for use in schema.json:

    Lightning TypeUI EditorData TypeUse For
    lightning__stringTypeText inputStringGeneral text
    lightning__integerTypeNumber inputIntegerWhole numbers
    lightning__colorTypeColor pickerString (hex)Colors
    lightning__dateTimeTypeDate/time pickerDateTimeDates
    lightning__booleanTypeToggle/checkboxBooleanTrue/false
    lightning__urlTypeURL inputStringLinks
    lightning__textTypeText areaStringLong text
    lightning__richTextTypeRich text editorStringFormatted text
    lightning__imageTypeImage selectorString (URL)Images
    lightning__iconTypeIcon pickerStringSLDS icons

    #8.4 Schema.json Configuration (Required)

    The schema.json file defines property structure, types, and validation using JSON Schema specification with Salesforce extensions.

    #Basic Schema Structure

    {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
            "propertyName": {
                "type": "string",
                "lightning:type": "lightning__stringType",
                "title": "Display Label",
                "description": "Help text shown to admin",
                "default": "defaultValue"
            }
        },
        "required": ["propertyName"]
    }
    

    #Complete Schema Example: Border & Layout Type

    {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "title": "Layout and Border Style",
        "description": "Configure component layout and border styling",
        "properties": {
            "borderStyle": {
                "type": "string",
                "lightning:type": "lightning__stringType",
                "title": "Border Style",
                "description": "Style of the component border",
                "enum": ["none", "solid", "dashed", "dotted", "double"],
                "default": "none"
            },
            "borderWidth": {
                "type": "integer",
                "lightning:type": "lightning__integerType",
                "title": "Border Width",
                "description": "Width in pixels (0-20)",
                "minimum": 0,
                "maximum": 20,
                "default": 1
            },
            "borderColor": {
                "type": "string",
                "lightning:type": "lightning__colorType",
                "title": "Border Color",
                "description": "Color of the border",
                "default": "#cccccc"
            },
            "borderRadius": {
                "type": "integer",
                "lightning:type": "lightning__integerType",
                "title": "Border Radius",
                "description": "Corner roundness in pixels",
                "minimum": 0,
                "maximum": 50,
                "default": 0
            },
            "layoutWidth": {
                "type": "string",
                "lightning:type": "lightning__stringType",
                "title": "Width",
                "description": "Component width",
                "enum": ["auto", "full", "half", "third", "quarter"],
                "default": "auto"
            },
            "layoutHeight": {
                "type": "integer",
                "lightning:type": "lightning__integerType",
                "title": "Height",
                "description": "Fixed height in pixels (0 for auto)",
                "minimum": 0,
                "maximum": 1000,
                "default": 0
            },
            "padding": {
                "type": "integer",
                "lightning:type": "lightning__integerType",
                "title": "Padding",
                "description": "Internal spacing in pixels",
                "minimum": 0,
                "maximum": 100,
                "default": 16
            },
            "margin": {
                "type": "integer",
                "lightning:type": "lightning__integerType",
                "title": "Margin",
                "description": "External spacing in pixels",
                "minimum": 0,
                "maximum": 100,
                "default": 0
            },
            "backgroundColor": {
                "type": "string",
                "lightning:type": "lightning__colorType",
                "title": "Background Color",
                "description": "Component background color",
                "default": "#ffffff"
            }
        },
        "required": ["borderStyle", "layoutWidth"]
    }
    

    #Schema Keywords Reference

    KeywordPurposeExample
    typeJSON Schema data type"string", "integer", "boolean", "object", "array"
    lightning:typeSalesforce UI editor"lightning__colorType"
    titleDisplay label"Border Width"
    descriptionHelp text"Width in pixels"
    defaultDefault value1
    enumAllowed values (picklist)["none", "solid", "dashed"]
    minimumMin numeric value0
    maximumMax numeric value100
    minLengthMin string length1
    maxLengthMax string length255
    patternRegex validation"^https?://"
    requiredRequired properties["prop1", "prop2"]
    formatString format"uri", "email", "date-time"

    #8.5 Design.json Configuration (Optional)

    The design.json file controls UI layout and can override default editors with custom property editors.

    #Layout Types

    TypeDescriptionBest For
    tabsHorizontal tab navigation2-5 property groups
    accordionCollapsible sectionsMany groups, space-constrained
    verticalStacked propertiesSimple linear layout
    (default)No groupingFew properties

    #Tabs Layout Example

    {
        "type": "tabs",
        "properties": {
            "Borders": ["borderStyle", "borderWidth", "borderColor", "borderRadius"],
            "Size": ["layoutWidth", "layoutHeight"],
            "Spacing": ["padding", "margin"],
            "Colors": ["backgroundColor"]
        }
    }
    

    #Accordion Layout Example

    {
        "type": "accordion",
        "properties": {
            "Border Settings": ["borderStyle", "borderWidth", "borderColor", "borderRadius"],
            "Layout Settings": ["layoutWidth", "layoutHeight", "padding", "margin"],
            "Appearance": ["backgroundColor"]
        }
    }
    

    #Vertical Layout Example

    {
        "type": "vertical",
        "properties": {
            "": ["borderStyle", "borderWidth", "borderColor"]
        }
    }
    

    #8.6 Editor Overrides in Design.json

    Override default editors with custom property editor components:

    {
        "type": "tabs",
        "properties": {
            "Alignment": ["textAlignment"],
            "Borders": ["borderStyle", "borderWidth", "borderColor"]
        },
        "propertyEditors": {
            "textAlignment": "c/alignmentPropertyEditor",
            "borderStyle": "c/borderStylePicker"
        }
    }
    

    #Complete Design.json with Overrides

    {
        "type": "tabs",
        "properties": {
            "Layout": ["alignment", "spacing", "width"],
            "Borders": ["borderStyle", "borderWidth", "borderColor", "borderRadius"],
            "Colors": ["backgroundColor", "textColor"]
        },
        "propertyEditors": {
            "alignment": "c/alignmentPropertyEditor",
            "borderStyle": "c/borderStyleVisualPicker",
            "backgroundColor": "c/colorPickerWithPreview",
            "textColor": "c/colorPickerWithPreview"
        }
    }
    

    #8.7 Using Custom Types WITHOUT Editor Overrides

    When you create a custom type with only schema.json (no design.json), Salesforce provides default editors based on lightning:type.

    #Step 1: Create Schema Only

    experiencePropertyTypeBundles/
      cardStyleType/
        schema.json     ← Only this file
    

    schema.json:

    {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
            "shadowDepth": {
                "type": "string",
                "lightning:type": "lightning__stringType",
                "title": "Shadow Depth",
                "enum": ["none", "small", "medium", "large"],
                "default": "small"
            },
            "roundedCorners": {
                "type": "boolean",
                "lightning:type": "lightning__booleanType",
                "title": "Rounded Corners",
                "default": true
            },
            "accentColor": {
                "type": "string",
                "lightning:type": "lightning__colorType",
                "title": "Accent Color",
                "default": "#0070d2"
            }
        }
    }
    

    #Step 2: Reference in Component XML

    <targetConfigs>
        <targetConfig targets="lightningCommunity__Default">
            <property name="cardStyle" 
                      type="cardStyleType" 
                      label="Card Styling"/>
        </targetConfig>
    </targetConfigs>
    

    #Step 3: Access in Component JavaScript

    import { LightningElement, api } from 'lwc';
    
    export default class MyCard extends LightningElement {
        @api cardStyle;  // Receives object with all sub-properties
        
        get shadowClass() {
            const depth = this.cardStyle?.shadowDepth || 'small';
            return `slds-box slds-box_${depth}`;
        }
        
        get hasRoundedCorners() {
            return this.cardStyle?.roundedCorners !== false;
        }
        
        get accentStyle() {
            const color = this.cardStyle?.accentColor || '#0070d2';
            return `border-left: 4px solid ${color}`;
        }
    }
    

    #8.8 Using Custom Types WITH Editor Overrides

    Add design.json to customize layout and/or override specific property editors.

    #Full Example: Theme Configuration Type

    Folder Structure:

    experiencePropertyTypeBundles/
      themeConfigType/
        schema.json
        design.json
    lwc/
      colorSchemeEditor/        ← Custom editor for colorScheme
      fontFamilyPicker/         ← Custom editor for fontFamily
      myThemedComponent/        ← Target component
    

    schema.json:

    {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "title": "Theme Configuration",
        "properties": {
            "colorScheme": {
                "type": "string",
                "lightning:type": "lightning__stringType",
                "title": "Color Scheme",
                "enum": ["light", "dark", "brand", "custom"],
                "default": "light"
            },
            "primaryColor": {
                "type": "string",
                "lightning:type": "lightning__colorType",
                "title": "Primary Color",
                "default": "#0070d2"
            },
            "secondaryColor": {
                "type": "string",
                "lightning:type": "lightning__colorType",
                "title": "Secondary Color",
                "default": "#706e6b"
            },
            "fontFamily": {
                "type": "string",
                "lightning:type": "lightning__stringType",
                "title": "Font Family",
                "enum": ["system", "salesforce-sans", "serif", "monospace"],
                "default": "system"
            },
            "fontSize": {
                "type": "integer",
                "lightning:type": "lightning__integerType",
                "title": "Base Font Size",
                "minimum": 12,
                "maximum": 24,
                "default": 14
            },
            "enableAnimations": {
                "type": "boolean",
                "lightning:type": "lightning__booleanType",
                "title": "Enable Animations",
                "default": true
            }
        },
        "required": ["colorScheme"]
    }
    

    design.json:

    {
        "type": "tabs",
        "properties": {
            "Colors": ["colorScheme", "primaryColor", "secondaryColor"],
            "Typography": ["fontFamily", "fontSize"],
            "Behavior": ["enableAnimations"]
        },
        "propertyEditors": {
            "colorScheme": "c/colorSchemeEditor",
            "fontFamily": "c/fontFamilyPicker"
        }
    }
    

    Custom Editor - colorSchemeEditor.js:

    import { LightningElement, api } from 'lwc';
    
    export default class ColorSchemeEditor extends LightningElement {
        @api label;
        @api description;
        @api required;
        @api value = 'light';
        @api errors;
        @api schema;
        
        schemes = [
            { value: 'light', label: 'Light', icon: 'utility:daylight', class: 'scheme-light' },
            { value: 'dark', label: 'Dark', icon: 'utility:night', class: 'scheme-dark' },
            { value: 'brand', label: 'Brand', icon: 'utility:palette', class: 'scheme-brand' },
            { value: 'custom', label: 'Custom', icon: 'utility:settings', class: 'scheme-custom' }
        ];
        
        get schemesWithSelection() {
            return this.schemes.map(s => ({
                ...s,
                isSelected: s.value === this.value,
                variant: s.value === this.value ? 'brand' : 'neutral'
            }));
        }
        
        handleSelect(event) {
            const newValue = event.currentTarget.dataset.value;
            this.dispatchEvent(new CustomEvent('valuechange', {
                detail: { value: newValue },
                bubbles: true,
                composed: true
            }));
        }
    }
    

    Target Component XML - myThemedComponent.js-meta.xml:

    <?xml version="1.0" encoding="UTF-8"?>
    <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
        <apiVersion>61.0</apiVersion>
        <isExposed>true</isExposed>
        <masterLabel>Themed Component</masterLabel>
        <targets>
            <target>lightningCommunity__Page</target>
            <target>lightningCommunity__Default</target>
        </targets>
        <targetConfigs>
            <targetConfig targets="lightningCommunity__Default">
                <property name="title" type="String" label="Title" default="Welcome"/>
                <property name="themeConfig" type="themeConfigType" label="Theme Settings"/>
            </targetConfig>
        </targetConfigs>
    </LightningComponentBundle>
    

    #8.9 Nested/Complex Types

    Custom types can contain nested objects for complex hierarchies:

    {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
            "header": {
                "type": "object",
                "title": "Header Settings",
                "properties": {
                    "backgroundColor": {
                        "type": "string",
                        "lightning:type": "lightning__colorType",
                        "default": "#ffffff"
                    },
                    "height": {
                        "type": "integer",
                        "lightning:type": "lightning__integerType",
                        "minimum": 40,
                        "maximum": 200,
                        "default": 60
                    }
                }
            },
            "footer": {
                "type": "object",
                "title": "Footer Settings",
                "properties": {
                    "backgroundColor": {
                        "type": "string",
                        "lightning:type": "lightning__colorType",
                        "default": "#f3f3f3"
                    },
                    "showSocialLinks": {
                        "type": "boolean",
                        "lightning:type": "lightning__booleanType",
                        "default": true
                    }
                }
            }
        }
    }
    

    #8.10 Custom Lightning Types Gotchas

    #🚨 TYPE GOTCHA #1: Folder Naming Must Match Type Name

    Problem: The folder name in experiencePropertyTypeBundles/ must exactly match what you reference in XML.

    experiencePropertyTypeBundles/
      layoutStyleType/          ← Folder name
        schema.json
    
    <!-- Must match folder name exactly (case-sensitive) -->
    <property name="layout" type="layoutStyleType" label="Layout"/>
                            ^^^^^^^^^^^^^^^^
    

    #🚨 TYPE GOTCHA #2: Schema Changes Break Existing Configurations

    Problem: Modifying schema.json after components are configured can invalidate existing configurations.

    Safe Changes:

    • Adding new optional properties (with defaults)
    • Expanding enum values
    • Increasing max values
    • Adding description/title

    Breaking Changes:

    • Removing properties
    • Changing property types
    • Reducing enum values
    • Adding required properties
    • Reducing max values

    Workaround: Version your types (e.g., layoutStyleTypeV2)

    #🚨 TYPE GOTCHA #3: Design.json Property Names Must Match Schema

    Problem: Properties referenced in design.json must exist in schema.json.

    // schema.json
    {
        "properties": {
            "borderWidth": { ... }  // ← Defined here
        }
    }
    
    // design.json
    {
        "properties": {
            "Borders": ["borderwidth"]  // ❌ WRONG: case mismatch
            "Borders": ["borderWidth"]  // ✅ CORRECT: exact match
        }
    }
    

    #🚨 TYPE GOTCHA #4: Missing Properties in Design.json Are Hidden

    Problem: If you use design.json, any schema property NOT included in a group is hidden from the UI.

    // schema.json has: borderStyle, borderWidth, borderColor, margin
    
    // design.json
    {
        "type": "tabs",
        "properties": {
            "Borders": ["borderStyle", "borderWidth", "borderColor"]
            // margin is NOT included - it will be HIDDEN!
        }
    }
    

    Solution: Always include ALL properties in design.json groups, or omit design.json entirely.

    #🚨 TYPE GOTCHA #5: Editor Override CPE Must Handle Object Values

    Problem: When overriding an editor within a custom type, your CPE's @api value receives only that sub-property's value, not the entire type object.

    // For a type with borderStyle property:
    export default class BorderStylePicker extends LightningElement {
        @api value;  // Receives just the borderStyle value ("solid"), 
                     // NOT the full type object
    }
    

    #🚨 TYPE GOTCHA #6: Enum Values vs Display Labels

    Problem: Enum values are shown as-is unless you use enumNames (not always supported).

    Workaround: Use a custom property editor for user-friendly display:

    // schema.json
    {
        "borderStyle": {
            "enum": ["none", "solid", "dashed"]  // Technical values
        }
    }
    
    // Use CPE to show friendly labels:
    // "None", "Solid Line", "Dashed Line"
    

    #🚨 TYPE GOTCHA #7: Default Values Must Match Type

    Problem: Default values must be valid for the specified type and constraints.

    // ❌ BAD: default outside min/max range
    {
        "borderWidth": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10,
            "default": 0        // Invalid! Below minimum
        }
    }
    
    // ✅ GOOD: default within range
    {
        "borderWidth": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10,
            "default": 1        // Valid
        }
    }
    

    #🚨 TYPE GOTCHA #8: Type Deployment Order

    Problem: Custom types must be deployed BEFORE components that reference them.

    Solution: Deploy in order:

    1. experiencePropertyTypeBundles/
    2. lwc/ (custom editors)
    3. lwc/ (target components)

    Or use a single deployment with all metadata together.

    #🚨 TYPE GOTCHA #9: No Array Support in UI

    Problem: While JSON Schema supports arrays, Experience Builder doesn't provide array editing UI.

    Workaround: Use JSON string serialization:

    {
        "itemsJson": {
            "type": "string",
            "lightning:type": "lightning__textType",
            "title": "Items (JSON)",
            "description": "JSON array of items"
        }
    }
    

    Then parse in your component or create a custom editor.

    #🚨 TYPE GOTCHA #10: Types Not Available in App Builder

    Problem: ExperiencePropertyTypeBundle works ONLY in Experience Builder, NOT in Lightning App Builder.

    Workaround: For App Builder, use:

    • Multiple individual properties
    • Apex datasource picklists
    • Standard property types only

    #8.11 Custom Type Best Practices

    1. Start with schema.json only - Add design.json only when needed
    2. Use meaningful defaults - Ensure components work without configuration
    3. Group logically - Tabs: 2-5 groups; Accordion: 5+ groups
    4. Document with descriptions - Help admins understand each property
    5. Version your types - buttonStyleTypeV1, buttonStyleTypeV2
    6. Test extensively - Verify all enum values, min/max, required fields
    7. Consider reusability - Design types for use across multiple components
    8. Keep editors optional - Only override when default UI is insufficient

    #9. Flow vs Experience Cloud CPE Differences

    #Complete Comparison Matrix

    FeatureExperience Cloud CPEFlow CPE
    Value Access@api value@api inputVariables
    Value TypeDirect valueArray of {name, value, valueDataType}
    Update Eventvaluechangeconfiguration_editor_input_value_changed
    Delete EventN/Aconfiguration_editor_input_value_deleted
    Type Mapping@api schema@api genericTypeMappings
    Context@api schema@api builderContext
    Element InfoN/A@api elementInfo
    Validation@api errors (from framework)@api validate() (you implement)
    Registrationeditor="c/component" in XMLconfigurationEditor="c-component"
    isExposedfalsefalse

    #Flow CPE Pattern (for reference)

    import { LightningElement, api } from 'lwc';
    
    export default class FlowPropertyEditor extends LightningElement {
        _inputVariables = [];
        
        @api
        get inputVariables() {
            return this._inputVariables;
        }
        set inputVariables(variables) {
            this._inputVariables = variables || [];
        }
        
        @api
        get builderContext() {
            return this._builderContext;
        }
        set builderContext(context) {
            this._builderContext = context;
        }
        
        // Access specific input
        get myPropertyValue() {
            const param = this._inputVariables.find(
                ({ name }) => name === 'myProperty'
            );
            return param?.value;
        }
        
        // Validation method (called by Flow Builder)
        @api
        validate() {
            const errors = [];
            if (!this.myPropertyValue) {
                errors.push({
                    key: 'myProperty',
                    errorString: 'This field is required'
                });
            }
            return errors;
        }
        
        handleChange(event) {
            this.dispatchEvent(new CustomEvent(
                'configuration_editor_input_value_changed',
                {
                    bubbles: true,
                    cancelable: false,
                    composed: true,
                    detail: {
                        name: 'myProperty',
                        newValue: event.target.value,
                        newValueDataType: 'String'
                    }
                }
            ));
        }
    }
    

    #10. Validation Patterns

    #Pattern: Real-Time Validation in Experience Cloud CPE

    Experience Cloud CPEs receive validation errors through @api errors from the framework, but you can implement additional UI validation:

    import { LightningElement, api, track } from 'lwc';
    
    export default class ValidatingEditor extends LightningElement {
        @api label;
        @api description;
        @api required;
        @api value;
        @api errors;  // Framework-provided errors
        @api schema;
        
        @track localErrors = [];
        
        // Combine framework and local errors
        get allErrors() {
            return [...(this.errors || []), ...this.localErrors];
        }
        
        get hasErrors() {
            return this.allErrors.length > 0;
        }
        
        get errorMessage() {
            return this.allErrors.map(e => e.message).join('; ');
        }
        
        validateValue(value) {
            const errors = [];
            
            // Required check
            if (this.required && !value) {
                errors.push({ message: `${this.label} is required` });
            }
            
            // Schema-based validation
            if (this.schema) {
                if (this.schema.minimum != null && value < this.schema.minimum) {
                    errors.push({ message: `Value must be at least ${this.schema.minimum}` });
                }
                if (this.schema.maximum != null && value > this.schema.maximum) {
                    errors.push({ message: `Value must be at most ${this.schema.maximum}` });
                }
                if (this.schema.pattern) {
                    const regex = new RegExp(this.schema.pattern);
                    if (!regex.test(value)) {
                        errors.push({ message: `Value must match pattern: ${this.schema.pattern}` });
                    }
                }
            }
            
            return errors;
        }
        
        handleChange(event) {
            const newValue = event.target.value;
            
            // Validate locally
            this.localErrors = this.validateValue(newValue);
            
            // Still dispatch the value (let framework handle persistence)
            this.dispatchEvent(new CustomEvent('valuechange', {
                detail: { value: newValue },
                bubbles: true,
                composed: true
            }));
        }
    }
    

    #Pattern: Debounced Validation

    import { LightningElement, api, track } from 'lwc';
    
    export default class DebouncedEditor extends LightningElement {
        @api label;
        @api value;
        @api errors;
        
        @track localErrors = [];
        _debounceTimer;
        
        handleInput(event) {
            const newValue = event.target.value;
            
            // Clear previous timer
            clearTimeout(this._debounceTimer);
            
            // Debounce validation (300ms)
            this._debounceTimer = setTimeout(() => {
                this.localErrors = this.validateValue(newValue);
                
                if (this.localErrors.length === 0) {
                    this.dispatchEvent(new CustomEvent('valuechange', {
                        detail: { value: newValue },
                        bubbles: true,
                        composed: true
                    }));
                }
            }, 300);
        }
        
        disconnectedCallback() {
            clearTimeout(this._debounceTimer);
        }
    }
    

    #11. Complex Data Handling

    #ExperiencePropertyTypeBundle Structure

    Create reusable complex property types with dedicated schema and design files:

    force-app/main/default/
      experiencePropertyTypeBundles/
        layoutStyleType/
          schema.json       ← Property definitions
          design.json       ← UI layout (tabs/accordion)
    

    schema.json:

    {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
            "padding": {
                "type": "integer",
                "lightning:type": "lightning__integerType",
                "minimum": 0,
                "maximum": 100,
                "default": 16
            },
            "margin": {
                "type": "integer",
                "lightning:type": "lightning__integerType",
                "minimum": 0,
                "maximum": 100,
                "default": 0
            },
            "borderStyle": {
                "type": "string",
                "lightning:type": "lightning__stringType",
                "enum": ["none", "solid", "dashed", "dotted"],
                "default": "none"
            },
            "borderWidth": {
                "type": "integer",
                "lightning:type": "lightning__integerType",
                "minimum": 0,
                "maximum": 10,
                "default": 1
            },
            "borderColor": {
                "type": "string",
                "lightning:type": "lightning__colorType",
                "default": "#cccccc"
            },
            "borderRadius": {
                "type": "integer",
                "lightning:type": "lightning__integerType",
                "minimum": 0,
                "maximum": 50,
                "default": 0
            }
        }
    }
    

    design.json:

    {
        "type": "tabs",
        "properties": {
            "Spacing": ["padding", "margin"],
            "Borders": ["borderStyle", "borderWidth", "borderColor", "borderRadius"]
        }
    }
    

    #Handling Objects/Arrays Workaround

    Since complex types aren't fully supported, use JSON serialization:

    // CPE handling complex nested data
    export default class ComplexDataEditor extends LightningElement {
        @api label;
        @api value;
        
        // Parse incoming JSON string
        get parsedConfig() {
            try {
                return this.value ? JSON.parse(this.value) : {
                    items: [],
                    settings: {}
                };
            } catch (e) {
                console.error('Parse error:', e);
                return { items: [], settings: {} };
            }
        }
        
        get items() {
            return this.parsedConfig.items || [];
        }
        
        get settings() {
            return this.parsedConfig.settings || {};
        }
        
        handleAddItem(event) {
            const newItem = {
                id: Date.now(),
                label: event.detail.label,
                value: event.detail.value
            };
            
            const updated = {
                ...this.parsedConfig,
                items: [...this.items, newItem]
            };
            
            this.dispatchValueChange(updated);
        }
        
        handleRemoveItem(event) {
            const idToRemove = event.target.dataset.id;
            
            const updated = {
                ...this.parsedConfig,
                items: this.items.filter(item => item.id !== parseInt(idToRemove))
            };
            
            this.dispatchValueChange(updated);
        }
        
        handleSettingChange(event) {
            const settingName = event.target.dataset.setting;
            const settingValue = event.target.value;
            
            const updated = {
                ...this.parsedConfig,
                settings: {
                    ...this.settings,
                    [settingName]: settingValue
                }
            };
            
            this.dispatchValueChange(updated);
        }
        
        dispatchValueChange(data) {
            this.dispatchEvent(new CustomEvent('valuechange', {
                detail: { value: JSON.stringify(data) },
                bubbles: true,
                composed: true
            }));
        }
    }
    

    #12. Reusability Strategies

    #Strategy 1: Parameterized via Schema

    Create one CPE that adapts based on schema:

    // universalPickerEditor.js
    export default class UniversalPickerEditor extends LightningElement {
        @api label;
        @api value;
        @api schema;
        
        get pickerType() {
            // Determine UI type from schema
            if (this.schema?.format === 'color') return 'color';
            if (this.schema?.format === 'date') return 'date';
            if (this.schema?.enum) return 'combobox';
            if (this.schema?.type === 'boolean') return 'checkbox';
            return 'text';
        }
        
        get options() {
            if (!this.schema?.enum) return [];
            return this.schema.enum.map(val => ({
                label: this.schema.enumLabels?.[val] || this.humanize(val),
                value: val
            }));
        }
        
        humanize(str) {
            return str.replace(/([A-Z])/g, ' $1')
                      .replace(/[_-]/g, ' ')
                      .trim()
                      .replace(/^\w/, c => c.toUpperCase());
        }
    }
    

    #Strategy 2: ExperiencePropertyTypeBundle Reuse

    Define type once, use in multiple components:

    <!-- Component A -->
    <property name="styling" type="layoutStyleType" label="Layout"/>
    
    <!-- Component B (reuses same type) -->
    <property name="containerStyle" type="layoutStyleType" label="Container Layout"/>
    

    #Strategy 3: Base CPE Class Pattern

    Create a base class with common functionality:

    // basePropertyEditor.js
    import { LightningElement, api } from 'lwc';
    
    export default class BasePropertyEditor extends LightningElement {
        @api label;
        @api description;
        @api required;
        @api value;
        @api errors;
        @api schema;
        
        get hasErrors() {
            return this.errors?.length > 0;
        }
        
        get errorMessage() {
            return this.hasErrors ? this.errors[0].message : '';
        }
        
        get isRequired() {
            return this.required === true || this.required === 'true';
        }
        
        dispatchValue(newValue) {
            this.dispatchEvent(new CustomEvent('valuechange', {
                detail: { value: newValue },
                bubbles: true,
                composed: true
            }));
        }
    }
    
    // colorPickerEditor.js - extends base
    import BasePropertyEditor from 'c/basePropertyEditor';
    
    export default class ColorPickerEditor extends BasePropertyEditor {
        handleColorChange(event) {
            this.dispatchValue(event.target.value);
        }
    }
    

    Note: LWC inheritance has limitations. Consider composition over inheritance.


    #13. Testing & Debugging

    #Local Development Testing

    1. Deploy CPE and target component to scratch org
    2. Create Experience Cloud site (or use existing)
    3. Add target component to page in Experience Builder
    4. Test CPE in property panel

    #Debug Logging Pattern

    const DEBUG = true;
    
    export default class DebugPropertyEditor extends LightningElement {
        @api 
        get value() {
            return this._value;
        }
        set value(val) {
            if (DEBUG) console.log('[CPE] Received value:', val);
            this._value = val;
        }
        _value;
        
        @api
        get errors() {
            return this._errors;
        }
        set errors(val) {
            if (DEBUG) console.log('[CPE] Received errors:', val);
            this._errors = val;
        }
        _errors;
        
        @api
        get schema() {
            return this._schema;
        }
        set schema(val) {
            if (DEBUG) console.log('[CPE] Received schema:', JSON.stringify(val, null, 2));
            this._schema = val;
        }
        _schema;
        
        handleChange(event) {
            const newValue = event.target.value;
            if (DEBUG) console.log('[CPE] Dispatching valuechange:', newValue);
            
            this.dispatchEvent(new CustomEvent('valuechange', {
                detail: { value: newValue },
                bubbles: true,
                composed: true
            }));
        }
    }
    

    #Common Issues Checklist

    SymptomCheck
    CPE not appearingeditor attribute in XML correct?
    Values not persistingbubbles: true, composed: true?
    Errors not showingCheck @api errors property exists
    Schema emptyVerify ExperiencePropertyTypeBundle deployed
    CPE shows "undefined"Check @api value default

    #14. Reference Implementation

    #Complete Alignment Picker CPE

    alignmentPropertyEditor.html:

    <template>
        <div class="slds-form-element" data-testid="alignment-editor">
            <label class="slds-form-element__label">
                <template if:true={isRequired}>
                    <abbr class="slds-required" title="required">*</abbr>
                </template>
                {label}
            </label>
            
            <div class="slds-form-element__control">
                <lightning-button-group>
                    <lightning-button-icon
                        icon-name="utility:left_align_text"
                        data-value="left"
                        variant={leftVariant}
                        onclick={handleSelect}
                        alternative-text="Left align"
                        class="alignment-button">
                    </lightning-button-icon>
                    <lightning-button-icon
                        icon-name="utility:center_align_text"
                        data-value="center"
                        variant={centerVariant}
                        onclick={handleSelect}
                        alternative-text="Center align"
                        class="alignment-button">
                    </lightning-button-icon>
                    <lightning-button-icon
                        icon-name="utility:right_align_text"
                        data-value="right"
                        variant={rightVariant}
                        onclick={handleSelect}
                        alternative-text="Right align"
                        class="alignment-button">
                    </lightning-button-icon>
                    <lightning-button-icon
                        icon-name="utility:justify_text"
                        data-value="justify"
                        variant={justifyVariant}
                        onclick={handleSelect}
                        alternative-text="Justify"
                        class="alignment-button">
                    </lightning-button-icon>
                </lightning-button-group>
            </div>
            
            <template if:true={hasErrors}>
                <div class="slds-form-element__help slds-text-color_error" role="alert">
                    {errorMessage}
                </div>
            </template>
            
            <template if:true={description}>
                <div class="slds-form-element__help">{description}</div>
            </template>
        </div>
    </template>
    

    alignmentPropertyEditor.js:

    import { LightningElement, api } from 'lwc';
    
    export default class AlignmentPropertyEditor extends LightningElement {
        // ══════════════════════════════════════════════════════════════
        // Property Editor Contract - All six @api properties required
        // ══════════════════════════════════════════════════════════════
        @api label = 'Alignment';
        @api description;
        @api required = false;
        @api value = 'left';
        @api errors;
        @api schema;
    
        // ══════════════════════════════════════════════════════════════
        // Computed Properties
        // ══════════════════════════════════════════════════════════════
        get isRequired() {
            return this.required === true || this.required === 'true';
        }
    
        get hasErrors() {
            return Array.isArray(this.errors) && this.errors.length > 0;
        }
    
        get errorMessage() {
            return this.hasErrors ? this.errors.map(e => e.message).join(', ') : '';
        }
    
        get leftVariant() {
            return this.value === 'left' ? 'brand' : 'border';
        }
    
        get centerVariant() {
            return this.value === 'center' ? 'brand' : 'border';
        }
    
        get rightVariant() {
            return this.value === 'right' ? 'brand' : 'border';
        }
    
        get justifyVariant() {
            return this.value === 'justify' ? 'brand' : 'border';
        }
    
        // ══════════════════════════════════════════════════════════════
        // Event Handlers
        // ══════════════════════════════════════════════════════════════
        handleSelect(event) {
            const newValue = event.currentTarget.dataset.value;
            
            // Don't dispatch if value unchanged
            if (newValue === this.value) {
                return;
            }
    
            // CRITICAL: Both bubbles and composed must be true
            this.dispatchEvent(new CustomEvent('valuechange', {
                detail: { value: newValue },
                bubbles: true,
                composed: true
            }));
        }
    }
    

    alignmentPropertyEditor.css:

    .alignment-button {
        margin: 0 2px;
    }
    
    .slds-form-element__help {
        margin-top: 0.25rem;
        font-size: 0.75rem;
    }
    

    alignmentPropertyEditor.js-meta.xml:

    <?xml version="1.0" encoding="UTF-8"?>
    <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
        <apiVersion>61.0</apiVersion>
        <isExposed>false</isExposed>
        <description>Custom property editor for text alignment selection</description>
    </LightningComponentBundle>
    

    #Quick Reference Card

    #Experience Cloud CPE Checklist

    □ Six @api properties: label, description, required, value, errors, schema
    □ isExposed="false" in js-meta.xml
    □ editor="c/componentName" in target component XML
    □ valuechange event with bubbles:true AND composed:true
    □ Handle null/undefined value with defaults
    □ Copy data before passing in event detail (avoid reactive data)
    □ Test in Experience Builder property panel
    □ Verify values persist after page refresh
    

    #Event Template

    this.dispatchEvent(new CustomEvent('valuechange', {
        detail: { value: /* your value */ },
        bubbles: true,
        composed: true
    }));
    

    #Resources

    • Salesforce Developer Blog: Custom Property Editors
    • LWC Developer Guide: Experience Builder
    • Official GitHub Samples
    • XML Configuration Reference

    Document Version History:

    • 1.0 (December 2025): Initial comprehensive specification

    On this page

    • Experience Cloud LWC Custom Property Editors
    • Complete Technical Specification: Rules, Design Patterns & Gotchas
    • Table of Contents
    • 1. Overview
    • When to Use CPEs
    • Key Architecture Concepts
    • 2. CPE Declaration & Registration (Critical Differences)
    • 2.1 Experience Cloud vs Flow/Apex: Side-by-Side Comparison
    • 2.2 Experience Cloud CPE: Correct Declaration
    • 2.3 Flow/Apex CPE: Correct Declaration (For Comparison)
    • 2.4 Declaration Comparison Table
    • 2.5 Common Declaration Mistakes
    • 2.6 Quick Reference: Which Pattern to Use?
    • 3. The Property Editor Contract (Mandatory Rules)
    • ⚠️ RULE: Required @api Properties
    • ⚠️ RULE: The valuechange Event Contract
    • ⚠️ RULE: CPE XML Configuration
    • 4. XML Configuration Rules
    • Target Component XML Structure
    • ⚠️ RULE: XML Modification Restrictions (Critical for Packages)
    • 5. Event Patterns & Communication
    • Understanding bubbles and composed
    • ⚠️ RULE: Event Data Handling
    • 6. Design Patterns
    • Pattern 1: Visual Selection Editor
    • Pattern 2: Schema-Driven Editor
    • Pattern 3: Accordion/Grouped Properties
    • 7. Critical Gotchas & Pitfalls
    • 🚨 GOTCHA : Component Isolation
    • 🚨 GOTCHA : Flow CPE vs Experience Cloud CPE
    • 🚨 GOTCHA : Kebab-Case vs CamelCase
    • 🚨 GOTCHA : Array/List Property Limitations
    • 🚨 GOTCHA : Missing bubbles or composed
    • 🚨 GOTCHA : LWR Site Publishing Model
    • 🚨 GOTCHA : Single Editor Limitation
    • 🚨 GOTCHA : Conditional Properties Not Supported
    • 🚨 GOTCHA : Dynamic Picklists in Experience Cloud
    • 🚨 GOTCHA : Expression Bindings
    • 8. Custom Lightning Types (ExperiencePropertyTypeBundle)
    • 8.1 Architecture Overview
    • 8.2 When to Use Custom Lightning Types
    • 8.3 Available Built-in Lightning Types
    • 8.4 Schema.json Configuration (Required)
    • 8.5 Design.json Configuration (Optional)
    • 8.6 Editor Overrides in Design.json
    • 8.7 Using Custom Types WITHOUT Editor Overrides
    • 8.8 Using Custom Types WITH Editor Overrides
    • 8.9 Nested/Complex Types
    • 8.10 Custom Lightning Types Gotchas
    • 8.11 Custom Type Best Practices
    • 9. Flow vs Experience Cloud CPE Differences
    • Complete Comparison Matrix
    • Flow CPE Pattern (for reference)
    • 10. Validation Patterns
    • Pattern: Real-Time Validation in Experience Cloud CPE
    • Pattern: Debounced Validation
    • 11. Complex Data Handling
    • ExperiencePropertyTypeBundle Structure
    • Handling Objects/Arrays Workaround
    • 12. Reusability Strategies
    • Strategy 1: Parameterized via Schema
    • Strategy 2: ExperiencePropertyTypeBundle Reuse
    • Strategy 3: Base CPE Class Pattern
    • 13. Testing & Debugging
    • Local Development Testing
    • Debug Logging Pattern
    • Common Issues Checklist
    • 14. Reference Implementation
    • Complete Alignment Picker CPE
    • Quick Reference Card
    • Experience Cloud CPE Checklist
    • Event Template
    • Resources