Component shape checklist
Check a component against these rules before you register it, or when you review one a tool generated. Each table lists a rule, what breaks without it, and the page that explains it.
The example is BlogHero in src/components/BlogHero.tsx, with the props title, summary and image, in the group Blog · Components. On the Blog post template it shows the entry AI 101 at /blog/ai-101.
Register atomic and layout components, and keep compounds rare
Check this before the rest. The other rules are about whether a component works. This one is about whether to register it at all. It's the most expensive one to fix later, because templates that already use the component have to change.
| Rule | What breaks without it | Where it's explained |
|---|---|---|
Register atomic components, which have no slot and render their own content, and layout components, which hold other components through a slot. | A component that renders several pieces of content fixes their arrangement in your code. Moving one of them is a code change and a release. | From a design to components |
| Register a compound only when its pieces always appear together, in the same order, and nobody rearranges them. A compound renders several pieces of content in an arrangement fixed in your code. | Authors can't rearrange its pieces or reuse one of them on its own. Every change to the arrangement needs a release. | From a design to components |
| Build the bigger thing in Studio, as a section, from the registered pieces. | The arrangement is stored in your stack, not your repo, so authors can change it without a code release. | Build sections |
Why this matters. The blog's author card is a photo and a name inside a link. Registered as one AuthorCard component, it works on day one. But moving the photo below the name is then a code change, a review and a release, because the arrangement is inside the component. Registered as Avatar and Byline and arranged in Studio, the same change is a drag on the canvas. Both look the same on the page, but only the second lets authors make the change.
Register an interactive component whole when built-in components can't build it, such as a carousel, a form or a modal. Expose the props authors need.
Bind
| Rule | What breaks without it | Where it's explained |
|---|---|---|
Give each prop the type of the content it binds to, such as string for title and imageurl for image. | The prop shows Data Mismatch Found in the Settings tab, and Studio passes a fallback value instead of the bound one. | Component schema: props, types and defaults |
Type an imageurl prop as a string in your component, and bind it to a file field's url. | The Data Picker offers only text fields for an imageurl prop. Inside a Repeater, any field can be bound. Bound to the file itself, the component gets its fallback value with no message. | Component schema: props, types and defaults |
Type a prop object or array when the component needs the whole value. | With any other type, Studio unwraps an object that has one key down to that key's value, so the component doesn't get the object. | Component schema: props, types and defaults |
Render children through a slot prop. Never hardcode child components inside the component. | A hardcoded child has no row in Layers and no settings, so authors can't select, configure or bind it. | From a design to components |
Render
| Rule | What breaks without it | Where it's explained |
|---|---|---|
Give every prop a defaultValue, and render when a value is empty. | A prop with no value and no default gets its type's empty value, such as "" for a string or [] for an array. A newly dropped component can render blank, or throw if your code expects a value. | Component schema: props, types and defaults |
| In a list prop, handle items with missing fields. | In the editor, an empty list bound to content arrives as one empty item, [{}], so authors have something to work with. Code that expects every field throws. | Render a list |
Render string props as children, and don't call string methods on them. | When a string prop has a placeholder and no value, it arrives in the editor as a React element that shows the placeholder, and on your live site as null. | Component schema: props, types and defaults |
| Don't throw. | The component renders Component Loading Error with the error message, on the canvas and on your live site. The rest of the template still renders. | This page |
Select
The recommended shape is wrap: false, with studioAttributes spread on the component's root element. If you leave wrap out, Studio puts a <div> around your component and selects that instead. The <div> sits between a grid or flex parent and your root, so the parent's layout applies to the <div>, not to your component.
| Rule | What breaks without it | Where it's explained |
|---|---|---|
Register with wrap: false. | The parent's grid or flex layout applies to Studio's <div>, not to your component. | Register a component |
With wrap: false, spread studioAttributes on one root element. | Studio can't find the component on the canvas. Clicking it selects its parent. | Register a component |
| Make the root a DOM element, or forward the ref to one. | studioAttributes carries a ref. If it reaches something that's neither an element nor an object with an element property, the browser console logs "Reference is not a valid object. It should have a element property." | Register a component |
Take studioAttributes out of the props by name. Don't spread every prop onto the DOM. | React warns about an unknown studioAttributes prop on a DOM element. | This page |
| Spread the remaining attributes on the same root. The ID and Custom Attributes you set in the Settings tab arrive as props. | Your component drops them without a warning, so the ID and attributes never reach your HTML. | This page |
The spread is safe on your live site, where studioAttributes carries no selection attributes. Custom Attributes can't be class or style. Use the Design tab for those.
import { registerComponent, type StudioAttributes } from "@contentstack/studio-react";
import type { HTMLAttributes } from "react";
type BlogHeroProps = StudioAttributes &
HTMLAttributes<HTMLElement> & {
title: string;
summary: string;
image: string;
};
export function BlogHero({ studioAttributes, title, summary, image, ...attributes }: BlogHeroProps) {
return (
<section {...attributes} {...studioAttributes}>
<img src={image} alt="" />
<h1>{title}</h1>
<p>{summary}</p>
</section>
);
}
registerComponent({
type: "blog-hero",
displayName: "BlogHero",
component: BlogHero,
wrap: false,
sections: ["Blog · Components"],
props: {
title: { type: "string", defaultValue: "Post title" },
summary: { type: "string", defaultValue: "A short summary of the post." },
image: { type: "imageurl", defaultValue: "/images/blog-hero.jpg" },
},
});
className is one of the attributes, so the spread also applies the Design tab's classes to the root.
Style
These rules apply only where the Design tab appears: on freeform templates, and on every composition once Enable Freeform Feature is on for the project.
| Rule | What breaks without it | Where it's explained |
|---|---|---|
Apply className to the root element. | The Design tab shows "For design changes to take effect, the component must be configured to use the className property." | Make your components styleable |
| Apply each named style group's prop to the element it styles. | The same message, naming the group's prop. | Make your components styleable |
| Don't name a style group after a registered prop. | The prop's value replaces the group's classes, so that group's edits don't render. | Make your components styleable |
Edit
This applies only if authors edit bound values in Visual Editor.
| Rule | What breaks without it | Where it's explained |
|---|---|---|
Turn on edit tags with cslp: { appendTags: true } when you initialize the SDK, and spread each $ prop, such as $title, onto the element that renders that value. | Nothing errors. The value renders, but authors can't edit it in Visual Editor. | Make bound values editable in Visual Editor |
What you see when a component is the wrong shape
| What you see | Where | Rule it points to |
|---|---|---|
| Data Mismatch Found under a prop. Its tooltip reads "Expected prop type {expectedType} but received {receivedType} in the component registry. Remove the binding and bind with correct data." | Settings tab | Bind: the prop's type doesn't match the bound content. |
Data binding type mismatch in component "{component}": Expected prop "{field}" to be {expectedType} but received {receivedType}. Path: {dataPath} | Browser console, on your live site | Bind: the same mismatch. |
| "This content may not display correctly in this view. Please contact your development team to update the component so it can support this type of content." | Inside your component, on the canvas and on your live site | Bind: a string prop is bound to a group, a reference or a list. |
| "No properties available for the selected component." | Settings tab | Bind: the registration has no props. The Attributes controls don't show either. |
| A newly dropped component is blank. | Canvas | Render: props have no defaultValue. |
| Component Loading Error followed by an error message | Canvas and your live site | Render: the component threw. |
| Clicking the component selects its parent. | Canvas | Select: wrap: false without the studioAttributes spread. |
| "Reference is not a valid object. It should have a element property." | Browser console, in the editor | Select: the ref reached something that isn't an element. |
| The ID or a custom attribute is missing from the HTML. | Your HTML | Select: the attributes aren't spread on the root. |
| "For design changes to take effect, the component must be configured to use the className property." | Design tab | Style: className or a group's prop isn't applied. |
| A bound value can't be edited in Visual Editor. | Visual Editor | Edit: edit tags are off, or the $ prop isn't spread. |
To register a component that passes these rules, see Register a component.