# Custom blocks and component types in GrapesJS, packaged as a plugin

*Source: https://www.editorstack.cc/guides/grapesjs-custom-blocks — last updated 2026-09-17. Licensed CC BY 4.0 with attribution to EditorStack.*

*Tested with GrapesJS 0.23.5. The code on this page runs in CI from examples/guides/grapesjs-custom-blocks in the site repository.*

## Summary

- A block is what users drag; a component type is what the dropped element is. Define the type first (tag, attributes, traits, styles), then make blocks whose content references it.
- GrapesJS 0.23.1 introduced isParsedNode as the preferred recognition hook, but with the default browser parser the node's tagName arrives upper-case ('A'), so compare case-insensitively.
- Put a type's CSS in its styles default rather than calling Css.addRules in the plugin: the rules then travel with the component into the export.

## Blocks, component types and plugins

Three concepts, often blurred:

- A **component type** defines what an element *is*: its tag, default attributes, the traits users
  can edit in the settings panel, its CSS, and how existing markup is recognised as that type.
- A **block** is what appears in the Blocks panel. Its `content` is inserted on drop.
- A **plugin** is a function `(editor, options) => void` that registers both, so the same set can
  be reused across editors and versioned like any other module.

The example builds a CTA button type with three traits and two blocks. Blocks like these are the
"block library" line in our [build versus buy estimate](/use-cases/build-vs-buy-visual-editor), the
part users judge an editor on.

## The plugin

