index.js is the block editor UI. It loads only inside the Gutenberg editor, never on the frontend. It must be a self-contained IIFE — no import, no export, no JSX.
IIFE Structure — Mandatory Pattern
( function () {
'use strict';
var registerBlockType = wp.blocks.registerBlockType;
var el = wp.element.createElement;
var Fragment = wp.element.Fragment;
var InspectorControls = wp.blockEditor.InspectorControls;
var PanelBody = wp.components.PanelBody;
var SelectControl = wp.components.SelectControl;
var TextControl = wp.components.TextControl;
var ToggleControl = wp.components.ToggleControl;
var useBlockProps = wp.blockEditor.useBlockProps;
// ── Panel Registry options ────────────────────────────────────────────
// Read live option lists from the Panel Registry. These update automatically
// when the registry is modified — no block edits required.
var REGISTRY = window.wptPanelRegistry || {};
function registryOptions( panel, field ) {
var fields = REGISTRY[panel] && REGISTRY[panel].fields ? REGISTRY[panel].fields : [];
for ( var i = 0; i < fields.length; i++ ) {
if ( fields[i].key === field ) {
return ( fields[i].options || [] ).map( function( v ) {
return { label: v, value: v };
});
}
}
return [];
}
var HEADING_OPTIONS = registryOptions( 'structure', 'headingLevel' );
var ROLE_OPTIONS = registryOptions( 'structure', 'semanticRole' );
var PADDING_OPTIONS = registryOptions( 'spacing', 'blockPadding' );
var ALIGN_OPTIONS = registryOptions( 'layout', 'textAlign' );
// ── Element Registry class builders ──────────────────────────────────
// Always guard with window.wptElements before calling.
// Returns '' as a safe fallback if the registry has not yet loaded.
function headingClasses( attr ) {
return window.wptElements
? window.wptElements.buildElementClasses( 'heading', attr.headingLevel || 'h2', attr.elementOverrides )
: '';
}
function descClasses( attr ) {
return window.wptElements
? window.wptElements.buildElementClasses( 'description', null, attr.elementOverrides )
: '';
}
// ── Root class builder ────────────────────────────────────────────────
function rootClass( attr ) {
var c = [ 'wpt-feature-card' ];
c.push( 'wpt-feature-card--theme-' + ( attr.themeMode || 'light' ) );
c.push( 'wpt-feature-card--bg-' + ( attr.backgroundColor || 'light' ) );
if ( attr.hideOnMobile ) { c.push( 'wpt-feature-card--hide-mobile' ); }
if ( attr.hideOnTablet ) { c.push( 'wpt-feature-card--hide-tablet' ); }
if ( attr.hideOnDesktop ) { c.push( 'wpt-feature-card--hide-desktop' ); }
return c.join( ' ' );
}
// ── Root inline style — spacing via CSS custom properties ─────────────
// Spacing values travel as CSS custom properties, not modifier classes.
// render.php mirrors this exact same map.
function rootStyle( attr ) {
var map = {
'none': '0',
'xs': 'var(--wpt-spacing-xs)',
'sm': 'var(--wpt-spacing-sm)',
'md': 'var(--wpt-spacing-md)',
'lg': 'var(--wpt-spacing-lg)',
'xl': 'var(--wpt-spacing-xl)',
'2xl': 'var(--wpt-spacing-2xl)',
'3xl': 'var(--wpt-spacing-3xl)',
'4xl': 'var(--wpt-spacing-4xl)',
};
var style = {};
if ( map[ attr.blockPadding ] ) { style['--wpt-feature-card-padding'] = map[ attr.blockPadding ]; }
if ( map[ attr.blockGap ] ) { style['--wpt-feature-card-gap'] = map[ attr.blockGap ]; }
if ( attr.spacingBottom && attr.spacingBottom !== 'none' && map[ attr.spacingBottom ] ) {
style['margin-block-end'] = map[ attr.spacingBottom ];
}
return style;
}
// ── Block registration ────────────────────────────────────────────────
registerBlockType( 'wptruss/feature-card', {
edit: function ( props ) {
var attr = props.attributes;
var setAttr = props.setAttributes;
var bp = useBlockProps({ className: rootClass( attr ), style: rootStyle( attr ) });
return el( Fragment, null,
el( InspectorControls, null,
// Panel 1 — Structure (always first, always open)
el( PanelBody, { title: '🏗️ Structure', initialOpen: true },
el( SelectControl, {
label: 'Heading Level', value: attr.headingLevel || 'h2',
options: HEADING_OPTIONS,
onChange: function( v ) { setAttr({ headingLevel: v }); }
}),
el( SelectControl, {
label: 'Semantic Wrapper', value: attr.semanticRole || 'section',
options: ROLE_OPTIONS,
onChange: function( v ) { setAttr({ semanticRole: v }); }
})
),
// Panel 2 — Design (always second)
el( PanelBody, { title: '🎨 Design', initialOpen: false },
el( SelectControl, {
label: 'Theme Mode', value: attr.themeMode || 'light',
options: [
{ label: 'Light', value: 'light' },
{ label: 'Dark', value: 'dark' },
],
onChange: function( v ) { setAttr({ themeMode: v }); }
}),
el( SelectControl, {
label: 'Background', value: attr.backgroundColor || 'light',
options: [
{ label: 'Light', value: 'light' },
{ label: 'Surface', value: 'surface' },
{ label: 'Primary', value: 'primary' },
{ label: 'Secondary', value: 'secondary' },
{ label: 'Dark', value: 'dark' },
],
onChange: function( v ) { setAttr({ backgroundColor: v }); }
})
),
// Panel 3 — Content (block-specific fields)
el( PanelBody, { title: '📝 Content', initialOpen: false },
el( TextControl, {
label: 'Heading', value: attr.heading || '',
onChange: function( v ) { setAttr({ heading: v }); }
}),
el( TextControl, {
label: 'Description', value: attr.description || '',
onChange: function( v ) { setAttr({ description: v }); }
})
),
// Panel 4 — Spacing
el( PanelBody, { title: '↕ Spacing', initialOpen: false },
el( SelectControl, {
label: 'Block Padding', value: attr.blockPadding || '3xl',
options: PADDING_OPTIONS,
onChange: function( v ) { setAttr({ blockPadding: v }); }
})
),
// Panel 5 — Layout (always last)
el( PanelBody, { title: '📱 Layout', initialOpen: false },
el( SelectControl, {
label: 'Text Align', value: attr.textAlign || 'left',
options: ALIGN_OPTIONS,
onChange: function( v ) { setAttr({ textAlign: v }); }
}),
el( ToggleControl, {
label: 'Hide on Mobile', checked: attr.hideOnMobile || false,
onChange: function( v ) { setAttr({ hideOnMobile: v }); }
}),
el( ToggleControl, {
label: 'Hide on Tablet', checked: attr.hideOnTablet || false,
onChange: function( v ) { setAttr({ hideOnTablet: v }); }
}),
el( ToggleControl, {
label: 'Hide on Desktop', checked: attr.hideOnDesktop || false,
onChange: function( v ) { setAttr({ hideOnDesktop: v }); }
})
)
),
// ── Editor canvas ─────────────────────────────────────────────
el( 'article', bp,
el( attr.headingLevel || 'h3', {
className: 'wpt-feature-card__heading ' + headingClasses( attr )
}, attr.heading || 'Feature heading' ),
el( 'p', {
className: 'wpt-feature-card__description ' + descClasses( attr )
}, attr.description || 'Feature description text.' )
)
);
},
save: function() {
return null; // All blocks use render.php. save() always returns null.
}
});
}() );
Element Registry API
The Element Registry exposes a single method on window.wptElements:
window.wptElements.buildElementClasses( elementType, variant, overrides )
| Argument | Type | Description | |
|---|---|---|---|
elementType |
string | Element handle: 'heading', 'description', 'button', 'badge', 'image', 'eyebrow', 'icon' |
|
variant |
string | null | Variant key for that element (e.g. 'h2' for heading). Pass null if the element has no variants. |
overrides |
object | The block’s elementOverrides attribute. Pass directly from attr.elementOverrides. |
Always guard the call. The element registry loads asynchronously. An unguarded call will throw if index.js evaluates before the registry is ready:
// ✅ Correct — guarded, returns '' as safe fallback
function headingClasses( attr ) {
return window.wptElements
? window.wptElements.buildElementClasses( 'heading', attr.headingLevel || 'h2', attr.elementOverrides )
: '';
}
// ❌ Wrong — throws if registry loads after index.js
function headingClasses( attr ) {
return window.wptElements.buildElementClasses( 'heading', attr.headingLevel, attr.elementOverrides );
}
Five-Panel Inspector Order
Always follow this order. The validator checks panel title strings and flags deviations:
| Position | Title | initialOpen |
Purpose |
|---|---|---|---|
| 1 | 🏗️ Structure | true |
Heading level, semantic role |
| 2 | 🎨 Design | false |
Theme mode, background colour |
| 3 | 📝 Content | false |
Block-specific editable fields |
| 4 | ↕ Spacing | false |
Padding, gap, spacing bottom |
| 5 | 📱 Layout | false |
Text alignment, responsive visibility |
Mandatory index.js Rules
- All code inside a single IIFE:
( function () { 'use strict'; ... }() ); - No
import, noexport, noexport default - No JSX — use
wp.element.createElement(aliased toel) - No
fetch(),XMLHttpRequest, oreval() - No
.innerHTMLor.outerHTMLDOM assignments save()always returnsnull— server render only- All conditional renders use ternary (
condition ? el(...) : null) — nevercondition && el(...) - All array attributes guarded with
Array.isArray()before.map() - Never pass dynamic strings into
()— build the string outside, pass a literal string to()