Components define behavior, not just markup
A GrapesJS component is a typed model with defaults, traits (editable settings),
and an optional view. You register one with
DomComponents.addType. This guide builds a custom component with a
trait and shows how imported HTML maps onto it.
1. Register a component type
editor.DomComponents.addType('rating', {
// Map matching HTML onto this type when content is loaded.
isComponent: (el) => el.classList?.contains('rating') && { type: 'rating' },
model: {
defaults: {
tagName: 'div',
classes: ['rating'],
// Editable settings shown in the trait panel:
traits: [
{ type: 'number', name: 'stars', label: 'Stars', min: 1, max: 5 },
],
stars: 5,
},
},
});
2. React to trait changes in a view
editor.DomComponents.addType('rating', {
model: {
defaults: { tagName: 'div', classes: ['rating'], stars: 5,
traits: [{ type: 'number', name: 'stars', min: 1, max: 5 }] },
},
view: {
init() {
this.listenTo(this.model, 'change:stars', this.render);
},
onRender() {
const n = this.model.get('stars');
this.el.textContent = 'β
'.repeat(n) + 'β'.repeat(5 - n);
},
},
});
3. Expose it as a block
editor.BlockManager.add('rating', {
label: 'Rating',
category: 'Widgets',
content: { type: 'rating', stars: 4 },
});
Because isComponent recognises .rating elements, loading
saved HTML re-creates the component β traits and behavior intact.
Common mistakes
Custom components fail in predictable ways. If your isComponent doesn't return a truthy value (usually { type }) for matching DOM, loaded HTML won't map onto your type and traits disappear on reload. If the view doesn't listenTo the model's change:<trait> event, edits in the trait panel won't re-render. Re-rendering inside a handler that itself triggers a change can cause an infinite loop β update the DOM directly instead of calling a full render where possible. And set draggable/droppable deliberately so your component can only be placed where it makes sense.
Prerequisites
You need a running GrapesJS editor and basic JavaScript. Components are registered
with DomComponents.addType; the three pieces are detection
(isComponent), the model (defaults + traits), and an optional view
(canvas behaviour).
Model: defaults, traits, and constraints
The model defines what the component is. Beyond traits, you can constrain where it can
go and what it accepts with draggable, droppable, and
editable:
editor.DomComponents.addType('rating', {
isComponent: (el) => el.classList?.contains('rating') && { type: 'rating' },
model: {
defaults: {
tagName: 'div',
classes: ['rating'],
droppable: false,
traits: [{ type: 'number', name: 'stars', min: 1, max: 5 }],
stars: 5,
},
},
});
View: react to trait changes
view: {
init() { this.listenTo(this.model, 'change:stars', this.render); },
onRender() {
const n = this.model.get('stars');
this.el.textContent = 'β
'.repeat(n) + 'β'.repeat(5 - n);
},
}
Why isComponent matters
isComponent runs when HTML is loaded into the editor. Returning the type
object for matching elements is what lets saved markup re-hydrate into your component
with its traits and behaviour intact β without it, a reload turns your component back
into plain HTML.
Best practices
Keep the model declarative and put behaviour in the view. Avoid triggering a full
render inside a change handler that itself causes a change β update the DOM directly to
prevent loops. Set draggable/droppable deliberately so a
component can only be placed where it makes sense. Expose meaningful traits so editors
can configure the component without code.
Next steps
Surface components as draggable units in the custom blocks guide, bundle them in a custom plugin, or browse GrapesJS plugins on GJS.Market.
FAQ
How do I create a custom component in GrapesJS?
Call editor.DomComponents.addType(type, { isComponent, model, view })
β defining detection, defaults/traits, and canvas behavior.
What are traits?
Traits are the editable settings shown in the trait panel for a selected
component; declare them in model.defaults.traits.
How does isComponent work?
It receives a DOM element and returns a truthy value (often
{ type }) when that element should become your component, so imported
HTML maps onto your type.