```ts title="cta-plugin.ts"
import type { Plugin } from 'grapesjs';

export type CtaPluginOptions = {
  /** Block category label in the Blocks panel. */
  category?: string;
};

const VARIANTS = [
  { id: 'primary', label: 'Primary' },
  { id: 'secondary', label: 'Secondary' },
];

export const ctaPlugin: Plugin<CtaPluginOptions> = (editor, options) => {
  const category = options.category ?? 'Marketing';

  // 1. A component type: what the element is, how it is recognised, what the user can edit.
  editor.Components.addType('cta-button', {
    // Recognise existing markup (pasted HTML, loaded templates) as this type.
    // With the default browser parser, tagName arrives upper-case ("A"): compare case-insensitively.
    isParsedNode: (node) =>
      node.tagName?.toLowerCase() === 'a' && (node.attributes?.class ?? '').split(/\s+/).includes('cta')
        ? { type: 'cta-button' }
        : undefined,
    model: {
      defaults: {
        tagName: 'a',
        attributes: { class: 'cta', href: '#', 'data-variant': 'primary' },
        components: 'Start free trial',
        // Component-scoped CSS: added with the first instance, exported with the page.
        styles: `
          .cta { display: inline-block; padding: 12px 22px; border-radius: 6px; font: 600 16px/1 system-ui, sans-serif; text-decoration: none; }
          .cta[data-variant="primary"] { background: #1f6feb; color: #fff; }
          .cta[data-variant="secondary"] { background: transparent; color: #1f6feb; border: 2px solid #1f6feb; }
        `,
        traits: [
          { type: 'text', name: 'href', label: 'Link' },
          { type: 'select', name: 'data-variant', label: 'Style', options: VARIANTS },
          { type: 'checkbox', name: 'target', label: 'New tab', valueTrue: '_blank', valueFalse: '' },
        ],
      },
    },
  });

  // 2. Blocks: what the user drags. One uses the component type, one is a composed section.
  editor.Blocks.add('cta-button', {
    label: 'CTA button',
    category,
    media: '<svg viewBox="0 0 24 24" width="32" height="32"><rect x="3" y="8" width="18" height="8" rx="2" fill="currentColor"/></svg>',
    content: { type: 'cta-button' },
  });

  editor.Blocks.add('cta-section', {
    label: 'CTA section',
    category,
    select: true,
    content: `<section class="cta-section" style="padding:48px 24px;text-align:center">
      <h2>Ready to start?</h2>
      <p>One sentence that removes the last objection.</p>
      <a class="cta" href="/signup" data-variant="primary">Create an account</a>
    </section>`,
  });
};
```

Notes on the choices:

- **`isParsedNode` with a case-insensitive tag check.** The GrapesJS documentation's example
  compares `node.tagName === 'input'`. When we logged the node in 0.23.5 with the default browser
  parser, `tagName` was `"A"` and our first version of this check never matched: loaded HTML came
  in as the built-in `link` type. The lower-cased comparison works; the attributes object had the
  `class` string as expected.
- **`styles` on the type.** The documentation describes component-scoped styles under "Components
  and CSS". The rules are added with the component and appear in `editor.getCss()`, which our test
  checks after editing.
- **Traits are attributes.** `href`, `data-variant` and `target` are edited directly; the CSS keys
  its variants off `data-variant`, so switching the select restyles the button with no JavaScript.
- **`content: { type: 'cta-button' }`** inserts the type with its defaults, while the section
  block uses HTML that the type recognises. Both paths end at the same component type.

## Registering it

```ts title="main.ts"
import grapesjs from 'grapesjs';
import 'grapesjs/dist/css/grapes.min.css';
import { ctaPlugin } from './cta-plugin';

const editor = grapesjs.init({
  container: '#gjs',
  height: '100vh',
  storageManager: false,
  plugins: [ctaPlugin],
  projectData: {
    pages: [{ component: '<main><h1>Pricing</h1><a class="cta" href="/buy">Buy now</a></main>' }],
  },
});

// Show the Blocks panel on load instead of the default Style Manager.
editor.onReady(() => editor.runCommand('core:open-blocks'));

// Exposed for the test harness.
Object.assign(window, { editor });
```

`plugins: [ctaPlugin]` works because the plugin needs no options. To pass options with type
checking, the GrapesJS documentation recommends the `usePlugin` helper; the
[email builder guide](/guides/grapesjs-email-builder) uses it with a third-party plugin.

## What our test checks

The verifier bundles the example and drives it in headless Chromium. It asserts that:

1. both blocks render in the Blocks panel under the "Marketing" category;
2. `<a class="cta">` in the initial project is recognised as `cta-button`;
3. the CTA block's content inserts a `cta-button` with `data-variant="primary"`;
4. with the component selected, typing into the Link trait input, choosing "secondary" in the
   Style select and ticking New tab update `href`, `data-variant` and `target="_blank"`;
5. the CTA inside the section block is recognised as `cta-button` too;
6. `getHtml()` and `getCss()` contain the edited attributes and the variant rule.

The drop gesture is simulated by inserting the block's content through the API, which is what a
drop inserts; the drag interaction itself is GrapesJS core behaviour and not what this guide
teaches. You can try real dragging in the [GrapesJS playground](/playground).

> **Sponsored — GJS.Market:** Ready-made GrapesJS block libraries — Writing every block by hand is the slow part. GJS.Market lists pre-built GrapesJS block libraries, free and paid. GJS.Market funds this site; check GrapesJS version support before you buy.

## What this guide does not cover

- **Custom views.** The type uses the default view. Components with their own rendering logic in
  the canvas need a `view` definition.
- **Scripts inside components.** Interactive components (tabs, sliders) need component scripts,
  documented separately under "Components and JS".
- **Migrations.** GrapesJS leaves values equal to a type's defaults out of stored project JSON
  (the `avoidDefaults` option, on by default), so changing a type's defaults later can change how
  existing projects load. We did not test that path; test it against saved projects before
  changing a shipped type. Stored projects are covered in
  [saving to a database](/guides/grapesjs-save-load-database).
- **React components as blocks.** GrapesJS blocks produce HTML. If your design-system components
  must be the blocks, [GrapesJS vs Craft.js](/compare/grapesjs-vs-craftjs) explains the
  alternative model.
- **Tailwind-styled blocks.** See [GrapesJS with Tailwind CSS](/guides/grapesjs-tailwind).


## Frequently asked questions

### What is the difference between a block and a component in GrapesJS?

A block is an entry in the Blocks panel with a label, a category and some content. A component is a node in the page, with a type that decides its tag, attributes, traits and behaviour. Dropping a block inserts its content, which can be plain HTML or a component definition such as { type: 'cta-button' }.

### Should I use isComponent or isParsedNode?

The GrapesJS documentation recommends isParsedNode for new component types from 0.23.1 onwards, and it takes priority when both exist. In the default browser parser the node it receives is a plain object with tagName in upper case and attributes as an object, so write checks that do not depend on case.

### How do traits map to HTML?

By default a trait's name is the attribute it edits. A text trait named href edits the href attribute; a select trait named data-variant edits data-variant; a checkbox trait writes valueTrue or valueFalse. Set changeProp: true to edit a component property instead of an attribute.


## Sources

1. [GrapesJS documentation: Component Manager (isParsedNode, styles)](https://grapesjs.com/docs/modules/Components.html) — accessed 2026-09-17
2. [GrapesJS documentation: Traits](https://grapesjs.com/docs/modules/Traits.html) — accessed 2026-09-17
3. [GrapesJS documentation: Block Manager](https://grapesjs.com/docs/modules/Blocks.html) — accessed 2026-09-17
4. [GrapesJS documentation: Plugins (usePlugin)](https://grapesjs.com/docs/modules/Plugins.html) — accessed 2026-09-17
