Subchapter 2.58
references/editor-react-component/COMPONENT-API.mdMarkdown16 KBView on GitHub
Rules and patterns for props structure, elementProps, data types, file structure, containers, data-driven components, and array props.
Is this element a named part (carries a global class string)?
│
├─ YES
│ └─ ✅ It gets an elementProps entry — spread it + merge its className.
│ Add config fields inside the entry only as needed
│ (data, direction, event handlers).
│
└─ NO → purely visual/decorative non-part
(icons, separators, decorations, layout wrappers,
elements only hidden/shown via CSS)
└─ ❌ CSS class only — no elementProps entryinterface ComponentProps {
// Identity
id?: string;
className?: string;
// Mandatory features (from @wix/editor-react-types)
direction?: Direction;
a11y?: A11y; // ALL ARIA attributes come through this (ariaLabel, role, etc.)
// Component-specific props (NO children unless container-type - see container rules below)
label?: string;
items?: Array<ItemType>;
// ... from specification
// NEVER add: ariaLabel, ariaDescribedBy, role, etc. - use a11y prop instead
// Sub-component configuration (only if needed - see elementProps rules)
elementProps?: {
[partName]?: {
[configProp]?: string | boolean | number; // Only include if element needs config beyond className
direction?: Direction;
};
};
}elementProps is a map keyed by the manifest’s inner-element keys; each entry
carries that element’s className (the editor sets it — this is how design
states reach the element) plus any data.
Every named inner element — every element that carries a global class
string — gets an elementProps entry, and you spread that entry onto the
element. This is mandatory, even when the element needs no other config:
without it the editor cannot style that element or drive its states.
How className gets merged depends on what the element is:
<button>, <li>, <div>): merge
elementProps?.<key>.className inline, alongside the global string and module
class — there’s no component to do it.<Inner/>): the spread alone is
enough. className rides along inside it, and the sub-component merges it onto
its own root (every skill component merges its incoming className). Don’t
merge it again at the call site.interface PlanCardProps {
className?: string;
elementProps?: {
cta?: { className?: string; href?: string };
};
}
// Raw HTML element — spread + merge className inline.
// Note: the elementProps key is the short part name (`cta`), while the global
// class is prefixed with the component name (`plan-card-cta`). Keep them as-is.
<button {...elementProps?.cta} className={classNames('plan-card-cta', styles.cta, elementProps?.cta?.className)}>
{label}
</button>
// Sub-component built with this skill — spread is enough (it merges className itself):
<PlanRow {...elementProps?.planRow} />Non-parts (module-class-only elements: icons, decorations, separators, layout wrappers — no global class) get no entry. Style them with CSS.
Inside each entry, add only the config the element needs (data, direction,
event handlers). Never add a bare className? as the only field to decide
whether an element qualifies — className always flows through; the entry
exists because the element is a named part.
See PARTS.md for root election (Step 0), the mandatory filter, and full rules. The root gets no elementProps entry — its className, a11y, and direction arrive as top-level props.
If a displayed value can be computed with a simple pure function from
other props and/or internal state, compute it internally — don’t
expose it as a prop. Expose only the source inputs (use numeric types
when arithmetic is needed: price: number, not price: string).
Example: subtotal = price × quantity → computed inside the component, not a prop.
children proplabel, text, placeholder, etc.imageSrc, videoUrl, iconName, etc.link, href, url, etc.items, options, menuItems, etc.Exception — Container-type components: Components whose purpose is to wrap arbitrary child elements (e.g., BoxContainer) MAY accept children: React.ReactNode. This applies only to structural containers — data-driven leaf components (Button, Tabs, Accordion, etc.) must NOT use children.
When the parent component defines an array prop (e.g., items), child/item components receive a single item directly. They do NOT redeclare the data structure in their own props.
Array elements MUST be objects with named keys. This enables stable item identity (each item can carry its own id/key), non-breaking extension (new fields can be added later without changing the prop signature), and semantic naming (each value has meaning instead of being an opaque scalar).
Allowed forms:
Array<{ key: ValueType, ... }>Array<AccordionItem> is OK because AccordionItem is { name, content })Never allowed as the array element:
Array<string>, Array<number>, Array<boolean>@wix/editor-react-types: Array<Image>, Array<Link>, Array<Video>, Array<Audio>, Array<VectorArt>, Array<RichText>. Wrap them in an object instead.Array<Array<{cover: image, caption: string}>> Array<{items: Array<image>, caption: string}>❌ Wrong:
tags: Array<string>;
prices: Array<number>;
flags: Array<boolean>;
images: Array<Image>;
links: Array<Link>;
nestedArrays: Array<Array<any>>✅ Correct:
tags: Array<{ label: string }>;
prices: Array<{ amount: number }>;
flags: Array<{ enabled: boolean }>;
gallery: Array<{ image: Image, caption?: string }>;
links: Array<{ link: Link, label: string }>;
items: Array<AccordionItem>; // AccordionItem is { name, content }When a component specification indicates a “container” or “slot” area where users can add nested content, use React.ReactNode for that content prop.
When to use React.ReactNode for a content prop:
RTL Support: Elements that render React.ReactNode content MUST have dir="ltr" to prevent RTL inheritance from the parent component. See DIRECTIONALITY.md.
When creating TypeScript interfaces for component props, use types from @wix/editor-react-types for more complex types, to see the allowed type from this library, look at the following file node_modules/@wix/react-component-schema/dist/editor-react-types.d.ts:
import type { Link } from "@wix/editor-react-types"; // Reference at node_modules/@wix/react-component-schema/dist/editor-react-types.d.tsAll resources rendered or fetched by the component (images, icons, fonts,
videos, audio, JSON data, etc.) MUST come from Wix services. Never reference
or call external (non-Wix) hosts — no unsplash.com, placehold.co,
picsum.photos, third-party CDN icon sets, or any custom backend the user
hasn’t asked for. This rule covers both src/href attributes and any
runtime fetching.
Allowed image hosts: static.wixstatic.com (and other *.wixstatic.com
subdomains). Allowed data: values supplied through props (populated by the
editor) or imported local assets bundled with the component.
Image defaults belong in the <componentName>.props.ts file’s exported
defaultProps constant (consumed by withDefaults(Component, defaultProps) in
component.tsx). Use a Wix-hosted image from the Free-from-Wix public
media catalog and populate only uri, url, and alt — leave
width, height, focalPoint, etc. unset so the editor fills them when
the user picks a real image.
By default, use the canonical fallback URL from the examples below for
every Image default (url and uri derived from the same fileName).
Only when the user explicitly asks for different/better default images,
fetch candidates from the Wix Free-from-Wix catalog:
GET https://publicmedia.wix.com/public/light_items?guid=bca5cb9f-45d2-4b11-8d6c-c9a7e7bd2873%3Aglobal%3Awix&pageSize=20&pageNumber=1&language=en&tags=free&mediaType=pictureThe endpoint is unauthenticated. Pick an item whose displayTags /
title fit the component’s purpose, then build the Image default from
its fileName:
url → `https://static.wixstatic.com/media/${fileName}`uri → fileNamealt → a short human description (use the item’s title or
displayTags)Do not call this endpoint when the user hasn’t asked for it — the canonical fallback URL is fine for unattended scaffolds.
Default image pool — use a different image for each slot:
| # | fileName | Description |
|---|---|---|
| 1 | 11062b_2f97b87dcea2446fa48e9ad9c5457ae1~mv2.jpg | Tropical beach aerial |
| 2 | 11062b_73f31c7e7d3544c69dc8ecd8d34c5717~mv2.jpg | Dead Sea landscape |
| 3 | 11062b_3682ebfcb08e4da5b3168b62819a1e68~mv2.jpg | Palm tree sunset |
| 4 | 11062b_45e67783d39c4963ab9e4fc418173233~mv2.jpg | Abstract pink waves |
| 5 | 11062b_4c11f014b0d04948b2e6f554076bc40a~mv2.jpg | Coastal village aerial |
When a component needs more than one image default, cycle through the pool above so every slot gets a visually distinct image. For a single image, use image #1.
Single Image prop:
export const defaultProps = {
image: {
url: "https://static.wixstatic.com/media/11062b_2f97b87dcea2446fa48e9ad9c5457ae1~mv2.jpg",
uri: "11062b_2f97b87dcea2446fa48e9ad9c5457ae1~mv2.jpg",
alt: "Default image",
},
} as const satisfies Omit<ExampleComponentProps, "id" | "className">;Array of objects with an Image field:
export const defaultProps = {
cards: [
{
title: "First card",
image: {
url: "https://static.wixstatic.com/media/11062b_2f97b87dcea2446fa48e9ad9c5457ae1~mv2.jpg",
uri: "11062b_2f97b87dcea2446fa48e9ad9c5457ae1~mv2.jpg",
alt: "First card image",
},
},
{
title: "Second card",
image: {
url: "https://static.wixstatic.com/media/11062b_73f31c7e7d3544c69dc8ecd8d34c5717~mv2.jpg",
uri: "11062b_73f31c7e7d3544c69dc8ecd8d34c5717~mv2.jpg",
alt: "Second card image",
},
},
],
} as const satisfies Omit<ExampleComponentProps, "id" | "className">;Inline fallback for HTML src / href: when rendering a raw HTML
attribute that takes a URL (<img src>, <source src>, <video poster>,
<a href> for an image link, etc.) and the value could be empty, fall
back to the same Wix-hosted URL — never to an external host or a relative
path that doesn’t exist:
<img
src={
image?.url ||
"https://static.wixstatic.com/media/11062b_2f97b87dcea2446fa48e9ad9c5457ae1~mv2.jpg"
}
alt={image?.alt || "Default image"}
/>Split logical UI pieces into separate named components. Never write inline JSX blocks for distinct complex UI pieces.
File structure for complex components (many subcomponents):
ComponentName/
├── components/ # Subcomponents folder
│ ├── SubComponent1/
│ │ ├── SubComponent1.tsx
│ │ ├── SubComponent1.module.scss
│ │ └── index.ts
│ └── SubComponent2/
│ ├── SubComponent2.tsx
│ └── index.ts
├── hooks/ # Custom hooks folder
│ ├── index.ts
│ └── useCustomHook.tsWhen to extract a sub-component:
❌ Wrong:
export interface ButtonProps {
children: React.ReactNode; // ❌
}
<Button>Click Me</Button>✅ Correct:
export interface ButtonProps {
label?: string;
icon?: string;
}
<Button label={label} icon={icon} />Component controls rendering internally:
const List = ({ items }) => (
<div>
{items.map(item => (
<ListItem key={item.id}>{item.label}</ListItem>
))}
</div>
);Parent defines array structure, child receives single item:
// Parent
interface ParentProps {
items: Array<AccordionItem>; // Array defined here
}
interface AccordionItem {
name: string;
content: React.ReactNode;
}
// Child receives single item
interface ChildProps {
item: AccordionItem & { id: string }; // Single item
isOpen?: boolean;
}
// Parent maps
items.map((item, index) => (
<Child key={index} item={{ ...item, id: index.toString() }} />
))For “container” or “slot” areas, use React.ReactNode:
interface AccordionItem {
name: string;
content: React.ReactNode; // Users add any content here
}
// Render
{items.map((item, index) => (
<div key={index} dir="ltr">
{typeof item.content === 'function' ? item.content({}) : item.content}
</div>
))}❌ Wrong:
export interface CardProps {
children: React.ReactNode; // ❌ children + React.ReactNode both forbidden
}
<Card><CardHeader>Title</CardHeader></Card>✅ Correct:
export interface CardProps {
title: string;
content: React.ReactNode;
}
<Card title="Title" content={<div>Content</div>} />❌ Wrong:
elementProps?: {
icon?: { className?: string; } // ❌ Only className
separator?: { className?: string; } // ❌ Only className
}✅ Correct:
// No elementProps for purely visual elements (including conditionally displayed)
// Style via CSS classes directlyWhy: Don’t declare a className?: field — className already flows through each named inner element’s elementProps entry (see “Propagate elementProps to inner elements”). Conditional display is controlled via CSS, not React props.
Use Array<T> over T[].
❌ Wrong:
interface ComponentProps {
items: Item[]; // ❌ Wrong syntax
tags: string[]; // ❌ Wrong syntax
tags: Array<string>; // ❌ Element must be an object with named keys
}✅ Correct:
interface ComponentProps {
items: Array<Item>; // ✅ Correct syntax
tags: Array<{ label: string }>; // ✅ Element is an object with named keys
}See also: Array Element Types: Always Objects with Named Keys — items must be objects with named keys.